Skip to content

Commit

Permalink
lib: cockpit: split location into a separate class
Browse files Browse the repository at this point in the history
  • Loading branch information
jelly committed Nov 15, 2024
1 parent 377a3f3 commit 4ebff51
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 179 deletions.
181 changes: 2 additions & 179 deletions pkg/lib/cockpit.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
} from './cockpit/_internal/common';
import { Deferred, later_invoke } from './cockpit/_internal/deferred';
import { event_mixin } from './cockpit/_internal/event-mixin';
import { url_root, transport_origin, calculate_application, calculate_url } from './cockpit/_internal/location-utils';
import { transport_origin, calculate_application, calculate_url } from './cockpit/_internal/location-utils';
import { get_window_location_hash, Location } from 'cockpit/location';
import { ensure_transport, transport_globals } from './cockpit/_internal/transport';
import { FsInfoClient } from "./cockpit/fsinfo";

Expand Down Expand Up @@ -1117,184 +1118,6 @@ function factory() {

let last_loc = null;

function get_window_location_hash() {
return (window.location.href.split('#')[1] || '');
}

function Location() {
const self = this;
const application = cockpit.transport.application();
self.url_root = url_root || "";

if (window.mock?.url_root)
self.url_root = window.mock.url_root;

if (application.indexOf("cockpit+=") === 0) {
if (self.url_root)
self.url_root += '/';
self.url_root = self.url_root + application.replace("cockpit+", '');
}

const href = get_window_location_hash();
const options = { };
self.path = decode(href, options);

/* Resolve dots and double dots */
function resolve_path_dots(parts) {
const out = [];
const length = parts.length;
for (let i = 0; i < length; i++) {
const part = parts[i];
if (part === "" || part == ".") {
continue;
} else if (part == "..") {
if (out.length === 0)
return null;
out.pop();
} else {
out.push(part);
}
}
return out;
}

function decode_path(input) {
const parts = input.split('/').map(decodeURIComponent);
let result, i;
let pre_parts = [];

if (self.url_root)
pre_parts = self.url_root.split('/').map(decodeURIComponent);

if (input && input[0] !== "/") {
result = [].concat(self.path);
result.pop();
result = result.concat(parts);
} else {
result = parts;
}

result = resolve_path_dots(result);
for (i = 0; i < pre_parts.length; i++) {
if (pre_parts[i] !== result[i])
break;
}
if (i == pre_parts.length)
result.splice(0, pre_parts.length);

return result;
}

function encode(path, options, with_root) {
if (typeof path == "string")
path = decode_path(path);

let href = "/" + path.map(encodeURIComponent).join("/");
if (with_root && self.url_root && href.indexOf("/" + self.url_root + "/") !== 0)
href = "/" + self.url_root + href;

/* Undo unnecessary encoding of these */
href = href.replaceAll("%40", "@");
href = href.replaceAll("%3D", "=");
href = href.replaceAll("%2B", "+");
href = href.replaceAll("%23", "#");

let opt;
const query = [];
function push_option(v) {
query.push(encodeURIComponent(opt) + "=" + encodeURIComponent(v));
}

if (options) {
for (opt in options) {
let value = options[opt];
if (!Array.isArray(value))
value = [value];
value.forEach(push_option);
}
if (query.length > 0)
href += "?" + query.join("&");
}
return href;
}

function decode(href, options) {
if (href[0] == '#')
href = href.substr(1);

const pos = href.indexOf('?');
const first = (pos === -1) ? href : href.substr(0, pos);
const path = decode_path(first);
if (pos !== -1 && options) {
href.substring(pos + 1).split("&")
.forEach(function(opt) {
const parts = opt.split('=');
const name = decodeURIComponent(parts[0]);
const value = decodeURIComponent(parts[1]);
if (options[name]) {
let last = options[name];
if (!Array.isArray(value))
last = options[name] = [last];
last.push(value);
} else {
options[name] = value;
}
});
}

return path;
}

function href_for_go_or_replace(/* ... */) {
let href;
if (arguments.length == 1 && arguments[0] instanceof Location) {
href = String(arguments[0]);
} else if (typeof arguments[0] == "string") {
const options = arguments[1] || { };
href = encode(decode(arguments[0], options), options);
} else {
href = encode.apply(self, arguments);
}
return href;
}

function replace(/* ... */) {
if (self !== last_loc)
return;
const href = href_for_go_or_replace.apply(self, arguments);
window.location.replace(window.location.pathname + '#' + href);
}

function go(/* ... */) {
if (self !== last_loc)
return;
const href = href_for_go_or_replace.apply(self, arguments);
window.location.hash = '#' + href;
}

Object.defineProperties(self, {
path: {
enumerable: true,
writable: false,
value: self.path
},
options: {
enumerable: true,
writable: false,
value: options
},
href: {
enumerable: true,
value: href
},
go: { value: go },
replace: { value: replace },
encode: { value: encode },
decode: { value: decode },
toString: { value: function() { return href } }
});
}

Object.defineProperty(cockpit, "location", {
enumerable: true,
get: function() {
Expand Down
179 changes: 179 additions & 0 deletions pkg/lib/cockpit/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2024 Red Hat, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { url_root, calculate_application } from './_internal/location-utils';

/* HACK: Mozilla will unescape 'window.location.hash' before returning
* it, which is broken.
*
* https://bugzilla.mozilla.org/show_bug.cgi?id=135309
*/
export function get_window_location_hash() {
return (window.location.href.split('#')[1] || '');
}

export class Location {
path: string[];
href: string;
url_root: string;
options: any;

constructor() {
const application = calculate_application();
this.url_root = url_root || "";

if (window.mock?.url_root)
this.url_root = window.mock.url_root;

if (application.indexOf("cockpit+=") === 0) {
if (this.url_root)
this.url_root += '/';
this.url_root = this.url_root + application.replace("cockpit+", '');
}

this.href = get_window_location_hash();
this.options = {}; // untyped
this.path = this.decode(this.href, this.options);
}

#resolve_path_dots(parts) {
const out = [];
const length = parts.length;
for (let i = 0; i < length; i++) {
const part = parts[i];
if (part === "" || part == ".") {
continue;
} else if (part == "..") {
if (out.length === 0)
return null;
out.pop();
} else {
out.push(part);
}
}
return out;
}

href_for_go_or_replace(/* ... */) {
console.log("thisthing", this);
let href;
if (arguments.length == 1 && arguments[0] instanceof Location) {
href = String(arguments[0]);
} else if (typeof arguments[0] == "string") {
const options = arguments[1] || { };
href = this.encode(this.decode(arguments[0], options), options);
} else {
href = this.encode.apply(this, arguments);
}
return href;
}

decode_path(input) {
const parts = input.split('/').map(decodeURIComponent);
let result, i;
let pre_parts = [];

if (this.url_root)
pre_parts = this.url_root.split('/').map(decodeURIComponent);

if (input && input[0] !== "/") {
result = [].concat(this.path);
result.pop();
result = result.concat(parts);
} else {
result = parts;
}

result = this.#resolve_path_dots(result);
for (i = 0; i < pre_parts.length; i++) {
if (pre_parts[i] !== result[i])
break;
}
if (i == pre_parts.length)
result.splice(0, pre_parts.length);

return result;
}

encode(path: string, options, with_root: boolean = false) {
if (typeof path == "string")
path = this.decode_path(path);

let href = "/" + path.map(encodeURIComponent).join("/");
if (with_root && this.url_root && href.indexOf("/" + this.url_root + "/") !== 0)
href = "/" + this.url_root + href;

/* Undo unnecessary encoding of these */
href = href.replaceAll("%40", "@");
href = href.replaceAll("%3D", "=");
href = href.replaceAll("%2B", "+");
href = href.replaceAll("%23", "#");

const query: string[] = [];
if (options) {
for (const opt in options) {
let value = options[opt];
if (!Array.isArray(value))
value = [value];
value.forEach(function(v: string) {
query.push(encodeURIComponent(opt) + "=" + encodeURIComponent(v));
});
}
if (query.length > 0)
href += "?" + query.join("&");
}
return href;
}

decode(href: string, options) {
if (href[0] == '#')
href = href.substr(1);

const pos = href.indexOf('?');
const first = (pos === -1) ? href : href.substr(0, pos);
const path = this.decode_path(first);
if (pos !== -1 && options) {
href.substring(pos + 1).split("&")
.forEach(function(opt) {
const parts = opt.split('=');
const name = decodeURIComponent(parts[0]);
const value = decodeURIComponent(parts[1]);
if (options[name]) {
let last = options[name];
if (!Array.isArray(value))
last = options[name] = [last];

Check failure

Code scanning / CodeQL

Remote property injection High

A property name to write to depends on a
user-provided value
.
last.push(value);
} else {
options[name] = value;

Check failure

Code scanning / CodeQL

Remote property injection High

A property name to write to depends on a
user-provided value
.
}
});
}

return path;
}

replace(/* ... */) {
const href = this.href_for_go_or_replace.apply(this, arguments);
window.location.replace(window.location.pathname + '#' + href);
}

go(/* ... */) {
const href = this.href_for_go_or_replace.apply(this, arguments);
window.location.hash = '#' + href;
}
}

0 comments on commit 4ebff51

Please sign in to comment.