├── .gitignore ├── LICENSE ├── profile └── chrome │ ├── CSS │ ├── agent_style.uc.css │ └── author_style.uc.css │ ├── JS │ ├── test.uc.js │ ├── userChrome_ag_css.sys.mjs │ └── userChrome_au_css.uc.js │ ├── resources │ ├── userChrome.ag.css │ └── userChrome.au.css │ └── utils │ ├── boot.sys.mjs │ ├── chrome.manifest │ ├── fs.sys.mjs │ ├── module_loader.mjs │ ├── uc_api.sys.mjs │ └── utils.sys.mjs ├── program ├── config.js └── defaults │ └── pref │ └── config-prefs.js ├── readme.md ├── test_profile └── chrome │ ├── resources │ ├── ico.png │ ├── test_file.txt │ ├── test_json.json │ └── write_test_basic.txt │ ├── tests │ ├── .sys.mjs │ ├── .uc.js │ ├── 000_test_runner.sys.mjs │ ├── aaa_test_script.uc.js │ ├── legacy_tests.uc.js │ ├── modules │ │ └── imported_esm.sys.mjs │ ├── sys.mjs │ ├── test_1.js │ ├── test_2.mjs │ ├── test_3.uc.js.txt │ ├── test_4.sys.mjs.txt │ ├── test_5uc.js │ ├── test_6sys.mjs │ ├── test_7.uc.jss │ ├── test_8.sys.mjss │ ├── test_manifest.manifest │ ├── test_manifest.uc.js │ ├── test_mjs.uc.mjs │ ├── test_module_script.sys.mjs │ ├── test_module_script.uc.js │ ├── uc.js │ ├── utils_tests.uc.mjs │ ├── write_to_shared.uc.js │ ├── x_disabled_script.uc.js │ └── x_disabled_system.sys.mjs │ ├── userChrome.css │ └── utils │ └── chrome.manifest ├── test_tb_profile └── chrome │ ├── resources │ ├── test_file.txt │ ├── test_json.json │ └── write_test_basic.txt │ ├── tests │ ├── 000_test_runner.sys.mjs │ ├── aaa_test_script.uc.js │ ├── test_module_script.uc.js │ └── utils_tests.uc.js │ ├── userChrome.css │ └── utils │ └── chrome.manifest ├── types ├── api │ ├── FileSystem.d.ts │ ├── Hotkeys.d.ts │ ├── Notifications.d.ts │ ├── Prefs.d.ts │ ├── Runtime.d.ts │ ├── Scripts.d.ts │ ├── Utils.d.ts │ └── Windows.d.ts ├── index.d.ts ├── package.json └── tsconfig.json └── uc_utils_old.md /.gitignore: -------------------------------------------------------------------------------- 1 | /test_profile/* 2 | !/test_profile/chrome 3 | /test_tb_profile/* 4 | !/test_tb_profile/chrome -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /profile/chrome/CSS/agent_style.uc.css: -------------------------------------------------------------------------------- 1 | /* ==UserScript== 2 | // @name agent style sheet 3 | // @description an example for @stylemode directive 4 | // @stylemode agent_sheet 5 | // ==/UserScript== 6 | */ 7 | -------------------------------------------------------------------------------- /profile/chrome/CSS/author_style.uc.css: -------------------------------------------------------------------------------- 1 | /* ==UserScript== 2 | // @description author style injected to browser.xhtml 3 | // @long-description 4 | /* 5 | Author style example. 6 | This creates a small dot to appear in the top left corner of devtools-button popup - but not in any other panel. 7 | 8 | Note: You don't have to have *any* header, if no @includes are declared then the style will affect browser.xhtml in author mode - like this style. 9 | Note: The header starts with /* instead of // 10 | Note: The header must end with a closing comment sequence. 11 | Note: You need to re-open comment if you use @long-description 12 | 13 | Like this: 14 | *//* 15 | // @usefileuri 16 | // @name example style 17 | // ==/UserScript==*/ 18 | 19 | /* Default to xul namespace */ 20 | @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); 21 | 22 | #customizationui-widget-panel[viewId="PanelUI-developer-tools"]::part(content)::before{ 23 | content: ""; 24 | display: flex; 25 | position: relative; 26 | z-index: 2; 27 | width: 9px; 28 | margin-inline-end: -9px; 29 | background-image: radial-gradient(currentcolor,currentcolor,transparent 90%); 30 | background-size: 5px 5px; 31 | background-position: 4px 4px; 32 | background-repeat: no-repeat; 33 | } 34 | -------------------------------------------------------------------------------- /profile/chrome/JS/test.uc.js: -------------------------------------------------------------------------------- 1 | console.log("Hi mom, I'm loaded!"); -------------------------------------------------------------------------------- /profile/chrome/JS/userChrome_ag_css.sys.mjs: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name userChrome_agent_css 3 | // @namespace userChrome_Agent_Sheet_CSS 4 | // @version 0.0.7 5 | // @description Load userChrome.ag.css as agent sheet from resources folder using chrome: uri 6 | // ==/UserScript== 7 | 8 | (function () { 9 | let sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService); 10 | 11 | // Try to load userChrome.ag.css as agent sheet 12 | // WARNING - agent sheets loaded like this affect each and every document you load including web sites. So be careful with your custom styles. 13 | 14 | try{ 15 | sss.loadAndRegisterSheet(Services.io.newURI("chrome://userChrome/content/userChrome.ag.css"), sss.AGENT_SHEET); 16 | }catch(e){ 17 | console.error(`Could not load userChrome.ag.css: ${e.name}`) 18 | } 19 | })(); -------------------------------------------------------------------------------- /profile/chrome/JS/userChrome_au_css.uc.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name userChrome_author_css 3 | // @namespace userChrome_Author_Sheet_CSS 4 | // @version 0.0.6 5 | // @description Load userChrome.au.css file as author sheet from resources folder using chrome: uri. The file is loaded only into the document where this script runs which by default is browser.xhtml 6 | // @onlyonce 7 | // ==/UserScript== 8 | 9 | (function () { 10 | // Store and preload the author style sheet 11 | const sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService); 12 | const sheet = sss.preloadSheet(makeURI("chrome://userChrome/content/userChrome.au.css"), sss.AUTHOR_SHEET); 13 | // Inject the preloaded style sheet to current window 14 | try{ 15 | window.windowUtils.addSheet(sheet,Ci.nsIDOMWindowUtils.AUTHOR_SHEET); 16 | }catch(e){ 17 | console.error(`Could not pre-load userChrome.au.css: ${e.name}`) 18 | } 19 | // Register a window created callback that injects the preloaded style sheet into that window global 20 | UC_API.Windows.onCreated(win => { 21 | try{ 22 | win.windowUtils.addSheet(sheet,Ci.nsIDOMWindowUtils.AUTHOR_SHEET); 23 | }catch(e){ 24 | console.error(`Could not pre-load userChrome.au.css: ${e.name}`) 25 | } 26 | }); 27 | })(); -------------------------------------------------------------------------------- /profile/chrome/resources/userChrome.ag.css: -------------------------------------------------------------------------------- 1 | /* This example style is loaded globally as agent sheet. That means it will affect every document that is loaded including web sites so you really want to constrain your styles to apply only where you want. Thus, the default namespace is set to xul to make rules apply to xul elements. Should you want to apply rules to html elements, you need to use the namespace selector like: html|input */ 2 | 3 | /* Default to xul namespace */ 4 | @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); 5 | 6 | /* Allow selecting html elements with named namespace selector */ 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | /* Demonstrates namespace selector - this would only affect tooltip elements that are html elements. But those don't exist so this does nothing. If you remove the "html|" prefix, then all your tooltips will be black */ 10 | html|tooltip { 11 | -moz-appearance: none; 12 | background-color: rgb(15, 17, 34); 13 | color: rgba(255, 255, 255, 1); 14 | border: none; 15 | padding: 5px; 16 | } 17 | -------------------------------------------------------------------------------- /profile/chrome/resources/userChrome.au.css: -------------------------------------------------------------------------------- 1 | /* This example file is supposed to be loaded as author style sheet. Notice that we can use ::part() ::host() etc. selectors here while they are unavailable in user stylesheets. */ 2 | 3 | /* Default to xul namespace */ 4 | @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); 5 | 6 | /* Allow selecting html elements with named namespace selector */ 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | /* Example - this creates a small dot to appear in the top left corner of devtools-button popup - but not in any other panel */ 10 | #customizationui-widget-panel[viewId="PanelUI-developer-tools"]::part(content)::before{ 11 | content: ""; 12 | display: flex; 13 | position: relative; 14 | z-index: 2; 15 | width: 9px; 16 | margin-inline-end: -9px; 17 | background-image: radial-gradient(currentcolor,currentcolor,transparent 90%); 18 | background-size: 5px 5px; 19 | background-position: 4px 4px; 20 | background-repeat: no-repeat; 21 | } 22 | -------------------------------------------------------------------------------- /profile/chrome/utils/boot.sys.mjs: -------------------------------------------------------------------------------- 1 | import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 2 | import { loaderModuleLink, Pref, FileSystem, windowUtils, showNotification, startupFinished, restartApplication, escapeXUL, toggleScript } from "chrome://userchromejs/content/utils.sys.mjs"; 3 | 4 | const FX_AUTOCONFIG_VERSION = "0.10.5"; 5 | console.warn( "Browser is executing custom scripts via autoconfig" ); 6 | 7 | const APP_VARIANT = (() => { 8 | let is_tb = AppConstants.BROWSER_CHROME_URL.startsWith("chrome://messenger"); 9 | return { 10 | THUNDERBIRD: is_tb, 11 | FIREFOX: !is_tb 12 | } 13 | })(); 14 | const BRAND_NAME = AppConstants.MOZ_APP_DISPLAYNAME_DO_NOT_USE; 15 | 16 | const BROWSERCHROME = (() => { 17 | if(APP_VARIANT.FIREFOX){ 18 | return AppConstants.BROWSER_CHROME_URL 19 | } 20 | return "chrome://messenger/content/messenger.xhtml" 21 | })(); 22 | 23 | const PREF_ENABLED = 'userChromeJS.enabled'; 24 | const PREF_SCRIPTSDISABLED = 'userChromeJS.scriptsDisabled'; 25 | 26 | function getDisabledScripts(){ 27 | return Services.prefs.getStringPref(PREF_SCRIPTSDISABLED,"").split(",") 28 | } 29 | 30 | const MODULE_LOADER = new (function(){ 31 | let compiledScript = null; 32 | let promise = ChromeUtils.compileScript("chrome://userchromejs/content/module_loader.mjs"); 33 | promise.then(s => { compiledScript = s }); 34 | 35 | this.ready = () => { 36 | if(compiledScript){ 37 | return Promise.resolve(compiledScript) 38 | } 39 | return promise 40 | } 41 | return this 42 | })(); 43 | 44 | class ScriptData { 45 | #preLoadedStyle; 46 | #chromeURI; 47 | #isRunning = false; 48 | #injectionFailed = false; 49 | constructor(leafName, headerText, noExec, isStyle){ 50 | const hasLongDescription = (/^\/\/\ @long-description/im).test(headerText); 51 | this.filename = leafName; 52 | this.name = headerText.match(/\/\/ @name\s+(.+)\s*$/im)?.[1]; 53 | this.charset = headerText.match(/\/\/ @charset\s+(.+)\s*$/im)?.[1]; 54 | this.description = hasLongDescription 55 | ? headerText.match(/\/\/ @description\s+.*?\/\*\s*(.+?)\s*\*\//is)?.[1] 56 | : headerText.match(/\/\/ @description\s+(.+)\s*$/im)?.[1]; 57 | this.version = headerText.match(/\/\/ @version\s+(.+)\s*$/im)?.[1]; 58 | this.author = headerText.match(/\/\/ @author\s+(.+)\s*$/im)?.[1]; 59 | this.icon = headerText.match(/\/\/ @icon\s+(.+)\s*$/im)?.[1]; 60 | this.homepageURL = headerText.match(/\/\/ @homepageURL\s+(.+)\s*$/im)?.[1]; 61 | this.downloadURL = headerText.match(/\/\/ @downloadURL\s+(.+)\s*$/im)?.[1]; 62 | this.updateURL = headerText.match(/\/\/ @updateURL\s+(.+)\s*$/im)?.[1]; 63 | this.optionsURL = headerText.match(/\/\/ @optionsURL\s+(.+)\s*$/im)?.[1]; 64 | this.id = headerText.match(/\/\/ @id\s+(.+)\s*$/im)?.[1] 65 | || `${leafName.split('.uc.js')[0]}@${this.author||'userChromeJS'}`; 66 | this.isESM = this.filename.endsWith(".mjs"); 67 | this.onlyonce = /\/\/ @onlyonce\b/.test(headerText); 68 | this.inbackground = this.filename.endsWith(".sys.mjs") || /\/\/ @backgroundmodule\b/.test(headerText); 69 | this.ignoreCache = /\/\/ @ignorecache\b/.test(headerText); 70 | this.manifest = headerText.match(/\/\/ @manifest\s+(.+)\s*$/im)?.[1]; 71 | this.type = isStyle ? "style" : "script"; 72 | this.styleSheetMode = isStyle 73 | ? headerText.match(/\/\/ @stylemode\s+(.+)\s*$/im)?.[1] === "agent_sheet" 74 | ? "agent" : "author" 75 | : null; 76 | this.useFileURI = /\/\/ @usefileuri\b/.test(headerText); 77 | this.noExec = isStyle || noExec; 78 | 79 | if(this.inbackground || this.styleSheetMode === "agent" || (!isStyle && noExec)){ 80 | this.regex = null; 81 | this.loadOrder = -1; 82 | }else{ 83 | // Construct regular expression to use to match target document 84 | let match, rex = { 85 | include: [], 86 | exclude: [] 87 | }; 88 | let findNextRe = /^\/\/ @(include|exclude)\s+(.+)\s*$/gm; 89 | while (match = findNextRe.exec(headerText)) { 90 | rex[match[1]].push( 91 | match[2].replace(/^main$/i, BROWSERCHROME).replace(/\*/g, '.*?') 92 | ); 93 | } 94 | if (!rex.include.length) { 95 | rex.include.push(BROWSERCHROME); 96 | } 97 | let exclude = rex.exclude.length ? `(?!${rex.exclude.join('$|')}$)` : ''; 98 | this.regex = new RegExp(`^${exclude}(${rex.include.join('|') || '.*'})$`,'i'); 99 | let loadOrder = headerText.match(/\/\/ @loadOrder\s+(\d+)\s*$/im)?.[1]; 100 | this.loadOrder = Number.parseInt(loadOrder) || 10; 101 | } 102 | 103 | Object.freeze(this); 104 | } 105 | get isEnabled() { 106 | return getDisabledScripts().indexOf(this.filename) === -1; 107 | } 108 | get injectionFailed(){ 109 | return this.#injectionFailed 110 | } 111 | get isRunning(){ 112 | return this.#isRunning 113 | } 114 | setRunning(){ 115 | this.#isRunning = true 116 | } 117 | markScriptInjectionFailure(){ 118 | this.#injectionFailed = true 119 | } 120 | get chromeURI(){ 121 | if(!this.#chromeURI){ 122 | this.#chromeURI = this.type === "style" 123 | ? Services.io.newURI(`chrome://userstyles/skin/${this.filename}`) 124 | : Services.io.newURI(`chrome://userscripts/content/${this.filename}`) 125 | } 126 | return this.#chromeURI 127 | } 128 | get referenceURI(){ 129 | return this.useFileURI && this.type === "style" 130 | ? FileSystem.convertChromeURIToFileURI(this.chromeURI) 131 | : this.chromeURI 132 | } 133 | get preLoadedStyle(){ 134 | return this.#preLoadedStyle 135 | } 136 | static preLoadAuthorStyle(aStyle){ 137 | if(aStyle.#injectionFailed){ 138 | console.warn(`ignoring style preload for ${aStyle.filename} because it has already failed`); 139 | return false 140 | } 141 | let sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService); 142 | try{ 143 | // Try to preload the file and store it 144 | aStyle.#preLoadedStyle = sss.preloadSheet(aStyle.referenceURI, sss.AUTHOR_SHEET); 145 | }catch(e){ 146 | console.error(`Could not pre-load ${aStyle.filename}: ${e.name}`) 147 | return false 148 | } 149 | aStyle.#isRunning = true; 150 | return true 151 | } 152 | static tryLoadStyleIntoWindow(aStyle,win){ 153 | if(aStyle.styleSheetMode !== "author" || !aStyle.regex?.test(win.location.href)){ 154 | return 155 | } 156 | if(!aStyle.#preLoadedStyle){ 157 | let success = ScriptData.preLoadAuthorStyle(aStyle); 158 | if(!success){ 159 | return 160 | } 161 | } 162 | win.windowUtils.addSheet(aStyle.#preLoadedStyle,Ci.nsIDOMWindowUtils.AUTHOR_SHEET); 163 | return 164 | } 165 | static markScriptRunning(aScript){ 166 | aScript.#isRunning = true; 167 | } 168 | static injectClassicScriptIntoGlobal(aScript,aGlobal){ 169 | try{ 170 | Services.scriptloader.loadSubScriptWithOptions( 171 | aScript.chromeURI.spec, 172 | { 173 | target: aGlobal, 174 | ignoreCache: aScript.ignoreCache 175 | } 176 | ) 177 | aScript.#isRunning = true; 178 | return Promise.resolve(1) 179 | }catch(ex){ 180 | aScript.#injectionFailed = true; 181 | return Promise.reject(ex) 182 | } 183 | } 184 | static registerScriptManifest(aScript){ 185 | if(aScript.#isRunning){ 186 | return 187 | } 188 | let cmanifest = FileSystem.getEntry(FileSystem.convertChromeURIToFileURI(`chrome://userscripts/content/${aScript.manifest}.manifest`)); 189 | if(cmanifest.isFile()){ 190 | Components.manager 191 | .QueryInterface(Ci.nsIComponentRegistrar).autoRegister(cmanifest.entry()); 192 | }else{ 193 | console.warn(`Script '${aScript.filename}' tried to register a manifest but requested file '${aScript.manifest}' doesn't exist`); 194 | } 195 | } 196 | static extractScriptHeader(aFSResult){ 197 | return aFSResult.content() 198 | .match(/^\/\/ ==UserScript==\s*[\n\r]+(?:.*[\n\r]+)*?\/\/ ==\/UserScript==\s*/m)?.[0] || "" 199 | } 200 | static extractStyleHeader(aFSResult){ 201 | return aFSResult.content() 202 | .match(/^\/\* ==UserScript==\s*[\n\r]+(?:.*[\n\r]+)*?\/\/ ==\/UserScript==\s*\*\//m)?.[0] || "" 203 | } 204 | static fromScriptFile(aFile){ 205 | if(aFile.fileSize < 24){ 206 | // Smaller files can't possibly have a valid header 207 | // This also means that we successfully generate a ScriptData for *folders* named "xx.uc.js"... 208 | return new ScriptData(aFile.leafName,"",aFile.fileSize === 0,false) 209 | } 210 | const result = FileSystem.readNSIFileSyncUncheckedWithOptions(aFile,{ metaOnly: true }); 211 | const headerText = this.extractScriptHeader(result); 212 | // If there are less than 2 bytes after the header then we mark the script as non-executable. This means that if the file only has a header then we don't try to inject it to any windows, since it wouldn't do anything. 213 | return new ScriptData(aFile.leafName, headerText, headerText.length > aFile.fileSize - 2,false); 214 | } 215 | static fromStyleFile(aFile){ 216 | if(aFile.fileSize < 24){ 217 | // Smaller files can't possibly have a valid header 218 | return new ScriptData(aFile.leafName,"",true,true) 219 | } 220 | const result = FileSystem.readNSIFileSyncUncheckedWithOptions(aFile,{ metaOnly: true }); 221 | return new ScriptData(aFile.leafName, this.extractStyleHeader(result), true,true); 222 | } 223 | } 224 | 225 | Pref.setIfUnset(PREF_ENABLED,true); 226 | Pref.setIfUnset(PREF_SCRIPTSDISABLED,""); 227 | 228 | // This is called if _previous_ startup was broken 229 | function showgBrowserNotification(){ 230 | Services.prefs.setBoolPref('userChromeJS.gBrowser_hack.enabled',true); 231 | showNotification( 232 | { 233 | label : "fx-autoconfig: Something was broken in last startup", 234 | type : "fx-autoconfig-gbrowser-notification", 235 | priority: "critical", 236 | buttons: [{ 237 | label: "Why am I seeing this?", 238 | callback: (notification) => { 239 | notification.ownerGlobal.openWebLinkIn( 240 | "https://github.com/MrOtherGuy/fx-autoconfig#startup-error", 241 | "tab" 242 | ); 243 | return false 244 | } 245 | }] 246 | } 247 | ) 248 | } 249 | 250 | // This is called if startup somehow takes over 5 seconds 251 | function maybeShowBrokenNotification(window){ 252 | if(window.isFullyOccluded && "gBrowser" in window){ 253 | console.log("Window was fully occluded, no need to panic") 254 | return 255 | } 256 | let aNotificationBox = window.gNotificationBox; 257 | aNotificationBox.appendNotification( 258 | "fx-autoconfig-broken-notification", 259 | { 260 | label: "fx-autoconfig: Startup might be broken", 261 | image: "chrome://browser/skin/notification-icons/popup.svg", 262 | priority: "critical" 263 | } 264 | ); 265 | } 266 | 267 | 268 | 269 | function updateMenuStatus(event){ 270 | const menu = event.target; 271 | if(!menu.id === "menuUserScriptsPopup"){ 272 | return 273 | } 274 | let disabledScripts = getDisabledScripts(); 275 | for(let item of menu.children){ 276 | if(item.getAttribute("type") != "checkbox"){ 277 | continue 278 | } 279 | if (disabledScripts.includes(item.dataset.filename)){ 280 | item.removeAttribute("checked"); 281 | }else{ 282 | item.setAttribute("checked","true"); 283 | } 284 | } 285 | } 286 | 287 | class UserChrome_js{ 288 | constructor(){ 289 | this.scripts = []; 290 | this.styles = []; 291 | this.SESSION_RESTORED = false; 292 | this.IS_ENABLED = Services.prefs.getBoolPref(PREF_ENABLED,false); 293 | this.isInitialWindow = true; 294 | this.initialized = false; 295 | this.init(); 296 | } 297 | registerScript(aScript,isDisabled){ 298 | if(aScript.type === "script"){ 299 | this.scripts.push(aScript); 300 | }else{ 301 | this.styles.push(aScript); 302 | } 303 | if(!isDisabled && aScript.manifest){ 304 | try{ 305 | ScriptData.registerScriptManifest(aScript); 306 | }catch(ex){ 307 | console.error(new Error(`@ ${aScript.filename}`,{cause:ex})); 308 | } 309 | } 310 | return isDisabled 311 | } 312 | init(){ 313 | if(this.initialized){ 314 | return 315 | } 316 | loaderModuleLink.setup(this,FX_AUTOCONFIG_VERSION,AppConstants.MOZ_APP_DISPLAYNAME_DO_NOT_USE,APP_VARIANT,ScriptData); 317 | 318 | if(!this.IS_ENABLED){ 319 | Services.obs.addObserver(this, 'domwindowopened', false); 320 | this.initialized = true; 321 | return 322 | } 323 | // gBrowserHack setup 324 | this.GBROWSERHACK_ENABLED = 325 | (Services.prefs.getBoolPref("userChromeJS.gBrowser_hack.required",false) ? 2 : 0) 326 | + (Services.prefs.getBoolPref("userChromeJS.gBrowser_hack.enabled",false) ? 1 : 0); 327 | this.PERSISTENT_DOMCONTENT_CALLBACK = Services.prefs.getBoolPref("userChromeJS.persistent_domcontent_callback",false); 328 | const disabledScripts = getDisabledScripts(); 329 | // load script data 330 | const scriptDir = FileSystem.getScriptDir(); 331 | if(scriptDir.isDirectory()){ 332 | for(let entry of scriptDir){ 333 | if (/^[A-Za-z0-9]+.*(\.uc\.js|\.uc\.mjs|\.sys\.mjs)$/i.test(entry.leafName)) { 334 | let script = ScriptData.fromScriptFile(entry); 335 | if(this.registerScript(script,disabledScripts.includes(script.filename))){ 336 | continue // script is disabled 337 | } 338 | if(script.inbackground){ 339 | try{ 340 | if(script.isESM){ 341 | ChromeUtils.importESModule( script.chromeURI.spec ); 342 | ScriptData.markScriptRunning(script); 343 | }else{ 344 | console.warn(`Refusing to import legacy jsm style backgroundmodule script: ${script.filename} - convert to ES6 modules instead`); 345 | } 346 | }catch(ex){ 347 | console.error(new Error(`@ ${script.filename}:${ex.lineNumber}`,{cause:ex})); 348 | } 349 | } 350 | } 351 | } 352 | } 353 | const styleDir = FileSystem.getStyleDir(); 354 | if(styleDir.isDirectory()){ 355 | for(let entry of styleDir){ 356 | if (/^[A-Za-z0-9]+.*\.uc\.css$/i.test(entry.leafName)) { 357 | let style = ScriptData.fromStyleFile(entry); 358 | this.registerScript(style,!disabledScripts.includes(style.filename)); 359 | } 360 | } 361 | this.addAgentStyles(this.styles.filter(style => style.styleSheetMode === "agent" && !disabledScripts.includes(style.filename))); 362 | } 363 | this.scripts.sort((a,b) => a.loadOrder - b.loadOrder); 364 | this.styles.sort((a,b) => a.loadOrder - b.loadOrder); 365 | Services.obs.addObserver(this, 'domwindowopened', false); 366 | this.initialized = true; 367 | 368 | } 369 | addAgentStyles(agentStyles){ 370 | if(agentStyles.length > 0){ 371 | let sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService); 372 | for(let style of agentStyles){ 373 | try{ 374 | sss.loadAndRegisterSheet(style.referenceURI, sss.AGENT_SHEET); 375 | ScriptData.markScriptRunning(style); 376 | }catch(e){ 377 | console.error(`Could not load ${style.filename}: ${e.name}`); 378 | } 379 | } 380 | } 381 | } 382 | onDOMContent(document){ 383 | const window = document.defaultView; 384 | if(!(/^chrome:(?!\/\/global\/content\/(commonDialog|alerts\/alert)\.xhtml)|about:(?!blank)/i).test(window.location.href)){ 385 | // Don't inject scripts to modal prompt windows or notifications 386 | if(this.IS_ENABLED && this.styles.length > 0){ 387 | const disabledScripts = getDisabledScripts(); 388 | for(let style of this.styles){ 389 | if(!disabledScripts.includes(style.filename)){ 390 | ScriptData.tryLoadStyleIntoWindow(style,window) 391 | } 392 | } 393 | } 394 | return 395 | } 396 | ChromeUtils.defineLazyGetter(window,"UC_API",() => 397 | ChromeUtils.importESModule("chrome://userchromejs/content/uc_api.sys.mjs") 398 | ) 399 | if(this.IS_ENABLED){ 400 | document.allowUnsafeHTML = false; // https://bugzilla.mozilla.org/show_bug.cgi?id=1432966 401 | 402 | // This is a hack to make gBrowser available for scripts. 403 | // Without it, scripts would need to check if gBrowser exists and deal 404 | // with it somehow. See bug 1443849 405 | const _gb = APP_VARIANT.FIREFOX && "_gBrowser" in window; 406 | if(this.GBROWSERHACK_ENABLED && _gb){ 407 | window.gBrowser = window._gBrowser; 408 | }else if(_gb && this.isInitialWindow){ 409 | this.isInitialWindow = false; 410 | let timeout = window.setTimeout(() => { 411 | maybeShowBrokenNotification(window); 412 | },5000); 413 | windowUtils.waitWindowLoading(window) 414 | .then(() => { 415 | // startup is fine, clear timeout 416 | window.clearTimeout(timeout); 417 | }) 418 | } 419 | // Inject scripts to window 420 | const disabledScripts = getDisabledScripts(); 421 | // Note, sys.mjs scripts have .regex = null 422 | const scriptsForWindow = this.scripts.filter(s => s.regex?.test(window.location.href)); 423 | 424 | // .uc.mjs scripts are loaded via module loader 425 | if(scriptsForWindow.some(s => s.isESM && !disabledScripts.includes(s.filename))){ 426 | MODULE_LOADER.ready().then(m => m.executeInGlobal(window)); 427 | } 428 | 429 | for(let script of scriptsForWindow){ 430 | if(script.isESM || disabledScripts.includes(script.filename) || script.injectionFailed || script.noExec || (script.onlyonce && script.isRunning)) { 431 | continue 432 | } 433 | ScriptData.injectClassicScriptIntoGlobal(script,window) 434 | } 435 | for(let style of this.styles){ 436 | if(!disabledScripts.includes(style.filename)){ 437 | ScriptData.tryLoadStyleIntoWindow(style,window) 438 | } 439 | } 440 | } 441 | if(window.isChromeWindow){ 442 | const menu = document.querySelector( 443 | APP_VARIANT.FIREFOX ? "#menu_openDownloads" : "menuitem#addressBook"); 444 | if(menu){ 445 | menu.parentNode.addEventListener("popupshown", 446 | (ev) => this.generateScriptMenuItemsIfNeeded(ev.target.ownerDocument), 447 | {once: true} 448 | ); 449 | } 450 | } 451 | } 452 | 453 | // Add simple script menu to menubar tools popup 454 | generateScriptMenuItemsIfNeeded(aDoc){ 455 | { 456 | let menu = aDoc.getElementById("userScriptsMenu"); 457 | if(menu){ 458 | return menu 459 | } 460 | } 461 | const popup = aDoc.querySelector( 462 | APP_VARIANT.FIREFOX ? "#menu_openDownloads" : "menuitem#addressBook")?.parentNode; 463 | 464 | if(aDoc.location.href !== BROWSERCHROME || !popup){ 465 | return null 466 | } 467 | const window = aDoc.ownerGlobal; 468 | 469 | window.MozXULElement.insertFTLIfNeeded("toolkit/about/aboutSupport.ftl"); 470 | let menuFragment = window.MozXULElement.parseXULToFragment(` 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | `); 480 | const itemsFragment = window.MozXULElement.parseXULToFragment(""); 481 | for(let script of this.scripts){ 482 | UserChrome_js.appendScriptMenuitemToFragment(window,itemsFragment,script); 483 | } 484 | if(this.styles.length){ 485 | itemsFragment.append(aDoc.createXULElement("menuseparator")); 486 | for(let style of this.styles){ 487 | UserChrome_js.appendScriptMenuitemToFragment(window,itemsFragment,style); 488 | } 489 | } 490 | if(!this.IS_ENABLED){ 491 | itemsFragment.append(window.MozXULElement.parseXULToFragment('')); 492 | } 493 | let menupopup = menuFragment.getElementById("menuUserScriptsPopup"); 494 | menupopup.prepend(itemsFragment); 495 | popup.prepend(menuFragment); 496 | menupopup.addEventListener("popupshown",updateMenuStatus); 497 | menupopup.addEventListener("command",ev => { 498 | switch(ev.target.id){ 499 | case "userScriptsMenu-OpenFolder": 500 | FileSystem.getScriptDir().showInFileManager(); 501 | break; 502 | case "userScriptsMenu-Restart": 503 | restartApplication(false); 504 | break; 505 | case "userScriptsMenu-ClearCache": 506 | restartApplication(true); 507 | break; 508 | default: 509 | if(ev.target.dataset.filename){ 510 | toggleScript(ev.target.dataset.filename); 511 | } 512 | } 513 | }); 514 | aDoc.l10n.formatValues(["restart-button-label","clear-startup-cache-label","show-dir-label"]) 515 | .then(values => { 516 | let baseTitle = `${values[0]} ${BRAND_NAME}`; 517 | aDoc.getElementById("userScriptsMenu-Restart").setAttribute("label", baseTitle); 518 | aDoc.getElementById("userScriptsMenu-ClearCache").setAttribute("label", values[1].replace("…","") + " & " + baseTitle); 519 | aDoc.getElementById("userScriptsMenu-OpenFolder").setAttribute("label",values[2]) 520 | }); 521 | return popup.querySelector("#userScriptsMenu"); 522 | } 523 | static appendScriptMenuitemToFragment(aWindow,aFragment,aScript){ 524 | aFragment.append( 525 | aWindow.MozXULElement.parseXULToFragment(` 526 | 530 | 531 | `) 532 | ); 533 | return 534 | } 535 | observe(aSubject, aTopic, aData) { 536 | aSubject.addEventListener('DOMContentLoaded', this, {once: !this.PERSISTENT_DOMCONTENT_CALLBACK, capture: true}); 537 | } 538 | 539 | handleEvent(aEvent){ 540 | switch (aEvent.type){ 541 | case "DOMContentLoaded": 542 | this.onDOMContent(aEvent.originalTarget); 543 | break; 544 | default: 545 | console.warn(new Error("unexpected event received",{cause:aEvent})); 546 | } 547 | } 548 | 549 | } 550 | 551 | const _ucjs = !Services.appinfo.inSafeMode && new UserChrome_js(); 552 | _ucjs && startupFinished().then(() => { 553 | _ucjs.SESSION_RESTORED = true; 554 | _ucjs.GBROWSERHACK_ENABLED === 2 && showgBrowserNotification(); 555 | if(Pref.setIfUnset("userChromeJS.firstRunShown",true)){ 556 | showNotification({ 557 | type: "fx-autoconfig-installed", 558 | label: `fx-autoconfig: ${BRAND_NAME} is being modified with custom autoconfig scripting` 559 | }); 560 | } 561 | }); 562 | -------------------------------------------------------------------------------- /profile/chrome/utils/chrome.manifest: -------------------------------------------------------------------------------- 1 | content userchromejs ./ 2 | content userscripts ../JS/ 3 | skin userstyles classic/1.0 ../CSS/ 4 | content userchrome ../resources/ 5 | -------------------------------------------------------------------------------- /profile/chrome/utils/fs.sys.mjs: -------------------------------------------------------------------------------- 1 | export class FileSystem{ 2 | static RESULT_CONTENT = Symbol("Content"); 3 | static RESULT_DIRECTORY = Symbol("Directory"); 4 | static RESULT_ERROR = Symbol("Error"); 5 | static RESULT_FILE = Symbol("File"); 6 | 7 | static getFileURIForFile(aEntry, type){ 8 | let qi = Services.io.getProtocolHandler('file').QueryInterface(Ci.nsIFileProtocolHandler); 9 | if(type === FileSystem.RESULT_DIRECTORY){ 10 | return qi.getURLSpecFromDir(aEntry) 11 | } 12 | if(type === FileSystem.RESULT_FILE){ 13 | return qi.getURLSpecFromActualFile(aEntry) 14 | } 15 | throw ResultError.fromKind(FileSystem.ERROR_KIND_INVALID_ARGUMENT,{expected: "FileSystem.RESULT_FILE | FileSystem.RESULT_DIRECTORY"}) 16 | } 17 | 18 | static convertChromeURIToFileURI(aURI){ 19 | const registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry); 20 | return registry.convertChromeURL( 21 | aURI instanceof Ci.nsIURI 22 | ? aURI 23 | : Services.io.newURI(aURI) 24 | ); 25 | } 26 | // Call to .parent is needed because chrome urls get implicit "filename" based on the provider 27 | static #SCRIPT_URI; 28 | static #STYLE_URI; 29 | static #RESOURCE_URI; 30 | static{ 31 | this.#RESOURCE_URI = FileSystem.getFileURIForFile( 32 | FileSystem.convertChromeURIToFileURI('chrome://userchrome/content/') 33 | .QueryInterface(Ci.nsIFileURL).file.parent, 34 | FileSystem.RESULT_DIRECTORY 35 | ); 36 | this.#SCRIPT_URI = FileSystem.getFileURIForFile( 37 | FileSystem.convertChromeURIToFileURI('chrome://userscripts/content/') 38 | .QueryInterface(Ci.nsIFileURL).file.parent, 39 | FileSystem.RESULT_DIRECTORY 40 | ); 41 | this.#STYLE_URI = FileSystem.getFileURIForFile( 42 | FileSystem.convertChromeURIToFileURI('chrome://userstyles/skin/') 43 | .QueryInterface(Ci.nsIFileURL).file.parent, 44 | FileSystem.RESULT_DIRECTORY 45 | ); 46 | } 47 | 48 | static get SCRIPT_URI(){ 49 | return Services.io.newURI(FileSystem.#SCRIPT_URI) 50 | } 51 | 52 | static get STYLE_URI(){ 53 | return Services.io.newURI(FileSystem.#STYLE_URI) 54 | } 55 | 56 | static get RESOURCE_URI(){ 57 | return Services.io.newURI(FileSystem.#RESOURCE_URI) 58 | } 59 | 60 | static getResourceDir(){ 61 | return FileSystemResult.fromNsIFile(FileSystem.RESOURCE_URI.QueryInterface(Ci.nsIFileURL).file) 62 | } 63 | 64 | static getScriptDir(){ 65 | return FileSystemResult.fromNsIFile(FileSystem.SCRIPT_URI.QueryInterface(Ci.nsIFileURL).file) 66 | } 67 | 68 | static getStyleDir(){ 69 | return FileSystemResult.fromNsIFile(FileSystem.STYLE_URI.QueryInterface(Ci.nsIFileURL).file) 70 | } 71 | 72 | static #getEntryFromString(aFilename, baseFileURI){ 73 | let baseDirectory = baseFileURI.QueryInterface(Ci.nsIFileURL).file; 74 | if(typeof aFilename !== "string"){ 75 | return FileSystemResult.fromErrorKind(FileSystem.ERROR_KIND_INVALID_ARGUMENT,{expected:"String"}); 76 | } 77 | const parts = aFilename.replace("\\","/").split("/").filter(a => a.length > 0); 78 | while(parts[0] === ".."){ 79 | baseDirectory = baseDirectory.parent; 80 | parts.shift(); 81 | } 82 | try{ 83 | for(let part of parts){ 84 | baseDirectory.append(part) 85 | } 86 | }catch(ex){ 87 | return FileSystemResult.fromErrorKind(FileSystem.ERROR_KIND_INVALID_ARGUMENT,{type:"Invalid path"}) 88 | } 89 | return FileSystemResult.fromNsIFile(baseDirectory) 90 | } 91 | 92 | static getEntry(aFilename, options = {}){ 93 | if(aFilename instanceof Ci.nsIURI){ 94 | if(aFilename.scheme === "chrome"){ 95 | return FileSystemResult.fromNsIFile(FileSystem.convertChromeURIToFileURI(aFilename).QueryInterface(Ci.nsIFileURL).file) 96 | } 97 | if(aFilename.scheme === "file"){ 98 | return FileSystemResult.fromNsIFile(aFilename.QueryInterface(Ci.nsIFileURL).file) 99 | } 100 | throw new Error("unsupported nsIURI conversion") 101 | } 102 | return FileSystem.#getEntryFromString(aFilename, options.baseDirectory || FileSystem.RESOURCE_URI) 103 | } 104 | static readNSIFileSyncUncheckedWithOptions(aFile,options){ 105 | let stream = Cc['@mozilla.org/network/file-input-stream;1'].createInstance(Ci.nsIFileInputStream); 106 | let cvstream = Cc['@mozilla.org/intl/converter-input-stream;1'].createInstance(Ci.nsIConverterInputStream); 107 | try{ 108 | stream.init(aFile, 0x01, 0, 0); 109 | cvstream.init(stream, 'UTF-8', 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); 110 | }catch(e){ 111 | console.error(e); 112 | cvstream.close(); 113 | stream.close(); 114 | return FileSystemResult.fromErrorKind(FileSystem.ERROR_KIND_NOT_READABLE,{cause: e, filename: aFile.leafName}) 115 | } 116 | let rv = {content:'',path: FileSystem.getFileURIForFile(aFile,FileSystem.RESULT_FILE)}; 117 | let data = {}; 118 | const metaOnly = !!options.metaOnly; 119 | while (cvstream.readString(4096, data)) { 120 | rv.content += data.value; 121 | if (metaOnly && rv.content.indexOf('// ==/UserScript==') > 0) { 122 | break; 123 | } 124 | } 125 | cvstream.close(); 126 | stream.close(); 127 | 128 | return FileSystemResult.fromContent(rv) 129 | } 130 | static readFileSync(aFile, options = {}) { 131 | if(typeof aFile === "string"){ 132 | const fsResult = FileSystem.#getEntryFromString(aFile, FileSystem.RESOURCE_URI); 133 | if(fsResult.isFile()){ 134 | return FileSystem.readNSIFileSyncUncheckedWithOptions(fsResult.entry(),options); 135 | } 136 | return fsResult.isError() 137 | ? fsResult 138 | : FileSystemResult.fromErrorKind(FileSystem.ERROR_KIND_NOT_FILE,{topic: aFile}) 139 | } 140 | if(aFile instanceof Ci.nsIFile){ 141 | return FileSystem.readNSIFileSyncUncheckedWithOptions(aFile,options); 142 | } 143 | throw ResultError.fromKind(FileSystem.ERROR_KIND_INVALID_ARGUMENT,{expected: "string | Ci.nsIFile"}) 144 | } 145 | static async readFile(aPath){ 146 | if(typeof aPath !== "string"){ 147 | throw ResultError.fromKind(FileSystem.ERROR_KIND_INVALID_ARGUMENT,{expected: "string"}) 148 | } 149 | try{ 150 | let path = FileSystem.#appendToBaseURI(aPath); 151 | return FileSystemResult.fromContent({ content: await IOUtils.readUTF8(path), path: PathUtils.toFileURI(path) }) 152 | }catch(ex){ 153 | return FileSystemResult.fromErrorKind(FileSystem.ERROR_KIND_NOT_READABLE,{cause: ex}) 154 | } 155 | } 156 | static async readJSON(path){ 157 | try{ 158 | let result = await FileSystem.readFile(path); 159 | return result.isError() 160 | ? null 161 | : JSON.parse(result.content()) 162 | }catch(ex){ 163 | console.error(ex) 164 | } 165 | return null 166 | } 167 | static #appendToBaseURI(aPath,aFileURI){ 168 | // Normally, this API can only write into resources directory 169 | // Writing outside of resources can be enabled using following pref 170 | const disallowUnsafeWrites = !Services.prefs.getBoolPref("userChromeJS.allowUnsafeWrites",false); 171 | 172 | const baseURI = aFileURI || FileSystem.RESOURCE_URI; 173 | let baseParts = PathUtils.split(baseURI.QueryInterface(Ci.nsIFileURL).file.path); 174 | let pathParts = aPath.split(/[\\\/]/); 175 | while(pathParts[0] === ".."){ 176 | baseParts.pop(); 177 | pathParts.shift(); 178 | if(disallowUnsafeWrites){ 179 | throw ResultError.fromKind(FileSystem.ERROR_KIND_NOT_ALLOWED) 180 | } 181 | } 182 | return PathUtils.join(...baseParts.concat(pathParts)) 183 | } 184 | static async writeFile(path, content, options = {}){ 185 | if(!path || typeof path !== "string"){ 186 | throw ResultError.fromKind(FileSystem.ERROR_KIND_INVALID_ARGUMENT,{expected: "string"}) 187 | } 188 | if(typeof content !== "string"){ 189 | throw ResultError.fromKind(FileSystem.ERROR_KIND_INVALID_ARGUMENT,{expected: "string"}) 190 | } 191 | const fileName = FileSystem.#appendToBaseURI(path); 192 | if(!options.tmpPath){ 193 | options.tmpPath = fileName + ".tmp"; 194 | } 195 | return IOUtils.writeUTF8( fileName, content, options ); 196 | } 197 | static createFileURI(fileName){ 198 | if(!fileName){ 199 | return FileSystem.#RESOURCE_URI 200 | } 201 | return FileSystem.convertChromeURIToFileURI(`chrome://userchrome/content/${fileName}`).spec 202 | } 203 | static chromeDir(){ 204 | return FileSystemResult.fromDirectory(Services.dirsvc.get('UChrm',Ci.nsIFile)) 205 | } 206 | static StringContent(obj){ 207 | return FileSystemResult.fromContent(obj) 208 | } 209 | static ERROR_KIND_NOT_EXIST = 1; 210 | static ERROR_KIND_NOT_DIRECTORY = 2; 211 | static ERROR_KIND_NOT_FILE = 3; 212 | static ERROR_KIND_NOT_CONTENT = 4; 213 | static ERROR_KIND_UNKNOWN_RESULT = 5; 214 | static ERROR_KIND_INVALID_ARGUMENT = 6; 215 | static ERROR_KIND_NOT_READABLE = 7; 216 | static ERROR_KIND_NOT_ALLOWED = 8; 217 | } 218 | 219 | class ResultError extends Error{ 220 | 221 | constructor(kind,options,info = {}){ 222 | super(ResultError.toMessage(kind,info),options); 223 | this.kind = kind; 224 | this.name = "ResultError"; 225 | } 226 | static toMessage(kind,info){ 227 | const strInfo = ResultError.parseInfo(info); 228 | switch(kind){ 229 | case FileSystem.ERROR_KIND_NOT_EXIST: 230 | return `Entry doesn't exist: ${strInfo}` 231 | case FileSystem.ERROR_KIND_NOT_DIRECTORY: 232 | return `Result is not a directory: ${strInfo}` 233 | case FileSystem.ERROR_KIND_NOT_FILE: 234 | return `Result is not a file: ${strInfo}` 235 | case FileSystem.ERROR_KIND_NOT_CONTENT: 236 | return `Result is not content: ${strInfo}` 237 | case FileSystem.ERROR_KIND_UNKNOWN_RESULT: 238 | return `Unknown result type: ${strInfo}` 239 | case FileSystem.ERROR_KIND_INVALID_ARGUMENT: 240 | return `Invalid argument: ${strInfo}` 241 | case FileSystem.ERROR_KIND_NOT_READABLE: 242 | return `File stream is not readable: ${strInfo}` 243 | case FileSystem.ERROR_KIND_NOT_ALLOWED: 244 | return "Writing outside of resources directory is not allowed" 245 | default: 246 | return "Unknown error" 247 | } 248 | } 249 | static parseInfo(aInfo){ 250 | return Object.entries(aInfo).map(a => `${a[0]}: ${a[1]}`).join("; ") 251 | } 252 | static fromKind(aKind,info){ 253 | return info instanceof ResultError 254 | ? info 255 | : new ResultError(aKind,{},info) 256 | } 257 | } 258 | 259 | class FileSystemResult{ 260 | #result; 261 | #type; 262 | #fileuri; 263 | constructor(data,resultType){ 264 | this.#result = data; 265 | this.#type = resultType; 266 | } 267 | 268 | get fileURI(){ 269 | if(this.isError()){ 270 | return null 271 | } 272 | if(!this.#fileuri){ 273 | this.#fileuri = FileSystemResult.#getFileURI(this) 274 | } 275 | return this.#fileuri 276 | } 277 | content(replaceNewlines){ 278 | if(this.isContent()){ 279 | return replaceNewlines 280 | ? this.#result.content.replace(/\r\n?/g, '\n') 281 | : this.#result.content 282 | } 283 | throw ResultError.fromKind(FileSystem.ERROR_KIND_NOT_CONTENT,{type:this.#type.description}) 284 | } 285 | get size(){ 286 | return this.isContent() 287 | ? this.#result.content.length 288 | : this.#result.fileSize 289 | } 290 | entry(){ 291 | if(this.isDirectory() || this.isFile()){ 292 | return this.#result 293 | } 294 | throw ResultError.fromKind(FileSystem.ERROR_KIND_NOT_EXIST,FileSystemResult.#generateErrorInfo(this)) 295 | } 296 | error(){ 297 | return this.isError() 298 | ? this.#result 299 | : null 300 | } 301 | readSync(){ 302 | if(!this.isFile()){ 303 | throw ResultError.fromKind(FileSystem.ERROR_KIND_NOT_FILE,FileSystemResult.#generateErrorInfo(this)) 304 | } 305 | return FileSystem.readNSIFileSyncUncheckedWithOptions(this.#result,{}).content() 306 | } 307 | read(){ 308 | if(!this.isFile()){ 309 | return Promise.reject(ResultError.fromKind(FileSystem.ERROR_KIND_NOT_FILE,FileSystemResult.#generateErrorInfo(this))) 310 | } 311 | return IOUtils.readUTF8(this.#result.path) 312 | } 313 | get type(){ 314 | return this.#type 315 | } 316 | isContent(){ 317 | return this.#type === FileSystem.RESULT_CONTENT 318 | } 319 | isFile(){ 320 | return this.#type === FileSystem.RESULT_FILE 321 | } 322 | isDirectory(){ 323 | return this.#type === FileSystem.RESULT_DIRECTORY 324 | } 325 | isError(){ 326 | return this.#type === FileSystem.RESULT_ERROR 327 | } 328 | [Symbol.iterator](){ 329 | try{ 330 | return this.entries() 331 | }catch(e){ 332 | console.warn(e) 333 | } 334 | return { next() { return { done: true } } } 335 | }; 336 | entries(){ 337 | if(!this.isDirectory()){ 338 | throw ResultError.fromKind(FileSystem.ERROR_KIND_NOT_DIRECTORY,FileSystemResult.#generateErrorInfo(this)) 339 | } 340 | let enumerator = this.#result.directoryEntries.QueryInterface(Ci.nsISimpleEnumerator); 341 | return { 342 | next() { 343 | return enumerator.hasMoreElements() 344 | ? { 345 | value: enumerator.getNext().QueryInterface(Ci.nsIFile), 346 | done: false 347 | } 348 | : { done: true } 349 | }, 350 | [Symbol.iterator]() { 351 | return this; 352 | }, 353 | }; 354 | } 355 | showInFileManager(){ 356 | try{ 357 | if(this.isFile()){ 358 | this.#result.reveal(); 359 | return true 360 | } 361 | if(this.isDirectory()){ 362 | this.#result.launch(); 363 | return true 364 | } 365 | }catch(ex){ 366 | console.error("Could not open file manager for: " + this.#result.leafName); 367 | } 368 | return false 369 | } 370 | static #generateErrorInfo(aResult){ 371 | if(aResult.isError()){ 372 | return aResult.#result 373 | } 374 | return { 375 | topic: aResult.isContent() 376 | ? aResult.#result.path 377 | : aResult.#result.leafName 378 | } 379 | } 380 | static #getFileURI(aResult){ 381 | if(aResult.isContent()){ 382 | return aResult.#result.path 383 | } 384 | return FileSystem.getFileURIForFile(aResult.#result,aResult.#type) 385 | } 386 | static fromDirectory(dir){ 387 | return new FileSystemResult(dir, FileSystem.RESULT_DIRECTORY) 388 | } 389 | static fromContent(content){ 390 | return new FileSystemResult(content, FileSystem.RESULT_CONTENT) 391 | } 392 | static fromErrorKind(aKind,aErrorDescription){ 393 | return new FileSystemResult(ResultError.fromKind(aKind,aErrorDescription), FileSystem.RESULT_ERROR) 394 | } 395 | static fromFile(file){ 396 | return new FileSystemResult(file, FileSystem.RESULT_FILE) 397 | } 398 | static fromNsIFile(entry){ 399 | if(!entry.exists()){ 400 | return FileSystemResult.fromErrorKind(FileSystem.ERROR_KIND_NOT_EXIST,{topic: entry.leafName}) 401 | } 402 | if(entry.isDirectory()){ 403 | return FileSystemResult.fromDirectory(entry) 404 | }else if(entry.isFile()){ 405 | return FileSystemResult.fromFile(entry) 406 | } 407 | return FileSystemResult.fromErrorKind(FileSystem.ERROR_KIND_UNKNOWN_RESULT,{topic: entry.leafName}) 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /profile/chrome/utils/module_loader.mjs: -------------------------------------------------------------------------------- 1 | { 2 | const PREF_SCRIPTSDISABLED = 'userChromeJS.scriptsDisabled'; 3 | 4 | let { loaderModuleLink } = ChromeUtils.importESModule("chrome://userchromejs/content/utils.sys.mjs"); 5 | 6 | let disabledScripts = Services.prefs.getStringPref(PREF_SCRIPTSDISABLED,"").split(","); 7 | 8 | let moduleScripts = loaderModuleLink.scripts 9 | .filter(s => s.isESM 10 | && s.regex?.test(window.location.href) 11 | && !disabledScripts.includes(s.filename) 12 | && !s.noExec 13 | && !(s.onlyonce && s.isRunning) 14 | && !s.injectionFailed 15 | ); 16 | for(let script of moduleScripts){ 17 | import(script.chromeURI.spec) 18 | .catch(ex => { 19 | console.error(new Error(`@ ${script.filename}:${ex.lineNumber}`,{cause:ex})); 20 | script.markScriptInjectionFailure(); 21 | }) 22 | .finally(()=>script.setRunning()) 23 | } 24 | } -------------------------------------------------------------------------------- /profile/chrome/utils/uc_api.sys.mjs: -------------------------------------------------------------------------------- 1 | const { 2 | Hotkey, 3 | windowUtils, 4 | SharedGlobal, 5 | Pref, 6 | FileSystem, 7 | restartApplication, 8 | startupFinished, 9 | createElement, 10 | createWidget, 11 | escapeXUL, 12 | loadURI, 13 | loaderModuleLink, 14 | getScriptData, 15 | getStyleData, 16 | parseStringAsScriptInfo, 17 | toggleScript, 18 | updateStyleSheet, 19 | showNotification 20 | } = ChromeUtils.importESModule("chrome://userchromejs/content/utils.sys.mjs"); 21 | 22 | export { 23 | FileSystem, 24 | Hotkey as Hotkeys, 25 | Pref as Prefs, 26 | SharedGlobal as SharedStorage, 27 | windowUtils as Windows 28 | } 29 | 30 | export const Runtime = Object.freeze({ 31 | appVariant: loaderModuleLink.variant.THUNDERBIRD 32 | ? "Thunderbird" 33 | : "Firefox", 34 | brandName: loaderModuleLink.brandName, 35 | config: null, 36 | restart: restartApplication, 37 | startupFinished: startupFinished, 38 | loaderVersion: loaderModuleLink.version 39 | }); 40 | 41 | export const Utils = Object.freeze({ 42 | createElement: createElement, 43 | createWidget: createWidget, 44 | escapeXUL: escapeXUL, 45 | loadURI: loadURI 46 | }); 47 | 48 | export const Scripts = Object.freeze({ 49 | getScriptData: getScriptData, 50 | getStyleData: getStyleData, 51 | getScriptMenuForDocument(doc){ 52 | return doc.getElementById("userScriptsMenu") || loaderModuleLink.getScriptMenu(doc) 53 | }, 54 | openScriptDir(){ 55 | FileSystem.getScriptDir().showInFileManager() 56 | }, 57 | openStyleDir(){ 58 | FileSystem.getStyleDir().showInFileManager() 59 | }, 60 | parseStringAsScriptInfo: parseStringAsScriptInfo, 61 | toggleScript: toggleScript, 62 | reloadStyleSheet: updateStyleSheet 63 | }); 64 | 65 | export const Notifications = Object.freeze({ 66 | show(def){ 67 | showNotification(def) 68 | } 69 | }); -------------------------------------------------------------------------------- /profile/chrome/utils/utils.sys.mjs: -------------------------------------------------------------------------------- 1 | import { FileSystem } from "chrome://userchromejs/content/fs.sys.mjs"; 2 | export { FileSystem }; 3 | export const SharedGlobal = {}; 4 | ChromeUtils.defineLazyGetter(SharedGlobal,"widgetCallbacks",() => {return new Map()}); 5 | const lazy = { 6 | startupPromises: new Set() 7 | }; 8 | ChromeUtils.defineESModuleGetters(lazy,{ 9 | CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs" 10 | }); 11 | 12 | export class Hotkey{ 13 | #matchingSelector; 14 | constructor(hotkeyDetails,commandDetails){ 15 | this.command = commandDetails; 16 | this.trigger = hotkeyDetails; 17 | this.#matchingSelector = null; 18 | } 19 | get matchingSelector(){ 20 | if(!this.#matchingSelector){ 21 | let trigger = this.trigger; 22 | this.#matchingSelector = `key[modifiers="${trigger.modifiers}"][${trigger.key?'key="'+trigger.key:'keycode="'+trigger.keycode}"]` 23 | } 24 | return this.#matchingSelector 25 | } 26 | async autoAttach(opt){ 27 | const suppress = opt?.suppressOriginal || false; 28 | await startupFinished(); 29 | for (let window of windowUtils.getAll()){ 30 | if(window.document.getElementById(this.trigger.id)){ 31 | continue 32 | } 33 | this.attachToWindow(window,{suppressOriginal: suppress}) 34 | } 35 | windowUtils.onCreated(win => { 36 | windowUtils.isBrowserWindow(win) && this.attachToWindow(win,{suppressOriginal: suppress}) 37 | }) 38 | } 39 | async attachToWindow(window,opt = {}){ 40 | await windowUtils.waitWindowLoading(window); 41 | if(opt.suppressOriginal){ 42 | this.suppressOriginalKey(window) 43 | } 44 | Hotkey.#createKey(window.document,this.trigger); 45 | if(this.command){ 46 | Hotkey.#createCommand(window.document,this.command); 47 | } 48 | } 49 | suppressOriginalKey(window){ 50 | let oldKey = window.document.querySelector(this.matchingSelector); 51 | if(oldKey){ 52 | oldKey.setAttribute("disabled","true") 53 | } 54 | } 55 | restoreOriginalKey(window){ 56 | let oldKey = window.document.querySelector(this.matchingSelector); 57 | oldKey.removeAttribute("disabled"); 58 | } 59 | static #createKey(doc,details){ 60 | let keySet = doc.getElementById("ucKeySet"); 61 | if(!keySet){ 62 | keySet = createElement(doc,"keyset",{id:"ucKeySet"}); 63 | doc.body.appendChild(keySet); 64 | } 65 | 66 | let key = createElement(doc,"key",details); 67 | keySet.appendChild(key); 68 | return 69 | } 70 | static #createCommand(doc,details){ 71 | let commandSet = doc.getElementById("ucCommandSet"); 72 | if(!commandSet){ 73 | commandSet = createElement(doc,"commandset",{id:"ucCommandSet"}); 74 | doc.body.insertBefore(commandSet,doc.body.firstChild); 75 | } 76 | if(doc.getElementById(details.id)){ 77 | console.warn("Fx-autoconfig: command with id '"+details.id+"' already exists"); 78 | return 79 | } 80 | let command = createElement(doc,"command",{id: details.id}); 81 | commandSet.insertBefore(command,commandSet.firstChild||null); 82 | const fun = details.command; 83 | command.addEventListener("command",ev => fun(ev.view,ev)) 84 | return 85 | } 86 | static ERR_KEY = 0; 87 | static NORMAL_KEY = 1; 88 | static FUN_KEY = 2; 89 | static VK_KEY = 4; 90 | 91 | static #getKeyCategory(key){ 92 | return (/^[\w-]$/).test(key) 93 | ? Hotkey.NORMAL_KEY 94 | : (/^VK_[A-Z]+/).test(key) 95 | ? Hotkey.VK_KEY 96 | : (/^F(?:1[0,1,2]|[1-9])$/).test(key) 97 | ? Hotkey.FUN_KEY 98 | : Hotkey.ERR_KEY 99 | } 100 | 101 | static define(desc){ 102 | let keyCategory = Hotkey.#getKeyCategory(desc.key); 103 | if(keyCategory === Hotkey.ERR_KEY){ 104 | throw new Error("Provided key '"+desc.key+"' is invalid") 105 | } 106 | let commandType = typeof desc.command; 107 | if(!(commandType === "string" || commandType === "function")){ 108 | throw new Error("command must be either a string or function") 109 | } 110 | if(commandType === "function" && !desc.id){ 111 | throw new Error("command id must be specified when callback is a function") 112 | } 113 | const validMods = ["accel","alt","ctrl","meta","shift"]; 114 | const mods = desc.modifiers?.toLowerCase().split(" ").filter(a => validMods.includes(a)); 115 | if(keyCategory === Hotkey.NORMAL_KEY && !(mods && mods.length > 0)){ 116 | throw new Error("Registering a hotkey with no modifiers is not supported, except for function keys F1-F12") 117 | } 118 | let keyDetails = { 119 | id: desc.id, 120 | modifiers: mods?.join(",").replace("ctrl","accel") ?? "", 121 | command: commandType === "string" 122 | ? desc.command 123 | : `cmd_${desc.id}` 124 | }; 125 | if(desc.reserved){ 126 | keyDetails.reserved = "true" 127 | } 128 | if(keyCategory === Hotkey.NORMAL_KEY){ 129 | keyDetails.key = desc.key.toUpperCase(); 130 | }else{ 131 | keyDetails.keycode = keyCategory === Hotkey.FUN_KEY ? `VK_${desc.key}` : desc.key; 132 | } 133 | return new Hotkey( 134 | keyDetails, 135 | commandType === "function" 136 | ? { id: keyDetails.command, command: desc.command } 137 | : null 138 | ) 139 | } 140 | } 141 | 142 | export class Pref{ 143 | #type; 144 | #name; 145 | #observerCallbacks; 146 | constructor(pref,type,value){ 147 | if(!(this instanceof Pref)){ 148 | return Pref.fromName(pref) 149 | } 150 | this.#name = pref; 151 | this.#type = type; 152 | } 153 | exists(){ 154 | return this.#type > 0; 155 | } 156 | get name(){ 157 | return this.#name 158 | } 159 | get value(){ 160 | try{ 161 | return Pref.getPrefOfType(this.#name,this.#type) 162 | }catch(ex){ 163 | this.#type = 0 164 | } 165 | return null 166 | } 167 | set value(some){ 168 | this.setTo(some); 169 | } 170 | defaultTo(value){ 171 | if(this.#type > 0){ 172 | return false 173 | } 174 | this.setTo(value); 175 | return true 176 | } 177 | hasUserValue(){ 178 | return this.#type > 0 && Services.prefs.prefHasUserValue(this.#name) 179 | } 180 | get type(){ 181 | if(this.#type === 32) 182 | return "string" 183 | if(this.#type === 64) 184 | return "number" 185 | if(this.#type === 128) 186 | return "boolean" 187 | return "invalid" 188 | } 189 | setTo(some){ 190 | const someType = Pref.getTypeof(some); 191 | if(someType > 0 && someType === this.#type || this.#type === 0){ 192 | return Pref.setPrefOfType(this.#name,someType,some); 193 | } 194 | throw new Error("Can't set pref to a different type") 195 | } 196 | reset(){ 197 | if(this.#type !== 0){ 198 | Services.prefs.clearUserPref(this.#name) 199 | } 200 | this.#type = Services.prefs.getPrefType(this.#name); 201 | } 202 | orFallback(some){ 203 | return this.#type > 0 204 | ? this.value 205 | : some 206 | } 207 | observe(_, topic, data) { 208 | if(topic !== "nsPref:changed"){ 209 | console.warn("Somehow pref observer got topic:",topic); 210 | return 211 | } 212 | const newType = Services.prefs.getPrefType(this.#name); 213 | const needsTypeRefresh = this.#type > 0 && this.#type != newType; 214 | if(needsTypeRefresh){ 215 | Services.prefs.removeObserver(this.#name,this); 216 | } 217 | this.#type = newType; 218 | for(let cb of this.#getObserverCallbacks()){ 219 | try{ 220 | cb(this) 221 | }catch(ex){ 222 | console.error(ex) 223 | } 224 | } 225 | if(needsTypeRefresh){ 226 | this.#observerCallbacks?.clear(); 227 | } 228 | } 229 | forget(){ 230 | Services.prefs.removeObserver(this.#name,this); 231 | this.#observerCallbacks?.clear(); 232 | } 233 | #getObserverCallbacks(){ 234 | if(!this.#observerCallbacks){ 235 | this.#observerCallbacks = new Set(); 236 | } 237 | return this.#observerCallbacks 238 | } 239 | addListener(callback){ 240 | let callbacks = this.#getObserverCallbacks(); 241 | if(callbacks.size === 0){ 242 | Services.prefs.addObserver(this.#name,this); 243 | } 244 | callbacks.add(callback); 245 | return this 246 | } 247 | removeListener(callback){ 248 | let callbacks = this.#getObserverCallbacks(); 249 | callbacks.delete(callback); 250 | if(callbacks.size === 0){ 251 | Services.prefs.removeObserver(this.#name,this) 252 | } 253 | } 254 | static fromName(some){ 255 | return new Pref(some,Services.prefs.getPrefType(some)) 256 | } 257 | static getPrefOfType(pref,type){ 258 | if(type === 32) 259 | return Services.prefs.getStringPref(pref) 260 | if(type === 64) 261 | return Services.prefs.getIntPref(pref) 262 | if(type === 128) 263 | return Services.prefs.getBoolPref(pref); 264 | return null; 265 | } 266 | static getTypeof(some){ 267 | const someType = typeof some; 268 | if(someType === "string") 269 | return 32 270 | if(someType === "number") 271 | return 64 272 | if(someType === "boolean") 273 | return 128 274 | return 0 275 | } 276 | static setPrefOfType(pref,type,value){ 277 | if(type === 32) 278 | return Services.prefs.setCharPref(pref,value); 279 | if(type === 64) 280 | return Services.prefs.setIntPref(pref,value); 281 | if(type === 128) 282 | return Services.prefs.setBoolPref(pref,value); 283 | throw new Error(`Unknown pref type: {type}`); 284 | } 285 | static setIfUnset(pref,value){ 286 | if(Services.prefs.getPrefType(pref) === 0){ 287 | Pref.setPrefOfType(pref,Pref.getTypeof(value),value); 288 | return true 289 | } 290 | return false 291 | } 292 | static get(prefPath){ 293 | return Pref.fromName(prefPath) 294 | } 295 | static set(prefName, value){ 296 | Pref.fromName(prefName).setTo(value) 297 | } 298 | static addListener(a,b){ 299 | let o = (q,w,e) => b(Pref.fromName(e),e); 300 | Services.prefs.addObserver(a,o); 301 | return {pref:a, observer:o} 302 | } 303 | static removeListener(a){ 304 | Services.prefs.removeObserver(a.pref,a.observer) 305 | } 306 | } 307 | 308 | function reRegisterStyleWithQualifiedURI(aURI,aType){ 309 | let sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService); 310 | try{ 311 | switch(aType){ 312 | case "agent": 313 | sss.unregisterSheet(aURI,sss.AGENT_SHEET); 314 | sss.loadAndRegisterSheet(aURI,sss.AGENT_SHEET); 315 | return true 316 | case "author": 317 | sss.unregisterSheet(aURI,sss.AUTHOR_SHEET); 318 | sss.loadAndRegisterSheet(aURI,sss.AUTHOR_SHEET); 319 | return true 320 | default: 321 | return false 322 | } 323 | }catch(e){ 324 | console.error(e); 325 | return false 326 | } 327 | } 328 | 329 | function reloadRegisteredStyleSheet(name) { 330 | let registeredStyles = loaderModuleLink.styles; 331 | if(!registeredStyles){ 332 | throw new Error("updateStyleSheet was called in a context without loader module access"); 333 | } 334 | let matchingStyle = registeredStyles.find( s => s.filename === name); 335 | if(!matchingStyle){ 336 | console.warn(`No registered style exists with name: ${name}`); 337 | return false 338 | } 339 | if(matchingStyle.styleSheetMode === "agent"){ 340 | return reRegisterStyleWithQualifiedURI(matchingStyle.referenceURI,"agent") 341 | }else{ 342 | let success = loaderModuleLink.scriptDataConstructor.preLoadAuthorStyle(matchingStyle); 343 | if(success){ 344 | const styleSheetType = 2; // styleSheetService.AUTHOR_SHEET 345 | let windows = Services.wm.getEnumerator(null); 346 | while (windows.hasMoreElements()) { 347 | let win = windows.getNext(); 348 | if(matchingStyle.regex.test(win.location.href)){ 349 | win.windowUtils.removeSheet(matchingStyle.referenceURI, styleSheetType); 350 | win.windowUtils.addSheet(matchingStyle.preLoadedStyle,styleSheetType); 351 | } 352 | } 353 | } 354 | return success 355 | } 356 | } 357 | function reloadStyleSheet(name, type) { 358 | if(type){ 359 | let sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService); 360 | try{ 361 | let uri = Services.io.newURI(`chrome://userchrome/content/${name}`); 362 | switch(type){ 363 | case "agent": 364 | sss.unregisterSheet(uri,sss.AGENT_SHEET); 365 | sss.loadAndRegisterSheet(uri,sss.AGENT_SHEET); 366 | return true 367 | case "author": 368 | sss.unregisterSheet(uri,sss.AUTHOR_SHEET); 369 | sss.loadAndRegisterSheet(uri,sss.AUTHOR_SHEET); 370 | return true 371 | default: 372 | return false 373 | } 374 | }catch(e){ 375 | console.error(e); 376 | return false 377 | } 378 | } 379 | let fsResult = FileSystem.getEntry(name); 380 | if(!fsResult.isFile()){ 381 | return false 382 | } 383 | let recentWindow = Services.wm.getMostRecentBrowserWindow(); 384 | if(!recentWindow){ 385 | return false 386 | } 387 | function recurseImports(sheet,all){ 388 | let z = 0; 389 | let rule = sheet.cssRules[0]; 390 | // loop through import rules and check that the "all" 391 | // doesn't already contain the same object 392 | while(rule instanceof CSSImportRule && !all.includes(rule.styleSheet) ){ 393 | all.push(rule.styleSheet); 394 | recurseImports(rule.styleSheet,all); 395 | rule = sheet.cssRules[++z]; 396 | } 397 | return all 398 | } 399 | 400 | let sheets = recentWindow.InspectorUtils.getAllStyleSheets(recentWindow.document,false).flatMap( x => recurseImports(x,[x]) ); 401 | 402 | // If a sheet is imported multiple times, then there will be 403 | // duplicates, because style system does create an object for 404 | // each instace but that's OK since sheets.find below will 405 | // only find the first instance and reload that which is 406 | // "probably" fine. 407 | 408 | let target = sheets.find(sheet => sheet.href === fsResult.fileURI); 409 | if(target){ 410 | recentWindow.InspectorUtils.parseStyleSheet(target,fsResult.readSync()); 411 | return true 412 | } 413 | return false 414 | } 415 | 416 | // This stores data we need to link from the loader module 417 | export const loaderModuleLink = new (function(){ 418 | let sessionRestored = false; 419 | let variant = null; 420 | let brandName = null; 421 | // .setup() is called once by boot.sys.mjs on startup 422 | this.setup = (ref,aVersion,aBrandName,aVariant,aScriptData) => { 423 | this.scripts = ref.scripts; 424 | this.styles = ref.styles; 425 | this.version = aVersion; 426 | this.getScriptMenu = (aDoc) => { 427 | return ref.generateScriptMenuItemsIfNeeded(aDoc); 428 | } 429 | brandName = aBrandName; 430 | variant = aVariant; 431 | this.scriptDataConstructor = aScriptData; 432 | delete this.setup; 433 | Object.freeze(this); 434 | return 435 | } 436 | Object.defineProperty(this,"variant",{ get: () => { 437 | if(variant === null){ 438 | let is_tb = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs").AppConstants.BROWSER_CHROME_URL.startsWith("chrome://messenger"); 439 | variant = { 440 | THUNDERBIRD: is_tb, 441 | FIREFOX: !is_tb 442 | } 443 | } 444 | return variant 445 | }}); 446 | Object.defineProperty(this,"brandName",{ get: () => { 447 | if(brandName === null){ 448 | brandName = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs").AppConstants.MOZ_APP_DISPLAYNAME_DO_NOT_USE 449 | } 450 | return brandName 451 | }}); 452 | this.setSessionRestored = () => { sessionRestored = true }; 453 | this.sessionRestored = () => sessionRestored; 454 | return this 455 | })(); 456 | 457 | // getScriptData() returns these types of objects 458 | export class ScriptInfo{ 459 | constructor(enabled){ 460 | this.isEnabled = enabled 461 | } 462 | asFile(){ 463 | return FileSystem.getEntry(FileSystem.convertChromeURIToFileURI(this.chromeURI)).entry() 464 | } 465 | static fromScript(aScript, isEnabled){ 466 | let info = new ScriptInfo(isEnabled); 467 | Object.assign(info,aScript); 468 | info.regex = aScript.regex ? new RegExp(aScript.regex.source, aScript.regex.flags) : null; 469 | info.chromeURI = aScript.chromeURI.spec; 470 | info.referenceURI = aScript.referenceURI.spec; 471 | info.isRunning = aScript.isRunning; 472 | info.injectionFailed = aScript.injectionFailed; 473 | return info 474 | } 475 | static fromString(aName, aStringAsFSResult, isStyle) { 476 | const ScriptData = loaderModuleLink.scriptDataConstructor; 477 | const headerText = ScriptData.extractScriptHeader(aStringAsFSResult); 478 | const scriptData = new ScriptData(aName, headerText, headerText.length > aStringAsFSResult.size - 2, isStyle); 479 | return ScriptInfo.fromScript(scriptData, false) 480 | } 481 | } 482 | 483 | export class windowUtils{ 484 | constructor(){ 485 | if(new.target){ 486 | throw new TypeError("windowUtils is not a constructor") 487 | } 488 | } 489 | static onCreated(fun){ 490 | if(!lazy.windowOpenedCallbacks){ 491 | Services.obs.addObserver(windowUtils.#observe, 'domwindowopened', false); 492 | lazy.windowOpenedCallbacks = new Set(); 493 | } 494 | lazy.windowOpenedCallbacks.add(fun) 495 | } 496 | static #observe(aSubject) { 497 | aSubject.addEventListener( 498 | 'DOMContentLoaded', 499 | windowUtils.#onDOMContent, 500 | {once:true}); 501 | } 502 | static getCreatedCallbacks(){ 503 | return lazy.windowOpenedCallbacks 504 | } 505 | static #onDOMContent(ev){ 506 | const window = ev.originalTarget.defaultView; 507 | for(let f of lazy.windowOpenedCallbacks){ 508 | try{ 509 | f(window) 510 | }catch(e){ 511 | console.error(e) 512 | } 513 | } 514 | } 515 | static getLastFocused(windowType){ 516 | return Services.wm.getMostRecentWindow(windowType === undefined ? windowUtils.mainWindowType : windowType) 517 | } 518 | static getAll(onlyBrowsers = true){ 519 | let windows = Services.wm.getEnumerator(onlyBrowsers ? windowUtils.mainWindowType : null); 520 | let wins = []; 521 | while (windows.hasMoreElements()) { 522 | wins.push(windows.getNext()); 523 | } 524 | return wins 525 | } 526 | static forEach(fun, onlyBrowsers = true){ 527 | let wins = windowUtils.getAll(onlyBrowsers); 528 | wins.forEach((w) => fun(w.document,w)) 529 | } 530 | static isBrowserWindow(window){ 531 | return window.document.documentElement.getAttribute("windowtype") === windowUtils.mainWindowType 532 | } 533 | static mainWindowType = loaderModuleLink.variant.FIREFOX ? "navigator:browser" : "mail:3pane"; 534 | 535 | static waitWindowLoading(win){ 536 | if(win && win.isChromeWindow){ 537 | if(loaderModuleLink.variant.FIREFOX){ 538 | if(win.gBrowserInit.delayedStartupFinished){ 539 | return Promise.resolve(win); 540 | } 541 | }else{ // APP_VARIANT = THUNDERBIRD 542 | if(win.gMailInit.delayedStartupFinished){ 543 | return Promise.resolve(win); 544 | } 545 | } 546 | return new Promise(resolve => { 547 | let observer = (subject) => { 548 | if(subject === win){ 549 | Services.obs.removeObserver(observer, "browser-delayed-startup-finished"); 550 | resolve(win) 551 | } 552 | }; 553 | Services.obs.addObserver(observer, "browser-delayed-startup-finished"); 554 | }); 555 | } 556 | return Promise.reject(new Error("reference is not a window")) 557 | } 558 | } 559 | 560 | export function createElement(doc,tag,props,isHTML = false){ 561 | let el = isHTML ? doc.createElement(tag) : doc.createXULElement(tag); 562 | for(let prop in props){ 563 | el.setAttribute(prop,props[prop]) 564 | } 565 | return el 566 | } 567 | 568 | export function createWidget(desc){ 569 | if(!desc || !desc.id ){ 570 | throw new Error("custom widget description is missing 'id' property"); 571 | } 572 | if(!(desc.type === "toolbarbutton" || desc.type === "toolbaritem")){ 573 | throw new Error(`custom widget has unsupported type: '${desc.type}'`); 574 | } 575 | const CUI = lazy.CustomizableUI; 576 | 577 | if(CUI.getWidget(desc.id)?.hasOwnProperty("source")){ 578 | // very likely means that the widget with this id already exists 579 | // There isn't a very reliable way to 'really' check if it exists or not 580 | throw new Error(`Widget with ID: '${desc.id}' already exists`); 581 | } 582 | // This is pretty ugly but makes onBuild much cleaner. 583 | let itemStyle = ""; 584 | if(desc.image){ 585 | if(desc.type==="toolbarbutton"){ 586 | itemStyle += "list-style-image:"; 587 | }else{ 588 | itemStyle += "background: transparent center no-repeat "; 589 | } 590 | itemStyle += /^chrome:\/\/|resource:\/\//.test(desc.image) 591 | ? `url(${desc.image});` 592 | : `url(chrome://userChrome/content/${desc.image});`; 593 | itemStyle += desc.style || ""; 594 | } 595 | const callback = desc.callback; 596 | if(typeof callback === "function"){ 597 | SharedGlobal.widgetCallbacks.set(desc.id,callback); 598 | } 599 | return CUI.createWidget({ 600 | id: desc.id, 601 | type: 'custom', 602 | defaultArea: desc.area || CUI.AREA_NAVBAR, 603 | onBuild: function(aDocument) { 604 | let toolbaritem = aDocument.createXULElement(desc.type); 605 | let props = { 606 | id: desc.id, 607 | class: `toolbarbutton-1 chromeclass-toolbar-additional ${desc.class?desc.class:""}`, 608 | overflows: !!desc.overflows, 609 | label: desc.label || desc.id, 610 | tooltiptext: desc.tooltip || desc.id, 611 | style: itemStyle 612 | }; 613 | for (let p in props){ 614 | toolbaritem.setAttribute(p, props[p]); 615 | } 616 | 617 | if(typeof callback === "function"){ 618 | const allEvents = !!desc.allEvents; 619 | toolbaritem.addEventListener("click",(ev) => { 620 | allEvents || ev.button === 0 && SharedGlobal.widgetCallbacks.get(ev.target.id)(ev,ev.target.ownerGlobal) 621 | }) 622 | } 623 | for (let attr in desc){ 624 | if(attr != "callback" && !(attr in props)){ 625 | toolbaritem.setAttribute(attr,desc[attr]) 626 | } 627 | } 628 | return toolbaritem; 629 | } 630 | }); 631 | } 632 | 633 | export function escapeXUL(markup) { 634 | return markup.replace(/[<>&'"]/g, (char) => { 635 | switch (char) { 636 | case `<`: 637 | return "<"; 638 | case `>`: 639 | return ">"; 640 | case `&`: 641 | return "&"; 642 | case `'`: 643 | return "'"; 644 | case '"': 645 | return """; 646 | } 647 | }); 648 | } 649 | 650 | function getScriptInfoForType(aFilter,aScriptList){ 651 | const filterType = typeof aFilter; 652 | if(aFilter && !(filterType === "string" || filterType === "function")){ 653 | throw "getScriptData() called with invalid filter type: "+filterType 654 | } 655 | if(filterType === "string"){ 656 | let script = aScriptList.find(s => s.filename === aFilter); 657 | return script ? ScriptInfo.fromScript(script,script.isEnabled) : null; 658 | } 659 | const disabledScripts = Services.prefs.getStringPref('userChromeJS.scriptsDisabled',"").split(","); 660 | if(filterType === "function"){ 661 | return aScriptList.filter(aFilter).map( 662 | script => ScriptInfo.fromScript(script,!disabledScripts.includes(script.filename)) 663 | ); 664 | } 665 | return aScriptList.map( 666 | script => ScriptInfo.fromScript(script,!disabledScripts.includes(script.filename)) 667 | ); 668 | } 669 | 670 | export function getScriptData(aFilter){ 671 | return getScriptInfoForType(aFilter, loaderModuleLink.scripts) 672 | } 673 | export function getStyleData(aFilter){ 674 | return getScriptInfoForType(aFilter, loaderModuleLink.styles) 675 | } 676 | 677 | export function loadURI(win,desc){ 678 | if(loaderModuleLink.variant.THUNDERBIRD){ 679 | console.warn("loadURI() is not supported on Thunderbird"); 680 | return false 681 | } 682 | if( !win 683 | || !desc 684 | || !desc.url 685 | || typeof desc.url !== "string" 686 | || !(["tab","tabshifted","window","current"]).includes(desc.where) 687 | ){ 688 | return false 689 | } 690 | const isJsURI = desc.url.slice(0,11) === "javascript:"; 691 | try{ 692 | win.openTrustedLinkIn( 693 | desc.url, 694 | desc.where, 695 | { "allowPopups":isJsURI, 696 | "inBackground":false, 697 | "allowInheritPrincipal":false, 698 | "private":!!desc.private, 699 | "userContextId":desc.url.startsWith("http")?desc.userContextId:null}); 700 | }catch(e){ 701 | console.error(e); 702 | return false 703 | } 704 | return true 705 | } 706 | 707 | export function parseStringAsScriptInfo(aName, aString, isStyle = false){ 708 | return ScriptInfo.fromString(aName, FileSystem.StringContent({content: aString}), isStyle) 709 | } 710 | 711 | export function restartApplication(clearCache){ 712 | clearCache && Services.appinfo.invalidateCachesOnRestart(); 713 | let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); 714 | Services.obs.notifyObservers( 715 | cancelQuit, 716 | "quit-application-requested", 717 | "restart" 718 | ); 719 | if (!cancelQuit.data) { 720 | Services.startup.quit( 721 | Services.startup.eAttemptQuit | Services.startup.eRestart 722 | ); 723 | return true 724 | } 725 | return false 726 | } 727 | 728 | export async function showNotification(description){ 729 | if(loaderModuleLink.variant.THUNDERBIRD){ 730 | console.warn('showNotification() is not supported on Thunderbird\nNotification label was: "'+description.label+'"'); 731 | return 732 | } 733 | await startupFinished(); 734 | let window = description.window; 735 | if(!(window && window.isChromeWindow)){ 736 | window = Services.wm.getMostRecentBrowserWindow(); 737 | } 738 | let aNotificationBox = window.gNotificationBox; 739 | if(description.tab){ 740 | let aBrowser = description.tab.linkedBrowser; 741 | if(!aBrowser){ return } 742 | aNotificationBox = window.gBrowser.getNotificationBox(aBrowser); 743 | } 744 | if(!aNotificationBox){ return } 745 | let type = description.type || "default"; 746 | let priority = aNotificationBox.PRIORITY_INFO_HIGH; 747 | switch (description.priority){ 748 | case "system": 749 | priority = aNotificationBox.PRIORITY_SYSTEM; 750 | break; 751 | case "critical": 752 | priority = aNotificationBox.PRIORITY_CRITICAL_HIGH; 753 | break; 754 | case "warning": 755 | priority = aNotificationBox.PRIORITY_WARNING_HIGH; 756 | break; 757 | } 758 | aNotificationBox.appendNotification( 759 | type, 760 | { 761 | label: description.label || "fx-autoconfig message", 762 | image: "chrome://browser/skin/notification-icons/popup.svg", 763 | priority: priority, 764 | eventCallback: typeof description.callback === "function" ? description.callback : null 765 | }, 766 | description.buttons 767 | ); 768 | } 769 | 770 | export function startupFinished(){ 771 | if(loaderModuleLink.sessionRestored() || lazy.startupPromises === null){ 772 | return Promise.resolve(); 773 | } 774 | if(lazy.startupPromises.size === 0){ 775 | const obs_topic = loaderModuleLink.variant.FIREFOX 776 | ? "sessionstore-windows-restored" 777 | : "browser-delayed-startup-finished"; 778 | const startupObserver = () => { 779 | Services.obs.removeObserver(startupObserver, obs_topic); 780 | loaderModuleLink.setSessionRestored(); 781 | for(let f of lazy.startupPromises){ f() } 782 | lazy.startupPromises.clear(); 783 | lazy.startupPromises = null; 784 | } 785 | Services.obs.addObserver(startupObserver, obs_topic); 786 | } 787 | return new Promise(resolve => lazy.startupPromises.add(resolve)) 788 | } 789 | 790 | export function toggleScript(aFilename){ 791 | if(typeof aFilename != "string"){ 792 | throw new Error("expected name of the script as string") 793 | } 794 | let script = aFilename.endsWith("js") 795 | ? getScriptData(aFilename) 796 | : getStyleData(aFilename); 797 | if(!script){ 798 | return null 799 | } 800 | const PREF_SCRIPTSDISABLED = 'userChromeJS.scriptsDisabled'; 801 | const prefValue = Services.prefs.getStringPref(PREF_SCRIPTSDISABLED,""); 802 | const isEnabled = prefValue.indexOf(script.filename) === -1; 803 | if (isEnabled) { 804 | Services.prefs.setCharPref(PREF_SCRIPTSDISABLED, `${script.filename},${prefValue}`); 805 | } else { 806 | Services.prefs.setCharPref(PREF_SCRIPTSDISABLED, prefValue.replace(new RegExp(`^${script.filename},?|,${script.filename}`), '')); 807 | } 808 | Services.appinfo.invalidateCachesOnRestart(); 809 | script.isEnabled = !isEnabled; 810 | return script 811 | } 812 | 813 | export function updateStyleSheet(name = "../userChrome.css",type){ 814 | if(name.endsWith(".uc.css")){ 815 | return reloadRegisteredStyleSheet(name) 816 | } 817 | return reloadStyleSheet(name,type) 818 | } 819 | -------------------------------------------------------------------------------- /program/config.js: -------------------------------------------------------------------------------- 1 | // skip 1st line 2 | try { 3 | 4 | let cmanifest = Cc['@mozilla.org/file/directory_service;1'].getService(Ci.nsIProperties).get('UChrm', Ci.nsIFile); 5 | cmanifest.append('utils'); 6 | cmanifest.append('chrome.manifest'); 7 | 8 | if(cmanifest.exists()){ 9 | Components.manager.QueryInterface(Ci.nsIComponentRegistrar).autoRegister(cmanifest); 10 | ChromeUtils.importESModule('chrome://userchromejs/content/boot.sys.mjs'); 11 | } 12 | 13 | } catch(ex) {}; -------------------------------------------------------------------------------- /program/defaults/pref/config-prefs.js: -------------------------------------------------------------------------------- 1 | pref("general.config.obscure_value", 0); 2 | pref("general.config.filename", "config.js"); 3 | // Sandbox needs to be disabled in release and Beta versions 4 | pref("general.config.sandbox_enabled", false); -------------------------------------------------------------------------------- /test_profile/chrome/resources/ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrOtherGuy/fx-autoconfig/f1f61958491c18e690bed8e04e89dd3a8e4a6c4d/test_profile/chrome/resources/ico.png -------------------------------------------------------------------------------- /test_profile/chrome/resources/test_file.txt: -------------------------------------------------------------------------------- 1 | This is a test file used in testing -------------------------------------------------------------------------------- /test_profile/chrome/resources/test_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "property": "This is a test file used in testing" 3 | } -------------------------------------------------------------------------------- /test_profile/chrome/resources/write_test_basic.txt: -------------------------------------------------------------------------------- 1 | test file content -------------------------------------------------------------------------------- /test_profile/chrome/tests/.sys.mjs: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/.uc.js: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/000_test_runner.sys.mjs: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name test_runner 3 | // @description module which runs and logs test results 4 | // ==/UserScript== 5 | 6 | import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; 7 | 8 | class Result{ 9 | constructor(test){ 10 | this.expected = test.expected; 11 | this.value = test.value; 12 | this.name = test.name; 13 | } 14 | static From(test){ 15 | if(typeof test.expected === "function"){ 16 | return test.expected(test.value) === true 17 | ? new Success(test) 18 | : new Failure(test) 19 | } 20 | if( test.value === test.expected ){ 21 | return new Success( test ) 22 | } 23 | return new Failure( test ) 24 | } 25 | log(){ 26 | console.info(`%c${this.name}: test was skipped`,"color: dodgerblue") 27 | } 28 | } 29 | 30 | class Failure extends Result{ 31 | constructor(test){ 32 | super(test); 33 | } 34 | log(){ 35 | let expected = (typeof this.expected === "function") ? "" : this.expected; 36 | console.warn(`${this.name} failed: expected:\n${expected}\ngot:\n${this.value}`); 37 | } 38 | } 39 | 40 | class Success extends Result{ 41 | constructor(test){ 42 | super(test); 43 | } 44 | log(){ 45 | console.info(`%c${this.name}: OK`,"color: lightgreen"); 46 | } 47 | } 48 | 49 | class TestWaitable{ 50 | #result; 51 | constructor(aTest){ 52 | this.name = aTest.name; 53 | this.state = aTest.disabled ? Test.SKIPPED : Test.WAITING; 54 | } 55 | get result(){ 56 | return this.#result 57 | } 58 | hasResult(){ 59 | return this.state != Test.WAITING 60 | } 61 | setResult(res){ 62 | this.#result = res; 63 | if(this.state != Test.SKIPPED){ 64 | this.state = res instanceof Success ? Test.SUCCESS : Test.FAILURE; 65 | } 66 | } 67 | log(){ 68 | if(this.hasResult()){ 69 | this.result.log() 70 | }else{ 71 | console.warn(`${this.name} failed to settle before test timeout!`) 72 | } 73 | } 74 | } 75 | 76 | const RESULTS = []; 77 | 78 | class Test{ 79 | #waitable; 80 | constructor(name,fun){ 81 | this.name = name; 82 | this.fun = fun; 83 | } 84 | get waitable(){ 85 | return this.#waitable; 86 | } 87 | exec(){ 88 | return this.fun(); 89 | } 90 | disable(){ 91 | this.disabled = true; 92 | return this 93 | } 94 | expectAsync(expect){ 95 | this.expected = expect; 96 | this.#waitable = new TestWaitable(this); 97 | RESULTS.push(this.#waitable); 98 | return Test.runnerAsync(this) 99 | } 100 | expect(expect){ 101 | this.expected = expect; 102 | this.#waitable = new TestWaitable(this); 103 | RESULTS.push(this.#waitable); 104 | return Test.runner(this) 105 | } 106 | async expectError(){ 107 | this.expected = ""; 108 | this.#waitable = new TestWaitable(this); 109 | RESULTS.push(this.#waitable); 110 | if(this.disabled){ 111 | this.#waitable.setResult(new Result(this)); 112 | return this 113 | } 114 | try{ 115 | await this.exec(); 116 | this.value = "Success"; 117 | this.#waitable.setResult(new Failure(this)); 118 | }catch(ex){ 119 | this.value = ex; 120 | this.#waitable.setResult(new Success(this)); 121 | } 122 | return this 123 | } 124 | static FAILURE = Symbol("failure"); 125 | static SKIPPED = Symbol("skipped"); 126 | static SUCCESS = Symbol("success"); 127 | static WAITING = Symbol("waiting"); 128 | 129 | static runner(test){ 130 | if(test.disabled){ 131 | test.#waitable.setResult(new Result(test)); 132 | return test 133 | } 134 | try{ 135 | test.value = test.exec(); 136 | test.#waitable.setResult( Result.From(test) ) 137 | }catch(e){ 138 | let fail = new Failure(test); 139 | fail.value = e; 140 | test.#waitable.setResult(fail); 141 | console.error(e); 142 | } 143 | return test 144 | } 145 | static async runnerAsync(test){ 146 | if(test.disabled){ 147 | test.#waitable.setResult(new Result(test)); 148 | return test 149 | } 150 | try{ 151 | test.value = await test.exec(); 152 | test.#waitable.setResult( Result.From(test) ) 153 | }catch(e){ 154 | let fail = new Failure(test); 155 | fail.value = e; 156 | test.#waitable.setResult(fail); 157 | fail.log(); 158 | } 159 | return test 160 | } 161 | static resolveOnTimeout(millis){ 162 | return new Promise(res => { 163 | setTimeout(res,millis) 164 | }) 165 | } 166 | static rejectOnTimeout(millis){ 167 | return new Promise((_,reject)=>{ 168 | setTimeout(reject,millis) 169 | }) 170 | } 171 | 172 | static #state = { 173 | isRunning: false 174 | } 175 | 176 | static async waitForTestSet(aTestSet){ 177 | const TIMEOUT = 8000; 178 | if(this.#state.isRunning){ 179 | throw "a test set is already runnning" 180 | } 181 | this.#state.isRunning = true; 182 | try{ 183 | let resolution = await Promise.race([Test.rejectOnTimeout(TIMEOUT),Promise.allSettled(aTestSet)]); 184 | }catch(ex){ } 185 | Test.logResults(); 186 | this.#state.isRunning = false; 187 | } 188 | 189 | static logResults(){ 190 | const passed = RESULTS.reduce((a,b) => a + (b.state === Test.SUCCESS ? 1 : 0),0); 191 | const failed = RESULTS.reduce((a,b) => a + (b.state === Test.FAILURE ? 1 : 0),0); 192 | const timed_out = RESULTS.reduce((a,b) => a + (b.hasResult() ? 0 : 1),0); 193 | const skipped = RESULTS.length - (passed + failed + timed_out); 194 | const total = RESULTS.length; 195 | while(RESULTS.length > 0){ 196 | RESULTS.shift().log() 197 | } 198 | console.info( 199 | `%cPassed: ${passed}/${total}\nFailed: ${failed}/${total}\nTimeout: ${timed_out}/${total}\nSkipped: ${skipped}/${total}`, 200 | "color: rgb(120,160,240)"); 201 | } 202 | } 203 | 204 | export { Test } -------------------------------------------------------------------------------- /test_profile/chrome/tests/aaa_test_script.uc.js: -------------------------------------------------------------------------------- 1 | (()=>{42})(); -------------------------------------------------------------------------------- /test_profile/chrome/tests/legacy_tests.uc.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name test_utils_legacy 3 | // @onlyonce 4 | // @long-description 5 | // @description fallback for loaders without multi-line parsing 6 | /* 7 | This file is used to run various tests where main purpose 8 | is to test APIs provided by _ucUtils. 9 | Above line is left empty on purpose to test multi-line descriptions. 10 | 11 | Above line is also left empty 12 | */ 13 | // @loadOrder 5 14 | // ==/UserScript== 15 | 16 | "use strict"; 17 | 18 | (function(){ 19 | 20 | const BRAND_NAME = "Firefox Nightly"; 21 | const PREF_ALLLOW_UNSAFE = "userChromeJS.allowUnsafeWrites"; 22 | 23 | const { Test } = ChromeUtils.importESModule("chrome://userscripts/content/000_test_runner.sys.mjs"); 24 | 25 | 26 | const PREF_LISTENER = new (function(){ 27 | let clients = new Set(); 28 | let listener = _ucUtils.prefs.addListener("userChromeJS",(value, prefName) => { 29 | for(let client of clients){ 30 | client(value,prefName); 31 | } 32 | clients.clear() 33 | }); 34 | this.listenOnce = (fun) => { 35 | clients.add(fun); 36 | } 37 | this.forgetAboutIt = () => { 38 | _ucUtils.prefs.removeListener(listener); 39 | listener = null; 40 | } 41 | this.size = () => clients.size; 42 | this.reset = () => { 43 | listener && this.forgetAboutIt(); 44 | clients.clear(); 45 | } 46 | })(); 47 | 48 | // Needs to be alphabetical 49 | // This should only include files that are actually runnable tests 50 | // so no empty files or files with invalid name 51 | const TEST_FILES = [ 52 | "000_test_runner.sys.mjs", 53 | "aaa_test_script.uc.js", 54 | "test_module_script.sys.mjs", 55 | "test_module_script.uc.js", 56 | "test_registering_manifest", 57 | "utils_tests.uc.js", 58 | "write_to_shared.uc.js" 59 | ]; 60 | console.info("%crunning tests...","color: rgb(120,160,240)"); 61 | 62 | const PROMISES = [ 63 | // Synchronously read file content with string argument treated as relative path 64 | new Test( 65 | "readFileFromString", 66 | () => { return _ucUtils.readFile("test_file.txt") } 67 | ).expect("This is a test file used in testing"), 68 | 69 | // Synchronously read file content with reference to File object 70 | new Test("readFileFromFile", 71 | () => { 72 | let file = _ucUtils.getFSEntry("test_file.txt"); 73 | return _ucUtils.readFile(file); 74 | } 75 | ).expect("This is a test file used in testing"), 76 | 77 | // Async file read with string argument as relative path 78 | new Test( 79 | "readFileAsync", 80 | () => { return _ucUtils.readFileAsync("test_file.txt") } 81 | ).expectAsync("This is a test file used in testing"), 82 | 83 | // Async file read as json 84 | new Test( 85 | "readJSON", 86 | () => { 87 | return new Promise((resolve, reject) => { 88 | _ucUtils.readJSON("test_json.json") 89 | .then(some => resolve(some.property)) 90 | .catch(reject) 91 | }) 92 | } 93 | ).expectAsync("This is a test file used in testing"), 94 | 95 | // Write some content to text file 96 | new Test( 97 | "writeFileBasic", 98 | () => { 99 | return new Promise((resolve, reject) => { 100 | let bytes = null; 101 | _ucUtils.writeFile("write_test_basic.txt","test file content") 102 | .then(some => { bytes = some }) 103 | .then(() => _ucUtils.readFileAsync("write_test_basic.txt")) 104 | .then((text) => resolve(text + ": " + bytes) ) 105 | .catch(reject) 106 | }) 107 | } 108 | ).expectAsync("test file content: 17"), 109 | 110 | // List names of files in a directory 111 | new Test("listFileNames", 112 | () => { 113 | let files = _ucUtils.getFSEntry("../"); 114 | let names = []; 115 | while(files.hasMoreElements()){ 116 | let file = files.getNext().QueryInterface(Ci.nsIFile); 117 | if(file.isFile()){ 118 | names.push(file.leafName); 119 | } 120 | } 121 | return names.join(","); 122 | } 123 | ).expect("userChrome.css"), 124 | 125 | // TODO createFileURI 126 | 127 | // List folder names inside "chrome" directory 128 | new Test( 129 | "getChromeDir", 130 | () => { 131 | let items = _ucUtils.chromeDir.files; 132 | let names = []; 133 | while(items.hasMoreElements()){ 134 | let file = items.getNext().QueryInterface(Ci.nsIFile); 135 | if(file.isDirectory()){ 136 | names.push(file.leafName); 137 | } 138 | } 139 | return names.join(","); 140 | } 141 | ).expect("resources,tests,utils"), 142 | 143 | // Get File object from file name 144 | new Test( 145 | "getFSEntry", 146 | () => { return _ucUtils.getFSEntry("test_file.txt") != null } 147 | ).expect(true), 148 | 149 | // TODO togglescript 150 | 151 | // Set the pref to false (if it wasn't already) for the following tests 152 | Promise.resolve(_ucUtils.prefs.set(PREF_ALLLOW_UNSAFE,false)), 153 | 154 | // Writing outside of resources directory should fail because pref is disabled 155 | new Test( 156 | "excpectError_writeUserChromeCSS_BeforeStartup", 157 | () => { 158 | return _ucUtils.writeFile("../userChrome.css","#nav-bar{ background: #f00 !important; }") 159 | } 160 | ).expectError(), 161 | 162 | // This test should resolve after updateStyleSheet test has set the allow-unsafe pref to true 163 | new Test( 164 | "prefChangedToTrue", 165 | () => { 166 | return new Promise((resolve, reject) => { 167 | PREF_LISTENER.listenOnce((val,pref) => resolve(`${pref},${val}`)); 168 | Test.resolveOnTimeout(2000).then(reject); 169 | }) 170 | } 171 | ).expectAsync(PREF_ALLLOW_UNSAFE+",true"), 172 | 173 | // Set pref to allow writing outside of resources directory, and then write userChrome.css 174 | new Test( 175 | "updateStyleSheet", 176 | () => { 177 | _ucUtils.prefs.set(PREF_ALLLOW_UNSAFE,true); 178 | const getNavBarStyle = () => window.getComputedStyle(document.getElementById("nav-bar")); 179 | 180 | return new Promise((resolve, reject) => { 181 | _ucUtils.windowIsReady(window) 182 | .then( () => { 183 | // The color expected here is set in one of the tests that follow 184 | let oldColor = getNavBarStyle().backgroundColor; 185 | _ucUtils.writeFile("../userChrome.css","#nav-bar{ background: #ba5 !important; }") 186 | .then(() => _ucUtils.updateStyleSheet()) 187 | .then(()=>Test.resolveOnTimeout(2000)) // necessary because the style may not be applied immediately 188 | .then( () => resolve(oldColor + " : " + getNavBarStyle().backgroundColor) ) 189 | }) 190 | .catch(reject) 191 | }) 192 | } 193 | ).expectAsync("rgb(255, 0, 0) : rgb(187, 170, 85)"), 194 | 195 | /** 196 | * ! Keep these below as the last tests ! 197 | * 198 | * Restore old userChrome.css state 199 | * The above test setup sets pref to allow writing outside of resources 200 | * so this should succeed. 201 | */ 202 | 203 | // This test should resolve after timeout because pref listener was removed 204 | new Test( 205 | "prefNotChangedToFalse", 206 | () => { 207 | return new Promise((resolve, reject) => { 208 | PREF_LISTENER.listenOnce(reject); 209 | Test.resolveOnTimeout(200).then(PREF_LISTENER.forgetAboutIt); 210 | Test.resolveOnTimeout(4200) 211 | .then(() => resolve(PREF_LISTENER.size())); 212 | }) 213 | } 214 | ).expectAsync(1), 215 | 216 | new Test( 217 | "writeUserChromeCSS", 218 | () => { 219 | return new Promise((resolve, reject) => { 220 | Test.resolveOnTimeout(4000) 221 | .then(() => { 222 | return _ucUtils.writeFile("../userChrome.css","#nav-bar{ background: #f00 !important; }") 223 | }) 224 | .then(resolve) 225 | .catch(reject) 226 | }) 227 | } 228 | ).expectAsync(40) // 40 bytes written 229 | // Set allowUnsafeWrites pref back to false 230 | .then(() => _ucUtils.prefs.set(PREF_ALLLOW_UNSAFE,false)) 231 | .then(() => { 232 | // Check that writing userChrome.css now fails again 233 | new Test( 234 | "excpectError_writeUserChromeCSS_AfterStartup", 235 | () => { 236 | return _ucUtils.writeFile("../userChrome.css","#nav-bar{ background: #f00 !important; }") 237 | } 238 | ).expectError() 239 | }) 240 | 241 | ]; 242 | 243 | Test.waitForTestSet(PROMISES) 244 | .finally(() => PREF_LISTENER.reset()); 245 | 246 | })(); -------------------------------------------------------------------------------- /test_profile/chrome/tests/modules/imported_esm.sys.mjs: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name test_imported_esm 3 | // ==/UserScript== 4 | import{ _ucUtils } from "chrome://userchromejs/content/utils.sys.mjs"; 5 | const some = { 6 | test_value: 42, 7 | loaderVersion: _ucUtils.version, 8 | setToX: function(x){ 9 | this.test_value = x 10 | } 11 | }; 12 | 13 | export { some } -------------------------------------------------------------------------------- /test_profile/chrome/tests/sys.mjs: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_1.js: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_2.mjs: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_3.uc.js.txt: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_4.sys.mjs.txt: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_5uc.js: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_6sys.mjs: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_7.uc.jss: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_8.sys.mjss: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_manifest.manifest: -------------------------------------------------------------------------------- 1 | override chrome://global/skin/icons/folder.svg ../resources/ico.png -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_manifest.uc.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name test_registering_manifest 3 | // @manifest test_manifest 4 | // ==/UserScript== -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_mjs.uc.mjs: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name test_non_background_mjs 3 | // @loadOrder 2 4 | // @description 5 | /* 6 | This test is injected into a window global scope as module script because it has a .uc.mjs extension. We test if normal module mechanisms work such as import statement and that we still have access to _ucUtils from the window global. 7 | 8 | This cannot use the same test-set as main utils_tests because module-scripts are injected asynchronously and thus the test-set from utils has already started executing. 9 | */ 10 | // @long-description 11 | // @onlyonce 12 | // ==/UserScript== 13 | import { Cheese } from "chrome://userscripts/content/modules/imported_print.mjs"; 14 | import { Test } from "chrome://userscripts/content/000_test_runner.sys.mjs"; 15 | import { _ucUtils as importedUCUtils } from "chrome://userchromejs/content/utils.sys.mjs"; 16 | 17 | const PROMISES = [ 18 | new Test("non_background_mjs_got_cheese",()=>{ 19 | return Cheese.type 20 | }).expect("emmental"), 21 | 22 | new Test("non_background_mjs_got_ucUtils_from_window",()=>{ 23 | return _ucUtils.brandName 24 | }).expect("Firefox Nightly"), 25 | 26 | new Test("non_background_mjs_fallback_brandName",()=>{ 27 | return importedUCUtils.brandName 28 | }).expect("Firefox Nightly"), 29 | 30 | new Test("non_background_mjs_fallback_windows_length",()=>{ 31 | return importedUCUtils.windows.getAll(false).length > 0 32 | }).expect(true), 33 | ]; 34 | 35 | Test.waitForTestSet(PROMISES) -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_module_script.sys.mjs: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name test_module_script_ESM 3 | // ==/UserScript== 4 | 5 | import { some } from "chrome://userscripts/content/modules/imported_esm.sys.mjs"; 6 | import { Test } from "chrome://userscripts/content/000_test_runner.sys.mjs"; 7 | import { SharedGlobal } from "chrome://userchromejs/content/utils.sys.mjs"; 8 | 9 | new Test("expectError_no_utils_ESM",()=>{ 10 | return _ucUtils.sharedGlobal.test_utils.x 11 | }).expectError(); 12 | 13 | new Test("ESM_sharedGlobal_written",()=>{ 14 | SharedGlobal.test_module_script_ESM = {y: 42}; 15 | return true 16 | }).expect(true); 17 | 18 | new Test("expectError_no_window_ESM",()=>{ 19 | return window 20 | }).expectError(); 21 | 22 | new Test("ESM_import_some_equals_42",()=>{ 23 | return some.test_value 24 | }).expect(42); 25 | 26 | new Test("ESM_import_version_is_string",()=>{ 27 | return (typeof some.loaderVersion) 28 | }).expect("string"); 29 | 30 | new Test("ESM_import_set_value",()=>{ 31 | some.setToX(123); 32 | return new Promise(res => { 33 | const { some } = ChromeUtils.importESModule("chrome://userscripts/content/modules/imported_esm.sys.mjs"); 34 | res(some.test_value) 35 | }) 36 | }).expectAsync(123); 37 | -------------------------------------------------------------------------------- /test_profile/chrome/tests/test_module_script.uc.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name test_module_script 3 | // @backgroundmodule 4 | // ==/UserScript== 5 | let EXPORTED_SYMBOLS = []; 6 | const { Test } = ChromeUtils.importESModule("chrome://userscripts/content/000_test_runner.sys.mjs"); 7 | 8 | new Test("expectError_no_utils",()=>{ 9 | return _ucUtils.sharedGlobal.test_utils.x 10 | }).expectError(); 11 | 12 | new Test("expectError_no_window",()=>{ 13 | return window 14 | }).expectError(); 15 | -------------------------------------------------------------------------------- /test_profile/chrome/tests/uc.js: -------------------------------------------------------------------------------- 1 | // This file should be ignored by manager 2 | console.warn("This isn't supposed to run!"); -------------------------------------------------------------------------------- /test_profile/chrome/tests/utils_tests.uc.mjs: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name utils_tests.uc.mjs 3 | // @onlyonce 4 | // @long-description 5 | // @description fallback for loaders without multi-line parsing 6 | /* 7 | This file is used to run various tests where main purpose 8 | is to test APIs provided by UC_API. 9 | 10 | Above line is left empty on purpose to test multi-line descriptions. 11 | 12 | Above line is also left empty 13 | */ 14 | // @loadOrder 5 15 | // ==/UserScript== 16 | 17 | "use strict"; 18 | import { 19 | FileSystem, 20 | Hotkeys, 21 | Notifications, 22 | Prefs, 23 | Scripts, 24 | SharedStorage, 25 | Runtime, 26 | Utils, 27 | Windows 28 | } from "chrome://userchromejs/content/uc_api.sys.mjs"; 29 | 30 | 31 | const BRAND_NAME = "Firefox Nightly"; 32 | const PREF_ALLLOW_UNSAFE = "userChromeJS.allowUnsafeWrites"; 33 | const RESOURCE_SPEC = FileSystem.RESOURCE_URI.spec; 34 | 35 | const { Test } = ChromeUtils.importESModule("chrome://userscripts/content/000_test_runner.sys.mjs"); 36 | 37 | 38 | const PREF_LISTENER = new (function(){ 39 | let clients = new Set(); 40 | let listener = Prefs.addListener("userChromeJS",(value, prefName) => { 41 | for(let client of clients){ 42 | client(value,prefName); 43 | } 44 | clients.clear() 45 | }); 46 | this.listenOnce = (fun) => { 47 | clients.add(fun); 48 | } 49 | this.forgetAboutIt = () => { 50 | Prefs.removeListener(listener); 51 | listener = null; 52 | } 53 | this.size = () => clients.size; 54 | this.reset = () => { 55 | listener && this.forgetAboutIt(); 56 | clients.clear(); 57 | } 58 | })(); 59 | 60 | // Needs to be alphabetical 61 | // This should only include files that are actually runnable tests 62 | // so no empty files or files with invalid name 63 | const TEST_FILES = [ 64 | "000_test_runner.sys.mjs", 65 | "aaa_test_script.uc.js", 66 | "test_mjs.uc.mjs", 67 | "test_module_script.sys.mjs", 68 | "test_module_script.uc.js", 69 | "test_registering_manifest", 70 | "legacy_tests.uc.js", 71 | "utils_tests.uc.mjs", 72 | "write_to_shared.uc.js", 73 | "x_disabled_system_module", 74 | "x_disabled_test.uc.js" 75 | ]; 76 | console.info("%crunning UC_API tests...","color: rgb(120,160,240)"); 77 | 78 | const PROMISES = [ 79 | // Can we read data from SharedStorage 80 | // The value should have been set by write_to_shared.uc.js which should have run before this one. 81 | new Test( 82 | "SharedStorage", 83 | () => { return SharedStorage.test_utils.x } 84 | ).expect(42), 85 | 86 | new Test( 87 | "SharedStorage_got_ESM_set_value", 88 | () => { return SharedStorage.test_module_script_ESM.y } 89 | ).expect(42), 90 | 91 | // Does _ucUtils give us correct brandName 92 | new Test( 93 | "brandName", 94 | () => { return Runtime.brandName } 95 | ).expect(BRAND_NAME), 96 | 97 | // calling createElement() without third argument should create a xul element 98 | new Test( 99 | "createXulElement", 100 | () => { 101 | let node = Utils.createElement( 102 | document, 103 | "vbox", 104 | { class: "test-vbox", "hidden": true } 105 | ); 106 | return node.outerHTML; 107 | } 108 | ).expect('