LCOV - code coverage report
Current view: top level - pkg/lib/cockpit/_internal - location.ts Coverage Total Hit
Test: cockpit Lines: 97.9 % 141 138
Test Date: 2026-06-17 06:28:00

            Line data    Source code
       1          127 : /*
       2              :  * Copyright (C) 2024 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: GPL-3.0-or-later
       4              :  */
       5              : 
       6              : /*
       7              :  * This is the "cockpit.location" API converted to TypeScript and moved out of
       8              :  * cockpit.js. In the future a new Location API should be designed to replace
       9              :  * the "cockpit.location" one and become importable from pkg/lib/cockpit as ESM module.
      10              :  */
      11              : 
      12              : import { url_root, calculate_application } from './location-utils';
      13              : 
      14              : type Options = { [name: string]: string | Array<string> };
      15              : type Path = string | string[] | Location;
      16              : 
      17          123 : export class Location {
      18              :     path: string[];
      19              :     href: string;
      20              :     url_root: string;
      21              :     options: Options;
      22          123 :     #hash_changed: boolean = false;
      23              : 
      24          123 :     constructor() {
      25          123 :         const application = calculate_application();
      26          121 :         this.url_root = url_root || "";
      27              : 
      28           19 :         if (window.mock?.url_root)
      29           19 :             this.url_root = window.mock.url_root;
      30              : 
      31           19 :         if (application.indexOf("cockpit+=") === 0) {
      32           19 :             if (this.url_root)
      33           19 :                 this.url_root += '/';
      34           19 :             this.url_root = this.url_root + application.replace("cockpit+", '');
      35           19 :         }
      36              : 
      37          123 :         this.href = window.location.hash.slice(1);
      38          123 :         this.options = {};
      39          123 :         this.path = this.decode(this.href, this.options);
      40          123 :     }
      41              : 
      42          134 :     #resolve_path_dots(parts: string[]): string[] {
      43          134 :         const out = [];
      44          134 :         for (let i = 0; i < parts.length; i++) {
      45          134 :             const part = parts[i];
      46          131 :             if (part === "" || part == ".") {
      47          134 :                 continue;
      48           30 :             } else if (part == "..") {
      49           30 :                 if (out.length === 0)
      50           30 :                     return [];
      51           30 :                 out.pop();
      52           30 :             } else {
      53          131 :                 out.push(part);
      54          131 :             }
      55          134 :         }
      56          134 :         return out;
      57          134 :     }
      58              : 
      59           40 :     #href_for_go_or_replace(path: Path, options?: Options): string {
      60           16 :         options = options || {};
      61           20 :         if (typeof path === "string") {
      62           20 :             return this.encode(this.decode(path, options), options);
      63            3 :         } else if (path instanceof Location) {
      64            3 :             return path.href;
      65            3 :         } else {
      66           29 :             return this.encode(path, options);
      67           29 :         }
      68           40 :     }
      69              : 
      70          134 :     #decode_path(input: string): string[] {
      71          134 :         let result, i;
      72          134 :         let pre_parts: string[] = [];
      73          134 :         const parts = input.split('/').map(decodeURIComponent);
      74              : 
      75          134 :         if (this.url_root)
      76           32 :             pre_parts = this.url_root.split('/').map(decodeURIComponent);
      77              : 
      78           33 :         if (input && input[0] !== "/" && this.path !== undefined) {
      79           33 :             result = [...this.path];
      80           33 :             result.pop();
      81           33 :             result = result.concat(parts);
      82           33 :         } else {
      83          134 :             result = parts;
      84          134 :         }
      85              : 
      86          134 :         result = this.#resolve_path_dots(result);
      87           32 :         for (i = 0; i < pre_parts.length; i++) {
      88           32 :             if (pre_parts[i] !== result[i])
      89           32 :                 break;
      90           32 :         }
      91          134 :         if (i == pre_parts.length)
      92          134 :             result.splice(0, pre_parts.length);
      93              : 
      94          134 :         return result;
      95          134 :     }
      96              : 
      97          131 :     encode(path: string | string[], options: Options, with_root: boolean = false): string {
      98          131 :         if (typeof path == "string")
      99           30 :             path = this.#decode_path(path);
     100              : 
     101          131 :         let href = "/" + path.map(encodeURIComponent).join("/");
     102           32 :         if (with_root && this.url_root && href.indexOf("/" + this.url_root + "/") !== 0)
     103           32 :             href = "/" + this.url_root + href;
     104              : 
     105              :         /* Undo unnecessary encoding of these */
     106          131 :         href = href.replaceAll("%40", "@");
     107          131 :         href = href.replaceAll("%3D", "=");
     108          131 :         href = href.replaceAll("%2B", "+");
     109          131 :         href = href.replaceAll("%23", "#");
     110              : 
     111          131 :         const query: string[] = [];
     112           66 :         if (options) {
     113           51 :             for (const opt of Object.keys(options).sort()) {
     114           51 :                 let value = options[opt];
     115           51 :                 if (!Array.isArray(value))
     116           51 :                     value = [value];
     117           23 :                 value.forEach(function(v: string) {
     118           23 :                     query.push(encodeURIComponent(opt) + "=" + encodeURIComponent(v));
     119           23 :                 });
     120           51 :             }
     121           66 :             if (query.length > 0)
     122           51 :                 href += "?" + query.join("&");
     123           66 :         }
     124          131 :         return href;
     125          131 :     }
     126              : 
     127              :     // NOTE: The options argument is modified in place
     128          134 :     decode(href: string, options: Options): string[] {
     129          134 :         if (href[0] == '#')
     130           30 :             href = href.substring(1);
     131              : 
     132          134 :         const pos = href.indexOf('?');
     133           54 :         const first = (pos === -1) ? href : href.substring(0, pos);
     134          134 :         const path = this.#decode_path(first);
     135           54 :         if (pos !== -1 && options) {
     136           54 :             href.substring(pos + 1).split("&")
     137           27 :                     .forEach(function(opt) {
     138           27 :                         const parts = opt.split('=');
     139           27 :                         const name = decodeURIComponent(parts[0]);
     140           27 :                         const value = decodeURIComponent(parts[1]);
     141            2 :                         if (options[name]) {
     142            2 :                             let last = options[name];
     143            2 :                             if (!Array.isArray(last))
     144            2 :                                 last = options[name] = [last];
     145            2 :                             last.push(value);
     146            2 :                         } else {
     147           27 :                             options[name] = value;
     148           27 :                         }
     149           27 :                     });
     150           54 :         }
     151              : 
     152          134 :         return path;
     153          134 :     }
     154              : 
     155            3 :     replace(path: Path, options?: Options) {
     156            3 :         if (this.#hash_changed)
     157            3 :             return;
     158            3 :         const href = this.#href_for_go_or_replace(path, options);
     159            3 :         window.location.replace(window.location.pathname + '#' + href);
     160            3 :     }
     161              : 
     162           38 :     go(path: Path, options?: Options) {
     163           38 :         if (this.#hash_changed)
     164           38 :             return;
     165           38 :         const hash = '#' + this.#href_for_go_or_replace(path, options);
     166              : 
     167            3 :         if (navigator.userAgent.includes("Firefox")) {
     168              :             // At least Firefox 146 and 147 will (sometimes) trigger a
     169              :             // full force reload of the page when setting
     170              :             // window.location.hash.  We can work around that by
     171              :             // clicking on a link.
     172              :             //
     173              :             // https://bugzilla.mozilla.org/show_bug.cgi?id=2018546
     174              : 
     175            3 :             const a = document.createElement('a');
     176            3 :             a.href = hash;
     177            3 :             document.body.appendChild(a);
     178            3 :             a.click();
     179            3 :             document.body.removeChild(a);
     180            3 :         } else {
     181           38 :             window.location.hash = hash;
     182           38 :         }
     183           38 :     }
     184              : 
     185           43 :     invalidate() {
     186           43 :         this.#hash_changed = true;
     187           43 :     }
     188              : 
     189            0 :     toString() {
     190            0 :         return this.href;
     191            0 :     }
     192          127 : }
        

Generated by: LCOV version 2.0-1