├── .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 |
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 |
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(''),
109 |
110 | // calling createElement() with third argument should create html element
111 | new Test(
112 | "createHTMLElement",
113 | () => {
114 | let node = Utils.createElement(
115 | document,
116 | "div",
117 | { class: "test-div", "hidden": true },
118 | true
119 | );
120 | return node.outerHTML;
121 | }
122 | ).expect(''),
123 |
124 | // Test if widget is created by inspecting if it has expected fill style
125 | new Test(
126 | "createWidget",
127 | () => {
128 | return new Promise((resolve, reject) => {
129 | const widgetID = "test-widget-too";
130 |
131 | const listener = {
132 | onWidgetAfterDOMChange: (aNode) => {
133 | if(aNode.id === widgetID){
134 | window.CustomizableUI.removeListener(listener);
135 | try{
136 | resolve(
137 | aNode.getAttribute("command") + ";"
138 | + aNode.getAttribute("image") + ";"
139 | + window.getComputedStyle(aNode.icon).fill
140 | )
141 | }catch(ex){
142 | reject(ex)
143 | }
144 | }
145 | }
146 | };
147 | window.CustomizableUI.addListener(listener);
148 |
149 | let widget = Utils.createWidget({
150 | id: widgetID,
151 | type: "toolbarbutton",
152 | label: "my-widget-label-too",
153 | tooltip: "test-tooltip",
154 | class: "test-button",
155 | image: "chrome://browser/skin/bookmark-star-on-tray.svg",
156 | style: "--toolbarbutton-icon-fill: #f0f; color: #f0f;",
157 | callback: function(ev,win){
158 | console.log(win.document.title)
159 | },
160 | command: "Browser:Screenshot"
161 | });
162 |
163 | });
164 | }
165 | ).expectAsync("Browser:Screenshot;chrome://browser/skin/bookmark-star-on-tray.svg;rgb(255, 0, 255)"),
166 |
167 | // Synchronously read file content with string argument treated as relative path
168 | new Test(
169 | "readFileFromString",
170 | () => { return FileSystem.readFileSync("test_file.txt").content() }
171 | ).expect("This is a test file used in testing"),
172 |
173 | // Synchronously read file content with reference to File object
174 | new Test("readFileFromFile",
175 | () => {
176 | let fsResult = FileSystem.getEntry("test_file.txt");
177 | return FileSystem.readFileSync(fsResult.entry()).content();
178 | }
179 | ).expect("This is a test file used in testing"),
180 |
181 | // Synchronously read file content from fsResult
182 | new Test("readFileFromFSResult",
183 | () => {
184 | let fsResult = FileSystem.getEntry("test_file.txt");
185 | return fsResult.readSync();
186 | }
187 | ).expect("This is a test file used in testing"),
188 |
189 | // Async file read with string argument as relative path
190 | new Test(
191 | "readFileAsync",
192 | () => { return FileSystem.readFile("test_file.txt") }
193 | ).expectAsync(fsResult => fsResult.content() === "This is a test file used in testing"),
194 |
195 | // Asynchronously read file content from fsResult
196 | new Test("readFileFromFSResultAsync",
197 | () => {
198 | let fsResult = FileSystem.getEntry("test_file.txt");
199 | return fsResult.read();
200 | }
201 | ).expectAsync("This is a test file used in testing"),
202 |
203 | // Async file read as json
204 | new Test(
205 | "readJSON",
206 | () => {
207 | return new Promise((resolve, reject) => {
208 | FileSystem.readJSON("test_json.json")
209 | .then(some => resolve(some.property))
210 | .catch(reject)
211 | })
212 | }
213 | ).expectAsync("This is a test file used in testing"),
214 |
215 | // Write some content to text file
216 | new Test(
217 | "writeFileBasic",
218 | () => {
219 | return new Promise((resolve, reject) => {
220 | let bytes = null;
221 | FileSystem.writeFile("write_test_basic.txt","test file content")
222 | .then(some => { bytes = some })
223 | .then(() => FileSystem.readFile("write_test_basic.txt"))
224 | .then((fsResult) => resolve(fsResult.content() + ": " + bytes) )
225 | .catch(reject)
226 | })
227 | }
228 | ).expectAsync("test file content: 17"),
229 |
230 | // List names of files in a directory
231 | new Test("listFileNames",
232 | () => {
233 | let names = [];
234 | for(let entry of FileSystem.getEntry("../")){
235 | if(entry.isFile()){
236 | names.push(entry.leafName);
237 | }
238 | }
239 | return names.join(",");
240 | }
241 | ).expect("userChrome.css"),
242 |
243 | // TODO createFileURI
244 |
245 | // List folder names inside "chrome" directory
246 | new Test(
247 | "getChromeDir",
248 | () => {
249 | let names = [];
250 | for(let entry of FileSystem.chromeDir()){
251 | if(entry.isDirectory()){
252 | names.push(entry.leafName);
253 | }
254 | }
255 | return names.join(",");
256 | }
257 | ).expect("css,resources,tests,utils"),
258 |
259 | // Get File object from file name
260 | new Test(
261 | "getFSEntry",
262 | () => { return FileSystem.getEntry("test_file.txt").isFile() }
263 | ).expect(true),
264 |
265 | new Test(
266 | "getFileURIFromFile",
267 | () => { return FileSystem.getEntry("test_file.txt").fileURI }
268 | ).expect(`${RESOURCE_SPEC}test_file.txt`),
269 |
270 | new Test(
271 | "getFileURIFromContent",
272 | () => {
273 | return new Promise(resolve => {
274 | FileSystem.readFile("test_file.txt")
275 | .then(result => resolve(result.fileURI))
276 | })
277 | }
278 | ).expectAsync(`${RESOURCE_SPEC}test_file.txt`),
279 |
280 | new Test(
281 | "getFileURIFromContentSync",
282 | () => {
283 | return FileSystem.readFileSync("test_file.txt").fileURI
284 | }
285 | ).expect(`${RESOURCE_SPEC}test_file.txt`),
286 |
287 | new Test(
288 | "createEmptyFileURI",
289 | () => {
290 | return FileSystem.createFileURI("").split("/").slice(-4).join("/");
291 | }
292 | ).expect("test_profile/chrome/resources/"),
293 |
294 | new Test(
295 | "createNonEmptyFileURI",
296 | () => {
297 | return FileSystem.createFileURI("test.txt").split("/").slice(-4).join("/");
298 | }
299 | ).expect("test_profile/chrome/resources/test.txt"),
300 |
301 | // Check that correct error kind for non-existing entry
302 | new Test(
303 | "getNonExistingFSEntry",
304 | () => { return FileSystem.getEntry("nonexistent.txt").error().kind }
305 | ).expect(FileSystem.ERROR_KIND_NOT_EXIST),
306 |
307 | // Try to get file entry with invalid argument
308 | new Test(
309 | "expectError_getFSEntryWithInvalidArgument",
310 | () => { return FileSystem.readFileSync([]) }
311 | ).expectError(),
312 |
313 | // return list of script names in directory (not file names)
314 | // Note: aaa_test_script.uc.js does not have a name so it should be first
315 | new Test(
316 | "getScriptData",
317 | () => {
318 | let scripts = Scripts.getScriptData();
319 | const names = scripts
320 | .sort((a, b) => (a.name < b.name ? -1 : 1))
321 | .map(a => a.name)
322 | .join(",")
323 | return scripts.length + ";" + names
324 | }
325 | ).expect(TEST_FILES.length+";,test_module_script,test_module_script_ESM,test_non_background_mjs,test_registering_manifest,test_runner,test_utils,test_utils_legacy,write-42,x_disabled,x_disabled_system_module"),
326 |
327 | // Tests load order.
328 | // The current script (this one) should be false.
329 | // background-modules should be true
330 | // scripts that have not been run yet should be false.
331 | // NOTE: this script has @loadOrder 5 thus none of the non-backgroundmodules
332 | // should have been run yet - except write_to_shared.uc.js which should run
333 | // before this because we check if we can read the value it sets from sharedGlobal
334 | // This test assumes that none of the test scripts have been manually disabled
335 | new Test(
336 | "getScriptLoadOrder",
337 | () => {
338 | let scripts = Scripts.getScriptData();
339 | return scripts.sort((a, b) => (a.name < b.name ? -1 : 1))
340 | .map(a => a.isRunning)
341 | .join(",");
342 | }
343 | ).expect("false,true,true,false,false,true,false,false,true,false,false"),
344 |
345 | // Test invalid getScriptData() filter 1
346 | new Test(
347 | "expectError_unsupportedScriptDataFilterNumber",
348 | () => Scripts.getScriptData(123)
349 | ).expectError(),
350 |
351 | // Test invalid getScriptData() filter 2
352 | new Test(
353 | "expectError_unsupportedScriptDataFilterObject",
354 | () => Scripts.getScriptData({})
355 | ).expectError(),
356 |
357 | // Test getting single ScriptInfo object
358 | new Test(
359 | "getSingleScriptInfo",
360 | () => Scripts.getScriptData("test_module_script.sys.mjs").name
361 | ).expect("test_module_script_ESM"),
362 |
363 | // Test getting non-existing ScriptInfo object
364 | new Test(
365 | "getNonExistingScriptInfo",
366 | () => Scripts.getScriptData("non-existing-name")
367 | ).expect(null),
368 |
369 | // Test getting ScriptInfo from string
370 | new Test(
371 | "getScriptInfoFromString",
372 | () => {
373 | const headertext = `// ==UserScript==
374 | // @name fake-text
375 | // @description hello world!
376 | // @loadOrder 5
377 | // ==/UserScript==
378 | `;
379 | return Scripts.parseStringAsScriptInfo("fake-file.txt",headertext);
380 | }
381 | ).expect( scriptInfo => {
382 | return scriptInfo.filename === "fake-file.txt"
383 | && scriptInfo.isRunning === false
384 | && scriptInfo.noExec === true
385 | && scriptInfo.regex === null
386 | && scriptInfo.loadOrder === -1
387 | }),
388 |
389 | // Test ScriptInfo where default regex should be created
390 | new Test(
391 | "getScriptInfoFromStringWithoutNoExec",
392 | () => {
393 | const headertext = `// ==UserScript==
394 | // @name fake-text-2
395 | // @description hello world 2!
396 | // @loadOrder 5
397 | // ==/UserScript==
398 | console.log("hello world!")
399 | `;
400 | return Scripts.parseStringAsScriptInfo("fake-file.txt",headertext);
401 | }
402 | ).expect( scriptInfo => {
403 | return scriptInfo.filename === "fake-file.txt"
404 | && scriptInfo.isRunning === false
405 | && scriptInfo.noExec === false
406 | && scriptInfo.regex.test("chrome://browser/content/browser.xhtml")
407 | && scriptInfo.loadOrder === 5
408 | }),
409 |
410 | // Test getting ScriptInfo from filter function
411 | new Test(
412 | "getScriptInfoWithFilter",
413 | () => Scripts.getScriptData(s => s.inbackground).length
414 | ).expect(4),
415 |
416 | // Test getting correct chromeURI via ScriptInfo
417 | new Test(
418 | "getScriptInfoChromeURI",
419 | () => Scripts.getScriptData("utils_tests.uc.mjs").chromeURI
420 | ).expect("chrome://userscripts/content/utils_tests.uc.mjs"),
421 |
422 | // Test getting correct chromeURI via ScriptInfo for styles
423 | new Test(
424 | "getStyleInfo",
425 | () => Scripts.getStyleData().length
426 | ).expect(2),
427 |
428 | // Test getting correct chromeURI via ScriptInfo for styles
429 | new Test(
430 | "getStyleInfoChromeURI",
431 | () => Scripts.getStyleData("author_style.uc.css")?.chromeURI
432 | ).expect("chrome://userstyles/skin/author_style.uc.css"),
433 |
434 | // Test if ScriptInfo can be converted to nsIFile
435 | new Test(
436 | "ScriptInfoConvertedToNsIFile",
437 | () => Scripts.getStyleData("author_style.uc.css")?.asFile().exists()
438 | ).expect(true),
439 |
440 | // test single-line script descriptions
441 | new Test(
442 | "single-line script descriptions",
443 | () => Scripts.getScriptData("000_test_runner.sys.mjs")?.description
444 | ).expect("module which runs and logs test results"),
445 |
446 | // test multi-line script descriptions
447 | new Test(
448 | "multi-line script descriptions",
449 | () => {
450 | let script = Scripts.getScriptData("utils_tests.uc.mjs");
451 | return script ? script.description.split("\n") : [];
452 | }
453 | ).expect(lines => {
454 | return lines.length === 6
455 | && lines[lines.length - 1] === "Above line is also left empty";
456 | }),
457 |
458 | // TODO togglescript
459 |
460 | // Check if script menu is available (this test runs in browser.xhtml context)
461 | new Test(
462 | "getScriptMenu",
463 | () => {
464 | let before = document.getElementById("userScriptsMenu") ? 0 : 1;
465 | let after = Scripts.getScriptMenuForDocument(document).menupopup ? 2 : 0;
466 | return before + after;
467 | }).expect(3),
468 |
469 | // Set the pref to false (if it wasn't already) for the following tests
470 | Promise.resolve(Prefs.set(PREF_ALLLOW_UNSAFE,false)),
471 |
472 | // Writing outside of resources directory should fail because pref is disabled
473 | new Test(
474 | "excpectError_writeUserChromeCSS_BeforeStartup",
475 | () => {
476 | return FileSystem.writeFile("../userChrome.css","#nav-bar{ background: #f00 !important; }")
477 | }
478 | ).expectError(),
479 |
480 | // This test should resolve after reloadStyleSheet test has set the allow-unsafe pref to true
481 | new Test(
482 | "prefChangedToTrue",
483 | () => {
484 | return new Promise((resolve, reject) => {
485 | PREF_LISTENER.listenOnce((val,pref) => resolve(`${pref},${val.value}`));
486 | Test.resolveOnTimeout(2000).then(reject);
487 | })
488 | }
489 | ).expectAsync(PREF_ALLLOW_UNSAFE+",true"),
490 |
491 | // Set pref to allow writing outside of resources directory, and then write userChrome.css
492 | new Test(
493 | "reloadStyleSheet",
494 | () => {
495 | Prefs.set(PREF_ALLLOW_UNSAFE,true);
496 | const getNavBarStyle = () => window.getComputedStyle(document.getElementById("nav-bar"));
497 |
498 | return new Promise((resolve, reject) => {
499 | Windows.waitWindowLoading(window)
500 | .then( () => {
501 | // The color expected here is set in one of the tests that follow
502 | let oldColor = getNavBarStyle().backgroundColor;
503 | FileSystem.writeFile("../userChrome.css","#nav-bar{ background: #ba5 !important; }")
504 | .then(() => Scripts.reloadStyleSheet())
505 | .then(()=>Test.resolveOnTimeout(2000)) // necessary because the style may not be applied immediately
506 | .then( () => resolve(oldColor + " : " + getNavBarStyle().backgroundColor) )
507 | })
508 | .catch(reject)
509 | })
510 | }
511 | ).expectAsync("rgb(255, 0, 0) : rgb(187, 170, 85)"),
512 |
513 | // TODO updateMenuStatus
514 |
515 | // Should resolve startupFinished when startup has been finished
516 | new Test(
517 | "startupFinished",
518 | () => {
519 | return new Promise((resolve ,reject) => {
520 | setTimeout(reject, 8000); // Startup should not take 8 seconds
521 | Runtime.startupFinished()
522 | .then(() => resolve(42))
523 | })
524 | }
525 | ).expectAsync((val) => val === 42),
526 |
527 | // Can get reference to first browser-window window-object
528 | new Test(
529 | "windows.getAll",
530 | () => {
531 | return Windows.getAll()[0].AppConstants.MOZ_APP_BASENAME;
532 | }
533 | ).expect((val) => val === "Firefox"),
534 |
535 | // Is this a browser window
536 |
537 | new Test(
538 | "windows.isBrowserWindow",
539 | () => {
540 | return Windows.isBrowserWindow(window);
541 | }
542 | ).expect(true),
543 |
544 | // Has current window object been fully restored
545 | new Test(
546 | "windows.waitWindowLoading",
547 | () => {
548 | return new Promise(async (resolve, reject) => {
549 | setTimeout(reject, 8000);
550 | let init1 = window.gBrowserInit.delayedStartupFinished;
551 | await Windows.waitWindowLoading(window);
552 | resolve( `${init1};${window.gBrowserInit.delayedStartupFinished}`)
553 | })
554 | }
555 | ).expectAsync("false;true"),
556 |
557 | new Test(
558 | "hotkeys.define",
559 | async () => {
560 | let details = {
561 | id: "myHotkey",
562 | modifiers: "ctrl shift",
563 | key: "y",
564 | command: (win,ev) => { console.log(win.document.title)}
565 | };
566 | let hk = Hotkeys.define(details);
567 | hk.attachToWindow(window);
568 | await Windows.waitWindowLoading(window);
569 | let key = document.getElementById("myHotkey");
570 | return key.getAttribute("modifiers") + "," + key.getAttribute("key")+","+hk.matchingSelector;
571 | }
572 | ).expectAsync('accel,shift,Y,key[modifiers="accel,shift"][key="Y"]'),
573 |
574 | // TODO loadURI
575 |
576 | // TODO showNotification
577 |
578 | new Test(
579 | "CancelRestart",
580 | () => {
581 |
582 | return new Promise((resolve ,reject) => {
583 | let reason = null;
584 |
585 | let cancelObserver = (subject, topic, data) => {
586 | subject.data = true;
587 | reason = data;
588 | Services.obs.removeObserver(cancelObserver, "quit-application-requested");
589 | };
590 |
591 | Services.obs.addObserver(cancelObserver, "quit-application-requested");
592 |
593 | Test.resolveOnTimeout(2000)
594 | .then(Runtime.restart)
595 | .then(
596 | some => resolve( `${reason} ${some ? "succeeded" : "canceled"}` )
597 | ).catch(reject)
598 | })
599 | }
600 | ).expectAsync("restart canceled"),
601 |
602 | /**
603 | * ! Keep these below as the last tests !
604 | *
605 | * Restore old userChrome.css state
606 | * The above test setup sets pref to allow writing outside of resources
607 | * so this should succeed.
608 | */
609 |
610 | // This test should resolve after timeout because pref listener was removed
611 | // Note: this test will fail if pref "userChromeJS.firstRunShown" isn't set on startup
612 | new Test(
613 | "prefNotChangedToFalse",
614 | () => {
615 | return new Promise((resolve, reject) => {
616 | PREF_LISTENER.listenOnce(reject);
617 | Test.resolveOnTimeout(200).then(PREF_LISTENER.forgetAboutIt);
618 | Test.resolveOnTimeout(4200)
619 | .then(() => resolve(PREF_LISTENER.size()));
620 | })
621 | }
622 | ).expectAsync(1),
623 |
624 | new Test(
625 | "writeUserChromeCSS",
626 | () => {
627 | return new Promise((resolve, reject) => {
628 | Test.resolveOnTimeout(4000)
629 | .then(() => {
630 | return FileSystem.writeFile("../userChrome.css","#nav-bar{ background: #f00 !important; }")
631 | })
632 | .then(resolve)
633 | .catch(reject)
634 | })
635 | }
636 | ).expectAsync(40) // 40 bytes written
637 | // Set allowUnsafeWrites pref back to false
638 | .then(() => Services.prefs.clearUserPref(PREF_ALLLOW_UNSAFE))
639 | .then(() => {
640 | // Check that writing userChrome.css now fails again
641 | new Test(
642 | "excpectError_writeUserChromeCSS_AfterStartup",
643 | () => {
644 | return FileSystem.writeFile("../userChrome.css","#nav-bar{ background: #f00 !important; }")
645 | }
646 | ).expectError()
647 | })
648 |
649 | ];
650 |
651 | Test.waitForTestSet(PROMISES)
652 | .finally(() => PREF_LISTENER.reset());
653 |
654 |
--------------------------------------------------------------------------------
/test_profile/chrome/tests/write_to_shared.uc.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name write-42
3 | // @description write 42 to shared global which utils_tests.uc.js should later check
4 | // @loadOrder 1
5 | // @onlyonce
6 | // ==/UserScript==
7 | _ucUtils.sharedGlobal.test_utils = { x: 42 }
--------------------------------------------------------------------------------
/test_profile/chrome/tests/x_disabled_script.uc.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name x_disabled
3 | // @loadOrder 1
4 | // ==/UserScript==
5 | // This file should not run because it is disabled by pref
6 | console.warn("Disabled script isn't supposed to run!");
--------------------------------------------------------------------------------
/test_profile/chrome/tests/x_disabled_system.sys.mjs:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name x_disabled_system_module
3 | // @loadOrder 1
4 | // ==/UserScript==
5 | // This file should not run because it is disabled by pref
6 | console.warn("Disabled script isn't supposed to run!");
--------------------------------------------------------------------------------
/test_profile/chrome/userChrome.css:
--------------------------------------------------------------------------------
1 | #nav-bar{ background: #f00 !important; }
--------------------------------------------------------------------------------
/test_profile/chrome/utils/chrome.manifest:
--------------------------------------------------------------------------------
1 | content userchromejs ../../../profile/chrome/utils/
2 | content userscripts ../tests/
3 | skin userstyles classic/1.0 ../../../profile/chrome/CSS/
4 | content userchrome ../resources/
5 |
--------------------------------------------------------------------------------
/test_tb_profile/chrome/resources/test_file.txt:
--------------------------------------------------------------------------------
1 | This is a test file used in testing
--------------------------------------------------------------------------------
/test_tb_profile/chrome/resources/test_json.json:
--------------------------------------------------------------------------------
1 | {
2 | "property": "This is a test file used in testing"
3 | }
--------------------------------------------------------------------------------
/test_tb_profile/chrome/resources/write_test_basic.txt:
--------------------------------------------------------------------------------
1 | test file content
--------------------------------------------------------------------------------
/test_tb_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_tb_profile/chrome/tests/aaa_test_script.uc.js:
--------------------------------------------------------------------------------
1 | (()=>{42})();
--------------------------------------------------------------------------------
/test_tb_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.import("chrome://userscripts/content/000_test_runner.jsm");
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_tb_profile/chrome/tests/utils_tests.uc.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name test_utils
3 | // @onlyonce
4 | // @loadOrder 5
5 | // ==/UserScript==
6 |
7 | "use strict";
8 |
9 | (function(){
10 |
11 | const BRAND_NAME = "Thunderbird";
12 | const PREF_ALLLOW_UNSAFE = "userChromeJS.allowUnsafeWrites";
13 | const SHARED_GLOBAL_TEST_X = 42;
14 |
15 | const { Test } = ChromeUtils.importESModule("chrome://userscripts/content/000_test_runner.sys.mjs");
16 |
17 | // Write some stuff to sharedGlobal
18 | _ucUtils.sharedGlobal.test_utils = { x: SHARED_GLOBAL_TEST_X };
19 |
20 | const PREF_LISTENER = new (function(){
21 | let clients = new Set();
22 | let listener = _ucUtils.prefs.addListener("userChromeJS",(value, prefName) => {
23 | for(let client of clients){
24 | client(value,prefName);
25 | }
26 | clients.clear()
27 | });
28 | this.listenOnce = (fun) => {
29 | clients.add(fun);
30 | }
31 | this.forgetAboutIt = () => {
32 | _ucUtils.prefs.removeListener(listener);
33 | listener = null;
34 | }
35 | this.size = () => clients.size;
36 | this.reset = () => {
37 | listener && this.forgetAboutIt();
38 | clients.clear();
39 | }
40 | })();
41 |
42 | // Needs to be alphabetical
43 | const TEST_FILES = [
44 | "000_test_runner.sys.mjs",
45 | "aaa_test_script.uc.js",
46 | "test_module_script.uc.js",
47 | "utils_tests.uc.js"
48 | ];
49 | console.info("%crunning tests...","color: rgb(120,160,240)");
50 | const PROMISES = [
51 | // Can we read data from sharedGlobal
52 | new Test(
53 | "sharedGlobal",
54 | () => { return _ucUtils.sharedGlobal.test_utils.x }
55 | ).expect(SHARED_GLOBAL_TEST_X),
56 |
57 | // Does _ucUtils give us correct brandName
58 | new Test(
59 | "brandName",
60 | () => { return _ucUtils.brandName }
61 | ).expect(BRAND_NAME),
62 |
63 | // calling createElement() without third argument should create a xul element
64 | new Test(
65 | "createXulElement",
66 | () => {
67 | let node = _ucUtils.createElement(
68 | document,
69 | "vbox",
70 | { class: "test-vbox", "hidden": true }
71 | );
72 | return node.outerHTML;
73 | }
74 | ).expect(''),
75 |
76 | // calling createElement() with third argument should create html element
77 | new Test(
78 | "createHTMLElement",
79 | () => {
80 | let node = _ucUtils.createElement(
81 | document,
82 | "div",
83 | { class: "test-div", "hidden": true },
84 | true
85 | );
86 | return node.outerHTML;
87 | }
88 | ).expect(''),
89 |
90 | // TODO createWidget
91 |
92 | // Synchronously read file content with string argument treated as relative path
93 | new Test(
94 | "readFileFromString",
95 | () => { return _ucUtils.fs.readFileSync("test_file.txt").content() }
96 | ).expect("This is a test file used in testing"),
97 |
98 | // Synchronously read file content with reference to File object
99 | new Test("readFileFromFile",
100 | () => {
101 | let fsResult = _ucUtils.fs.getEntry("test_file.txt");
102 | return _ucUtils.fs.readFileSync(fsResult.entry()).content();
103 | }
104 | ).expect("This is a test file used in testing"),
105 |
106 | // Async file read with string argument as relative path
107 | new Test(
108 | "readFileAsync",
109 | () => { return _ucUtils.fs.readFile("test_file.txt") }
110 | ).expectAsync(fsResult => fsResult.content() === "This is a test file used in testing"),
111 |
112 | // Async file read as json
113 | new Test(
114 | "readJSON",
115 | () => {
116 | return new Promise((resolve, reject) => {
117 | _ucUtils.fs.readJSON("test_json.json")
118 | .then(some => resolve(some.property))
119 | .catch(reject)
120 | })
121 | }
122 | ).expectAsync("This is a test file used in testing"),
123 |
124 | // Write some content to text file
125 | new Test(
126 | "writeFileBasic",
127 | () => {
128 | return new Promise((resolve, reject) => {
129 | let bytes = null;
130 | _ucUtils.fs.writeFile("write_test_basic.txt","test file content")
131 | .then(some => { bytes = some })
132 | .then(() => _ucUtils.fs.readFile("write_test_basic.txt"))
133 | .then((fsResult) => resolve(fsResult.content() + ": " + bytes) )
134 | .catch(reject)
135 | })
136 | }
137 | ).expectAsync("test file content: 17"),
138 |
139 | // List names of files in a directory
140 | new Test("listFileNames",
141 | () => {
142 | let names = [];
143 | for(let entry of _ucUtils.fs.getEntry("../")){
144 | if(entry.isFile()){
145 | names.push(entry.leafName);
146 | }
147 | }
148 | return names.join(",");
149 | }
150 | ).expect("userChrome.css"),
151 |
152 | // TODO createFileURI
153 |
154 | // List folder names inside "chrome" directory
155 | new Test(
156 | "getChromeDir",
157 | () => {
158 | let names = [];
159 | for(let entry of _ucUtils.fs.chromeDir()){
160 | if(entry.isDirectory()){
161 | names.push(entry.leafName);
162 | }
163 | }
164 | return names.join(",");
165 | }
166 | ).expect("css,resources,tests,utils"),
167 |
168 | // Get File object from file name
169 | new Test(
170 | "getFSEntry",
171 | () => { return _ucUtils.fs.getEntry("test_file.txt").isFile() }
172 | ).expect(true),
173 |
174 | // return list of script names in directory (not file names)
175 | // Note: aaa_test_script.uc.js does not have a name so it should be first
176 | new Test(
177 | "getScriptData",
178 | () => {
179 | let scripts = _ucUtils.getScriptData();
180 | const names = scripts
181 | .sort((a, b) => (a.name < b.name ? -1 : 1))
182 | .map(a => a.name)
183 | .join(",")
184 | return scripts.length + ";" + names
185 | }
186 | ).expect(TEST_FILES.length+";,test_module_script,test_runner,test_utils"),
187 |
188 | // Tests load order.
189 | // The current script (this one) should be false.
190 | // background-modules should be true
191 | // scripts that have not been run yet should be false.
192 | // NOTE: this script has @loadOrder 5 thus none of the non-backgroundmodules should have been run yet
193 | // This test assumes that none of the test scripts have been manually disabled
194 | new Test(
195 | "getScriptLoadOrder",
196 | () => {
197 | let scripts = _ucUtils.getScriptData();
198 | return scripts
199 | .sort((a, b) => (a.name < b.name ? -1 : 1))
200 | .map(a => a.isRunning)
201 | .join(",");
202 | }
203 | ).expect("false,true,true,false"),
204 |
205 | // Can get reference to first browser-window window-object
206 | new Test(
207 | "getWindows",
208 | () => {
209 | return _ucUtils.windows.get()[0].AppConstants.MOZ_APP_BASENAME;
210 | }
211 | ).expect("Thunderbird"),
212 |
213 | // TODO togglescript
214 |
215 | // Set the pref to false (if it wasn't already) for the following tests
216 | Promise.resolve(_ucUtils.prefs.set(PREF_ALLLOW_UNSAFE,false)),
217 |
218 | // Writing outside of resources directory should fail because pref is disabled
219 | new Test(
220 | "excpectError_writeUserChromeCSS_BeforeStartup",
221 | () => {
222 | return _ucUtils.fs.writeFile("../userChrome.css","#navigation-toolbox{ background: #f00 !important; }")
223 | }
224 | ).expectError(),
225 |
226 | // This test should resolve after updateStyleSheet test has set the allow-unsafe pref to true
227 | new Test(
228 | "prefChangedToTrue",
229 | () => {
230 | return new Promise((resolve, reject) => {
231 | PREF_LISTENER.listenOnce((val,pref) => resolve(`${pref},${val.value}`));
232 | Test.resolveOnTimeout(2000).then(reject);
233 | })
234 | }
235 | ).expectAsync(PREF_ALLLOW_UNSAFE+",true"),
236 |
237 | // Set pref to allow writing outside of resources directory, and then write userChrome.css
238 | new Test(
239 | "updateStyleSheet",
240 | () => {
241 | _ucUtils.prefs.set(PREF_ALLLOW_UNSAFE,true);
242 | const getNavBarStyle = () => window.getComputedStyle(document.getElementById("navigation-toolbox"));
243 |
244 | return new Promise((resolve, reject) => {
245 | _ucUtils.windowIsReady(window)
246 | .then( () => {
247 | // The color expected here is set in one of the tests that follow
248 | let oldColor = getNavBarStyle().backgroundColor;
249 | _ucUtils.fs.writeFile("../userChrome.css","#navigation-toolbox{ background: #ba5 !important; }")
250 | .then(() => _ucUtils.updateStyleSheet())
251 | .then(()=>Test.resolveOnTimeout(2000)) // necessary because the style may not be applied immediately
252 | .then( () => resolve(oldColor + " : " + getNavBarStyle().backgroundColor) )
253 | })
254 | .catch(reject)
255 | })
256 | }
257 | ).expectAsync("rgb(255, 0, 0) : rgb(187, 170, 85)"),
258 |
259 | // TODO updateMenuStatus
260 |
261 | // Should resolve startupFinished when startup has been finished
262 | new Test(
263 | "startupFinished",
264 | () => {
265 | return new Promise((resolve ,reject) => {
266 | setTimeout(reject, 8000); // Startup should not take 8 seconds
267 | _ucUtils.startupFinished()
268 | .then(() => resolve(42))
269 | })
270 | }
271 | ).expectAsync(42),
272 |
273 | // Has current window object been fully restored
274 | new Test(
275 | "WindowIsReady",
276 | () => {
277 | return new Promise((resolve, reject) => {
278 | setTimeout(reject, 8000);
279 | _ucUtils.windowIsReady(window)
280 | .then(() => resolve(42))
281 | })
282 | }
283 | ).expectAsync(42),
284 |
285 | new Test(
286 | "registerHotkey",
287 | () => {
288 | let details = {
289 | id: "myHotkey",
290 | modifiers: "ctrl shift",
291 | key: "Y"
292 | };
293 |
294 | let val = _ucUtils.registerHotkey(details,() => ({}) );
295 | if(!val){
296 | return false
297 | }
298 | let key = document.getElementById("myHotkey");
299 | return key.getAttribute("modifiers") + "," + key.getAttribute("key");
300 | }
301 | ).expect("accel,shift,Y"),
302 |
303 | // TODO loadURI
304 |
305 | // TODO showNotification
306 |
307 | new Test(
308 | "CancelRestart",
309 | () => {
310 |
311 | return new Promise((resolve ,reject) => {
312 | let reason = null;
313 |
314 | let cancelObserver = (subject, topic, data) => {
315 | subject.data = true;
316 | reason = data;
317 | Services.obs.removeObserver(cancelObserver, "quit-application-requested");
318 | };
319 |
320 | Services.obs.addObserver(cancelObserver, "quit-application-requested");
321 |
322 | Test.resolveOnTimeout(2000)
323 | .then(_ucUtils.restart)
324 | .then(
325 | some => resolve( `${reason} ${some ? "succeeded" : "canceled"}` )
326 | ).catch(reject)
327 | })
328 | }
329 | ).expectAsync("restart canceled"),
330 |
331 | /**
332 | * ! Keep these below as the last tests !
333 | *
334 | * Restore old userChrome.css state
335 | * The above test setup sets pref to allow writing outside of resources
336 | * so this should succeed.
337 | */
338 | // This test should resolve after timeout because pref listener was removed
339 | // Note: this test will fail if pref "userChromeJS.firstRunShown" isn't set on startup
340 | new Test(
341 | "prefNotChangedToFalse",
342 | () => {
343 | return new Promise((resolve, reject) => {
344 | PREF_LISTENER.listenOnce(reject);
345 | Test.resolveOnTimeout(200).then(PREF_LISTENER.forgetAboutIt);
346 | Test.resolveOnTimeout(4200)
347 | .then(() => resolve(PREF_LISTENER.size()));
348 | })
349 | }
350 | ).expectAsync(1),
351 |
352 | new Test(
353 | "writeUserChromeCSS",
354 | () => {
355 | return new Promise((resolve, reject) => {
356 | Test.resolveOnTimeout(4000)
357 | .then(() => {
358 | return _ucUtils.fs.writeFile("../userChrome.css","#navigation-toolbox{ background: #f00 !important; }")
359 | })
360 | .then(resolve)
361 | .catch(reject)
362 | })
363 | }
364 | ).expectAsync(51) // 50 bytes written
365 | // Set allowUnsafeWrites pref back to false
366 | .then(() => _ucUtils.prefs.set(PREF_ALLLOW_UNSAFE,false))
367 | .then(() => {
368 | // Check that writing userChrome.css now fails again
369 | new Test(
370 | "excpectError_writeUserChromeCSS_AfterStartup",
371 | () => {
372 | return _ucUtils.fs.writeFile("../userChrome.css","#navigation-toolbox{ background: #f00 !important; }")
373 | }
374 | ).expectError()
375 | })
376 |
377 | ];
378 |
379 | Test.waitForTestSet(PROMISES)
380 | .finally(() => PREF_LISTENER.reset());
381 |
382 | })();
383 |
--------------------------------------------------------------------------------
/test_tb_profile/chrome/userChrome.css:
--------------------------------------------------------------------------------
1 | #navigation-toolbox{ background: #f00 !important; }
--------------------------------------------------------------------------------
/test_tb_profile/chrome/utils/chrome.manifest:
--------------------------------------------------------------------------------
1 | content userchromejs ../../../profile/chrome/utils/
2 | content userscripts ../tests/
3 | skin userstyles classic/1.0 ../../../profile/chrome/CSS/
4 | content userchrome ../resources/
5 |
--------------------------------------------------------------------------------
/types/api/FileSystem.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The FileSystemResult result object is one of four types:
3 | * - Filesystem.RESULT_FILE get reference to a file
4 | * - Filesystem.RESULT_DIRECTORY get referece to a directory
5 | * - Filesystem.RESULT_ERROR non-existent file or other kind of error
6 | * - Filesystem.RESULT_CONTENT file read operation results
7 | *
8 | * The result object has various methods to access underlying data.
9 | */
10 | interface FileSystemResult {
11 | /**
12 | * @throws if called on anything except CONTENT type.
13 | */
14 | content(replaceNewlines: boolean): string;
15 |
16 | /** @returns nsIFile[] */
17 | entries(): any[];
18 |
19 | /**
20 | * @throws throws if called on CONTENT or ERROR types.
21 | * @returns nsIFile
22 | */
23 | entry(): any;
24 |
25 | error(): any | null;
26 | isContent(): boolean;
27 | isDirectory(): boolean;
28 | isError(): boolean;
29 | isFile(): boolean;
30 | read(): string;
31 | readSync(): string;
32 |
33 | /**
34 | * Tries to open a given file entry path in OS file manager.
35 | *
36 | * @returns true or false indicating success.
37 | */
38 | showInFileManager(): boolean;
39 |
40 | get fileURI(): string;
41 |
42 | /** size of read content or size of the file on disk */
43 | get size(): number;
44 |
45 | type: any;
46 | }
47 |
48 | interface WriteFileOptions {
49 | tmpPath: boolean;
50 | }
51 |
52 | /**
53 | * Scripts should generally use the resources folder for their files. The helper
54 | * functions interacting with filesystem expect resources to be the root folder
55 | * for script operations.
56 | *
57 | * The resources folder is registered to `chrome://` scheme so scripts and
58 | * stylesheets can use the following URL to access files within it:
59 | * ```
60 | * "chrome://userChrome/content/.txt"
61 | * ```
62 | *
63 | * Scripts folder is registered to: `chrome://userScripts/content/`.
64 | *
65 | * The loader module folder is registered to `chrome://userchromejs/content/`.
66 | *
67 | * Main idea is that various methods of the FileSystem namespace return a
68 | * {@link FileSystemResult} object instead of the actual operation result
69 | * directly.
70 | */
71 | interface UC_FileSystem {
72 | chromeDir(): FileSystemResult;
73 | getEntry(fileName: string): FileSystemResult;
74 | readFile(fileName: string): Promise;
75 | readFileSync(some: string | nsIFile): FileSystemResult;
76 |
77 | /**
78 | * Asynchronously try to read a file and parse it as json.
79 | * If file can't be parsed then returns `null`.
80 | */
81 | readJSON(fileName: string): Promise