Initial release v128.0-1
10
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/firefox-*
|
||||
/camoufox-*
|
||||
/mozilla-unified
|
||||
/extra-docs
|
||||
/.vscode
|
||||
_old/
|
||||
dist/
|
||||
/bundle/fonts/extra
|
||||
launch
|
||||
launch.exe
|
||||
373
LICENSE
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
105
Makefile
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
include upstream.sh
|
||||
export
|
||||
|
||||
ff_source_dir := firefox-$(version)
|
||||
lw_source_dir := camoufox-$(version)-$(release)
|
||||
|
||||
debs := python3 python3-dev python3-pip p7zip-full golang-go
|
||||
rpms := python3 python3-devel p7zip golang
|
||||
pacman := python python-pip p7zip go
|
||||
|
||||
.PHONY: help fetch clean distclean build package build-launcher check-arch edits run bootstrap dir package-common package-linux package-macos package-windows
|
||||
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " fetch - Clone Firefox source code"
|
||||
@echo " bootstrap - Set up build environment"
|
||||
@echo " dir - Prepare Camoufox source directory"
|
||||
@echo " edits - Camoufox developer UI"
|
||||
@echo " build-launcher - Build launcher"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " distclean - Remove everything including downloads"
|
||||
@echo " build - Build Camoufox"
|
||||
@echo " package-linux - Package Camoufox for Linux"
|
||||
@echo " package-macos - Package Camoufox for macOS"
|
||||
@echo " package-windows - Package Camoufox for Windows"
|
||||
@echo " run - Run Camoufox"
|
||||
|
||||
fetch:
|
||||
git clone --depth 1 --branch $(BASE_BRANCH) --single-branch $(REMOTE_URL) $(ff_source_dir)
|
||||
cd $(ff_source_dir) && git fetch --depth 1 origin $(BASE_REVISION) && git checkout $(BASE_REVISION)
|
||||
|
||||
dir:
|
||||
@if [ ! -d $(ff_source_dir) ]; then \
|
||||
make fetch; \
|
||||
fi
|
||||
rm -rf $(lw_source_dir)
|
||||
cp -r $(ff_source_dir) $(lw_source_dir)
|
||||
python3 scripts/patch.py $(version) $(release)
|
||||
|
||||
bootstrap: dir
|
||||
(sudo apt-get -y install $(debs) || sudo dnf -y install $(rpms) || sudo pacman -Sy $(pacman))
|
||||
cd $(lw_source_dir) && MOZBUILD_STATE_PATH=$$HOME/.mozbuild ./mach --no-interactive bootstrap --application-choice=browser
|
||||
|
||||
clean:
|
||||
rm -rf $(lw_source_dir)
|
||||
|
||||
distclean: clean
|
||||
rm -rf $(ff_source_dir)
|
||||
|
||||
build:
|
||||
@if [ ! -d $(lw_source_dir) ]; then \
|
||||
make dir; \
|
||||
fi
|
||||
cd $(lw_source_dir) && ./mach build
|
||||
|
||||
edits:
|
||||
python ./scripts/developer.py
|
||||
|
||||
check-arch:
|
||||
@if [ "$(arch)" != "x64" ] && [ "$(arch)" != "x86" ] && [ "$(arch)" != "arm64" ]; then \
|
||||
echo "Error: Invalid arch value. Must be x64, x86, or arm64."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
build-launcher: check-arch
|
||||
cd launcher && ./build.sh $(arch) $(os)
|
||||
|
||||
package-common: check-arch
|
||||
cd $(lw_source_dir) && cat browser/locales/shipped-locales | xargs ./mach package-multi-locale --locales
|
||||
cp -v $(lw_source_dir)/obj-*/dist/camoufox-$(version)-$(release).*.* .
|
||||
|
||||
package-linux: package-common
|
||||
make build-launcher arch=$(arch) os=linux;
|
||||
python3 scripts/package.py linux \
|
||||
--includes \
|
||||
settings/chrome.css \
|
||||
bundle/fontconfigs \
|
||||
--version $(version) \
|
||||
--release $(release) \
|
||||
--arch $(arch) \
|
||||
--fonts windows macos linux
|
||||
|
||||
package-macos: package-common
|
||||
make build-launcher arch=$(arch) os=macos;
|
||||
python3 scripts/package.py macos \
|
||||
--includes \
|
||||
settings/chrome.css \
|
||||
--version $(version) \
|
||||
--release $(release) \
|
||||
--arch $(arch) \
|
||||
--fonts windows linux
|
||||
|
||||
package-windows: package-common
|
||||
make build-launcher arch=$(arch) os=windows;
|
||||
python3 scripts/package.py windows \
|
||||
--includes \
|
||||
settings/chrome.css \
|
||||
~/.mozbuild/vs/VC/Redist/MSVC/14.38.33135/$(arch)/Microsoft.VC143.CRT/*.dll \
|
||||
--version $(version) \
|
||||
--release $(release) \
|
||||
--arch $(arch) \
|
||||
--fonts macos linux
|
||||
|
||||
run:
|
||||
cd $(lw_source_dir) && rm -rf ~/.camoufox && ./mach run
|
||||
360
README.md
Normal file
81
additions/browser/base/content/aboutDialog.css
Normal file
58
additions/browser/base/content/aboutDialog.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Services = object with smart getters for common XPCOM services
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
var { AppConstants } = ChromeUtils.import(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
|
||||
async function init(aEvent) {
|
||||
if (aEvent.target != document) {
|
||||
return;
|
||||
}
|
||||
|
||||
var distroId = Services.prefs.getCharPref("distribution.id", "");
|
||||
if (distroId) {
|
||||
var distroAbout = Services.prefs.getStringPref("distribution.about", "");
|
||||
// If there is about text, we always show it.
|
||||
if (distroAbout) {
|
||||
var distroField = document.getElementById("distribution");
|
||||
distroField.value = distroAbout;
|
||||
distroField.style.display = "block";
|
||||
}
|
||||
// If it's not a mozilla distribution, show the rest,
|
||||
// unless about text exists, then we always show.
|
||||
if (!distroId.startsWith("mozilla-") || distroAbout) {
|
||||
var distroVersion = Services.prefs.getCharPref(
|
||||
"distribution.version",
|
||||
""
|
||||
);
|
||||
if (distroVersion) {
|
||||
distroId += " - " + distroVersion;
|
||||
}
|
||||
|
||||
var distroIdField = document.getElementById("distributionId");
|
||||
distroIdField.value = distroId;
|
||||
distroIdField.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
// Display current version number
|
||||
let versionField = document.getElementById("versionNumber");
|
||||
versionField.innerHTML = AppConstants.MOZ_APP_VERSION_DISPLAY;
|
||||
|
||||
window.sizeToContent();
|
||||
|
||||
if (AppConstants.platform == "macosx") {
|
||||
window.moveTo(
|
||||
screen.availWidth / 2 - window.outerWidth / 2,
|
||||
screen.availHeight / 5
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
55
additions/browser/base/content/aboutDialog.xhtml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0"?> <!-- -*- Mode: HTML -*- -->
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/aboutDialog.css" type="text/css"?>
|
||||
|
||||
<window xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
id="aboutDialog"
|
||||
windowtype="Browser:About"
|
||||
onload="init(event);"
|
||||
#ifdef XP_MACOSX
|
||||
inwindowmenu="false"
|
||||
#else
|
||||
data-l10n-id="aboutDialog-title"
|
||||
#endif
|
||||
role="dialog"
|
||||
aria-describedby="version distribution distributionId communityDesc contributeDesc trademark"
|
||||
>
|
||||
#ifdef XP_MACOSX
|
||||
#include macWindow.inc.xhtml
|
||||
#else
|
||||
<script src="chrome://browser/content/utilityOverlay.js"/>
|
||||
#endif
|
||||
|
||||
<linkset>
|
||||
<html:link rel="localization" href="branding/brand.ftl"/>
|
||||
<html:link rel="localization" href="browser/aboutDialog.ftl"/>
|
||||
</linkset>
|
||||
|
||||
<script src="chrome://browser/content/aboutDialog.js"/>
|
||||
|
||||
<div id="grid">
|
||||
<div id="left" />
|
||||
<div id="right">
|
||||
<label id="wordmark">Camoufox</label>
|
||||
<label id="versionNumber" />
|
||||
<label id="distribution" />
|
||||
<label id="distributionId" />
|
||||
<label id="aboutText">
|
||||
Camoufox is an independent fork of Firefox for webscraping.
|
||||
</label>
|
||||
<label id="websiteLink" is="text-link" href="https://github.com/daijro/camoufox">https://github.com/daijro/camoufox</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<keyset>
|
||||
<key keycode="VK_ESCAPE" oncommand="window.close();"/>
|
||||
</keyset>
|
||||
|
||||
</window>
|
||||
|
||||
BIN
additions/browser/branding/camoufox/PrivateBrowsing_150.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
additions/browser/branding/camoufox/PrivateBrowsing_70.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
additions/browser/branding/camoufox/VisualElements_150.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
additions/browser/branding/camoufox/VisualElements_70.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
additions/browser/branding/camoufox/background.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
64
additions/browser/branding/camoufox/branding.nsi
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# NSIS branding defines for unofficial builds.
|
||||
# The official release build branding.nsi is located in other-license/branding/firefox/
|
||||
# The nightly build branding.nsi is located in browser/installer/windows/nsis/
|
||||
|
||||
# BrandFullNameInternal is used for some registry and file system values
|
||||
# instead of BrandFullName and typically should not be modified.
|
||||
!define BrandFullNameInternal "Mozilla Developer Preview"
|
||||
!define BrandFullName "Mozilla Developer Preview"
|
||||
!define CompanyName "mozilla.org"
|
||||
!define URLInfoAbout "https://www.mozilla.org"
|
||||
!define HelpLink "https://support.mozilla.org"
|
||||
|
||||
!define URLStubDownloadX86 "https://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
|
||||
!define URLStubDownloadAMD64 "https://download.mozilla.org/?os=win64&lang=${AB_CD}&product=firefox-latest"
|
||||
!define URLStubDownloadAArch64 "https://download.mozilla.org/?os=win64-aarch64&lang=${AB_CD}&product=firefox-latest"
|
||||
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"
|
||||
!define URLSystemRequirements "https://www.mozilla.org/firefox/system-requirements/"
|
||||
!define Channel "unofficial"
|
||||
|
||||
# The installer's certificate name and issuer expected by the stub installer
|
||||
!define CertNameDownload "Mozilla Corporation"
|
||||
!define CertIssuerDownload "DigiCert SHA2 Assured ID Code Signing CA"
|
||||
|
||||
# Dialog units are used so the UI displays correctly with the system's DPI
|
||||
# settings.
|
||||
!define PROFILE_CLEANUP_LABEL_TOP "35u"
|
||||
!define PROFILE_CLEANUP_LABEL_LEFT "0"
|
||||
!define PROFILE_CLEANUP_LABEL_WIDTH "100%"
|
||||
!define PROFILE_CLEANUP_LABEL_HEIGHT "80u"
|
||||
!define PROFILE_CLEANUP_LABEL_ALIGN "center"
|
||||
!define PROFILE_CLEANUP_CHECKBOX_LEFT "center"
|
||||
!define PROFILE_CLEANUP_CHECKBOX_WIDTH "100%"
|
||||
!define PROFILE_CLEANUP_BUTTON_LEFT "center"
|
||||
!define INSTALL_BLURB_TOP "137u"
|
||||
!define INSTALL_BLURB_WIDTH "60u"
|
||||
!define INSTALL_FOOTER_TOP "-48u"
|
||||
!define INSTALL_FOOTER_WIDTH "250u"
|
||||
!define INSTALL_INSTALLING_TOP "70u"
|
||||
!define INSTALL_INSTALLING_LEFT "0"
|
||||
!define INSTALL_INSTALLING_WIDTH "100%"
|
||||
!define INSTALL_PROGRESS_BAR_TOP "112u"
|
||||
!define INSTALL_PROGRESS_BAR_LEFT "20%"
|
||||
!define INSTALL_PROGRESS_BAR_WIDTH "60%"
|
||||
!define INSTALL_PROGRESS_BAR_HEIGHT "12u"
|
||||
|
||||
!define PROFILE_CLEANUP_CHECKBOX_TOP_MARGIN "20u"
|
||||
!define PROFILE_CLEANUP_BUTTON_TOP_MARGIN "20u"
|
||||
!define PROFILE_CLEANUP_BUTTON_X_PADDING "40u"
|
||||
!define PROFILE_CLEANUP_BUTTON_Y_PADDING "4u"
|
||||
|
||||
# Font settings that can be customized for each channel
|
||||
!define INSTALL_HEADER_FONT_SIZE 28
|
||||
!define INSTALL_HEADER_FONT_WEIGHT 400
|
||||
!define INSTALL_INSTALLING_FONT_SIZE 28
|
||||
!define INSTALL_INSTALLING_FONT_WEIGHT 400
|
||||
|
||||
# UI Colors that can be customized for each channel
|
||||
!define COMMON_TEXT_COLOR 0xFFFFFF
|
||||
!define COMMON_BACKGROUND_COLOR 0x000000
|
||||
!define INSTALL_INSTALLING_TEXT_COLOR 0xFFFFFF
|
||||
13
additions/browser/branding/camoufox/configure.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# there is a possible patch to consider when changing this:
|
||||
# see: patches/browser-confvars.patch
|
||||
|
||||
MOZ_APP_NAME=camoufox
|
||||
MOZ_APP_BASENAME=Camoufox
|
||||
MOZ_APP_PROFILE=camoufox
|
||||
MOZ_APP_VENDOR=Camoufox
|
||||
MOZ_APP_DISPLAYNAME=Camoufox
|
||||
MOZ_APP_REMOTINGNAME=camoufox
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 35 KiB |
BIN
additions/browser/branding/camoufox/content/about-logo.png
Normal file
|
After Width: | Height: | Size: 682 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"></svg>
|
||||
|
After Width: | Height: | Size: 72 B |
BIN
additions/browser/branding/camoufox/content/about-logo@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="336" height="48"></svg>
|
||||
|
After Width: | Height: | Size: 71 B |
BIN
additions/browser/branding/camoufox/content/about.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="172" height="48"></svg>
|
||||
|
After Width: | Height: | Size: 71 B |
18
additions/browser/branding/camoufox/content/jar.mn
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
browser.jar:
|
||||
% content branding %content/branding/ contentaccessible=yes
|
||||
content/branding/about.png
|
||||
content/branding/about-logo.png
|
||||
content/branding/about-logo.svg
|
||||
content/branding/about-logo@2x.png
|
||||
content/branding/about-wordmark.svg
|
||||
content/branding/firefox-wordmark.svg
|
||||
content/branding/icon16.png (../default16.png)
|
||||
content/branding/icon32.png (../default32.png)
|
||||
content/branding/icon48.png (../default48.png)
|
||||
content/branding/icon64.png (../default64.png)
|
||||
content/branding/icon128.png (../default128.png)
|
||||
content/branding/aboutDialog.css
|
||||
7
additions/browser/branding/camoufox/content/moz.build
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
JAR_MANIFESTS += ["jar.mn"]
|
||||
BIN
additions/browser/branding/camoufox/default128.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
additions/browser/branding/camoufox/default16.png
Normal file
|
After Width: | Height: | Size: 830 B |
BIN
additions/browser/branding/camoufox/default22.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
additions/browser/branding/camoufox/default24.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
additions/browser/branding/camoufox/default256.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
additions/browser/branding/camoufox/default32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
additions/browser/branding/camoufox/default48.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
additions/browser/branding/camoufox/default64.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
additions/browser/branding/camoufox/disk.icns
Normal file
BIN
additions/browser/branding/camoufox/document.icns
Normal file
BIN
additions/browser/branding/camoufox/document.ico
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
additions/browser/branding/camoufox/document_pdf.ico
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
additions/browser/branding/camoufox/dsstore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<Application xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
|
||||
<VisualElements
|
||||
ShowNameOnSquare150x150Logo='on'
|
||||
Square150x150Logo='browser\VisualElements\VisualElements_150.png'
|
||||
Square70x70Logo='browser\VisualElements\VisualElements_70.png'
|
||||
ForegroundText='light'
|
||||
BackgroundColor='#14171a'/>
|
||||
</Application>
|
||||
BIN
additions/browser/branding/camoufox/firefox.icns
Normal file
BIN
additions/browser/branding/camoufox/firefox.ico
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
additions/browser/branding/camoufox/firefox64.ico
Normal file
|
After Width: | Height: | Size: 112 KiB |
13
additions/browser/branding/camoufox/locales/en-US/brand.dtd
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!ENTITY brandShorterName "Camoufox">
|
||||
<!ENTITY brandShortName "Camoufox">
|
||||
<!ENTITY brandFullName "Camoufox">
|
||||
<!-- LOCALIZATION NOTE (brandProductName):
|
||||
This brand name can be used in messages where the product name needs to
|
||||
remain unchanged across different versions (Nightly, Beta, etc.). -->
|
||||
<!ENTITY brandProductName "Camoufox">
|
||||
<!ENTITY vendorShortName "Camoufox">
|
||||
<!ENTITY trademarkInfo.part1 " ">
|
||||
23
additions/browser/branding/camoufox/locales/en-US/brand.ftl
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
## Firefox Brand
|
||||
##
|
||||
## Firefox must be treated as a brand, and kept in English.
|
||||
## It cannot be:
|
||||
## - Declined to adapt to grammatical case.
|
||||
## - Transliterated.
|
||||
## - Translated.
|
||||
##
|
||||
## Reference: https://www.mozilla.org/styleguide/communications/translation/
|
||||
|
||||
-brand-shorter-name = Camoufox
|
||||
-brand-short-name = Camoufox
|
||||
-brand-full-name = Camoufox
|
||||
-brand-shortcut-name = Camoufox
|
||||
# This brand name can be used in messages where the product name needs to
|
||||
# remain unchanged across different versions (Nightly, Beta, etc.).
|
||||
-brand-product-name = Camoufox
|
||||
-vendor-short-name = Camoufox
|
||||
trademarkInfo = { " " }
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
brandShorterName=Camoufox
|
||||
brandShortName=Camoufox
|
||||
brandFullName=Camoufox
|
||||
# LOCALIZATION NOTE(brandProductName):
|
||||
# This brand name can be used in messages where the product name needs to
|
||||
# remain unchanged across different versions (Nightly, Beta, etc.).
|
||||
brandProductName=Camoufox
|
||||
vendorShortName=Camoufox
|
||||
|
||||
syncBrandShortName=Camoufox Sync
|
||||
13
additions/browser/branding/camoufox/locales/jar.mn
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#filter substitution
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
[localization] @AB_CD@.jar:
|
||||
branding (en-US/**/*.ftl)
|
||||
|
||||
@AB_CD@.jar:
|
||||
% locale branding @AB_CD@ %locale/branding/
|
||||
# Unofficial branding only exists in en-US
|
||||
locale/branding/brand.dtd (en-US/brand.dtd)
|
||||
locale/branding/brand.properties (en-US/brand.properties)
|
||||
9
additions/browser/branding/camoufox/locales/moz.build
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DEFINES['MOZ_DISTRIBUTION_ID_UNQUOTED'] = CONFIG['MOZ_DISTRIBUTION_ID']
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
BIN
additions/browser/branding/camoufox/logo.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
13
additions/browser/branding/camoufox/moz.build
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += ["content", "locales"]
|
||||
|
||||
DIST_SUBDIR = "browser"
|
||||
export("DIST_SUBDIR")
|
||||
|
||||
include("../branding-common.mozbuild")
|
||||
FirefoxBranding()
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
BIN
additions/browser/branding/camoufox/newtab.ico
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
additions/browser/branding/camoufox/newwindow.ico
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
additions/browser/branding/camoufox/pbmode.ico
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<Application xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
|
||||
<VisualElements
|
||||
ShowNameOnSquare150x150Logo='on'
|
||||
Square150x150Logo='browser\VisualElements\PrivateBrowsing_150.png'
|
||||
Square70x70Logo='browser\VisualElements\PrivateBrowsing_70.png'
|
||||
ForegroundText='light'
|
||||
BackgroundColor='#14171a'/>
|
||||
</Application>
|
||||
BIN
additions/browser/branding/camoufox/stubinstaller/bgstub.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,61 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
body {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#label,
|
||||
#progress_background,
|
||||
#blurb {
|
||||
text-align: center;
|
||||
margin: 20px 30px;
|
||||
}
|
||||
|
||||
#label {
|
||||
font-size: 40px;
|
||||
margin-top: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#progress_background {
|
||||
margin: 0 auto;
|
||||
width: 60%;
|
||||
height: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
body.high-contrast #progress_background {
|
||||
outline: solid;
|
||||
}
|
||||
|
||||
#progress_bar {
|
||||
margin: 0;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background-color: #00AAFF;
|
||||
}
|
||||
|
||||
/* In high contrast mode, fill the entire progress bar with its border. */
|
||||
body.high-contrast #progress_bar {
|
||||
/* This border should be the height of progress_background. */
|
||||
border-top: 24px solid;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* This layout doesn't want the header or content text. */
|
||||
#header, #content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#blurb {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* The footer goes in the bottom right corner. */
|
||||
#footer {
|
||||
position: fixed;
|
||||
right: 50px;
|
||||
bottom: 59px;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
body {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#header,
|
||||
#refreshCheckboxContainer,
|
||||
#refreshButtonContainer {
|
||||
text-align: center;
|
||||
margin-left: 40px;
|
||||
margin-right: 40px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
#header {
|
||||
font-size: 35px;
|
||||
font-weight: normal;
|
||||
margin-top: 45px;
|
||||
}
|
||||
|
||||
#refreshCheckbox {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#checkboxLabel {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#refreshButton {
|
||||
padding: 8px 40px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* The footer goes in the bottom right corner. */
|
||||
#footer {
|
||||
position: fixed;
|
||||
right: 50px;
|
||||
bottom: 59px;
|
||||
}
|
||||
BIN
additions/browser/branding/camoufox/wizHeader.bmp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
additions/browser/branding/camoufox/wizHeaderRTL.bmp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
additions/browser/branding/camoufox/wizWatermark.bmp
Normal file
|
After Width: | Height: | Size: 151 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "None",
|
||||
"description": "Camoufox is not designed for manual use.",
|
||||
"manifest_version": 2,
|
||||
"version": "1",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "none@search.mozilla.org"
|
||||
}
|
||||
},
|
||||
"hidden": true,
|
||||
"chrome_settings_overrides": {
|
||||
"search_provider": {
|
||||
"keyword": ["@none"],
|
||||
"name": "None",
|
||||
"search_url": "http://127.0.0.1",
|
||||
"search_url_get_params": "&q={searchTerms}",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
malformedURI2=Please check that the URL is correct and try again.
|
||||
fileNotFound=Camoufox can’t find the file at %S.
|
||||
fileAccessDenied=The file at %S is not readable.
|
||||
dnsNotFound2=We can’t connect to the server at %S.
|
||||
unknownProtocolFound=Camoufox doesn’t know how to open this address, because one of the following protocols (%S) isn’t associated with any program or is not allowed in this context.
|
||||
connectionFailure=Camoufox can’t establish a connection to the server at %S.
|
||||
netInterrupt=The connection to %S was interrupted while the page was loading.
|
||||
netTimeout=The server at %S is taking too long to respond.
|
||||
redirectLoop=Camoufox has detected that the server is redirecting the request for this address in a way that will never complete.
|
||||
## LOCALIZATION NOTE (confirmRepostPrompt): In this item, don’t translate "%S"
|
||||
confirmRepostPrompt=To display this page, %S must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.
|
||||
resendButton.label=Resend
|
||||
unknownSocketType=Camoufox doesn’t know how to communicate with the server.
|
||||
netReset=The connection to the server was reset while the page was loading.
|
||||
notCached=This document is no longer available.
|
||||
netOffline=Camoufox is currently in offline mode and can’t browse the Web.
|
||||
isprinting=The document cannot change while Printing or in Print Preview.
|
||||
deniedPortAccess=This address uses a network port which is normally used for purposes other than Web browsing. Camoufox has canceled the request for your protection.
|
||||
proxyResolveFailure=Camoufox is configured to use a proxy server that can’t be found.
|
||||
proxyConnectFailure=Camoufox is configured to use a proxy server that is refusing connections.
|
||||
contentEncodingError=The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.
|
||||
unsafeContentType=The page you are trying to view cannot be shown because it is contained in a file type that may not be safe to open. Please contact the website owners to inform them of this problem.
|
||||
externalProtocolTitle=External Protocol Request
|
||||
externalProtocolPrompt=An external application must be launched to handle %1$S: links.\n\n\nRequested link:\n\n%2$S\n\nApplication: %3$S\n\n\nIf you were not expecting this request it may be an attempt to exploit a weakness in that other program. Cancel this request unless you are sure it is not malicious.\n
|
||||
#LOCALIZATION NOTE (externalProtocolUnknown): The following string is shown if the application name can't be determined
|
||||
externalProtocolUnknown=<Unknown>
|
||||
externalProtocolChkMsg=Remember my choice for all links of this type.
|
||||
externalProtocolLaunchBtn=Launch application
|
||||
malwareBlocked=The site at %S has been reported as an attack site and has been blocked based on your security preferences.
|
||||
harmfulBlocked=The site at %S has been reported as a potentially harmful site and has been blocked based on your security preferences.
|
||||
unwantedBlocked=The site at %S has been reported as serving unwanted software and has been blocked based on your security preferences.
|
||||
deceptiveBlocked=This web page at %S has been reported as a deceptive site and has been blocked based on your security preferences.
|
||||
cspBlocked=This page has a content security policy that prevents it from being loaded in this way.
|
||||
xfoBlocked=This page has an X-Frame-Options policy that prevents it from being loaded in this context.
|
||||
corruptedContentErrorv2=The site at %S has experienced a network protocol violation that cannot be repaired.
|
||||
## LOCALIZATION NOTE (sslv3Used) - Do not translate "%S".
|
||||
sslv3Used=Camoufox cannot guarantee the safety of your data on %S because it uses SSLv3, a broken security protocol.
|
||||
inadequateSecurityError=The website tried to negotiate an inadequate level of security.
|
||||
blockedByPolicy=Your organization has blocked access to this page or website.
|
||||
networkProtocolError=Camoufox has experienced a network protocol violation that cannot be repaired.
|
||||
BIN
additions/browser/themes/addons/dark/background.gif
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
92
additions/browser/themes/addons/dark/manifest.json
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "firefox-compact-dark@mozilla.org"
|
||||
}
|
||||
},
|
||||
|
||||
"name": "Dark",
|
||||
"description": "A theme with a dark color scheme.",
|
||||
"author": "Mozilla",
|
||||
"version": "1.2",
|
||||
|
||||
"icons": { "32": "icon.svg" },
|
||||
|
||||
"theme": {
|
||||
"colors": {
|
||||
"button_background_active": "#333333",
|
||||
"button_background_hover": "#282828",
|
||||
"bookmark_text": "rgba(255, 255, 255, 0.8)",
|
||||
"frame": "#000000",
|
||||
"frame_inactive": "#000000",
|
||||
"icons": "rgba(255, 255, 255, 0.8)",
|
||||
"icons_attention": "#9400ff",
|
||||
"ntp_background": "#000000",
|
||||
"ntp_text": "rgba(255, 255, 255, 0.8)",
|
||||
"popup": "#101010",
|
||||
"popup_border": "#303030",
|
||||
"popup_highlight": "#303030",
|
||||
"popup_highlight_text": "white",
|
||||
"popup_text": "rgba(255, 255, 255, 0.8)",
|
||||
"sidebar": "#101010",
|
||||
"sidebar_border": "#303030",
|
||||
"sidebar_highlight": "#303030",
|
||||
"sidebar_highlight_text": "white",
|
||||
"sidebar_text": "rgba(255, 255, 255, 0.8)",
|
||||
"tab_background_separator": "transparent",
|
||||
"tab_background_text": "#aaaaaa",
|
||||
"tab_loading": "white",
|
||||
"tab_selected": "rgba(200, 200, 200, 0.1)",
|
||||
"tab_text": "#ffffff",
|
||||
"tab_line": "rgba(255, 255, 255, 0.05)",
|
||||
"toolbar": "rgba(18, 18, 18, 0.8)",
|
||||
"toolbar_bottom_separator": "#101010",
|
||||
"toolbar_field": "#000000",
|
||||
"toolbar_field_border": "transparent",
|
||||
"toolbar_field_border_focus": "#303030",
|
||||
"toolbar_field_focus": "#111111",
|
||||
"toolbar_field_highlight": "#333333",
|
||||
"toolbar_field_highlight_text": "white",
|
||||
"toolbar_field_separator": "#101010",
|
||||
"toolbar_field_text": "rgba(255, 255, 255, 0.8)",
|
||||
"toolbar_field_text_focus": "white",
|
||||
"toolbar_top_separator": "rgba(18, 18, 18, 0.0)",
|
||||
"toolbar_vertical_separator": "rgba(255, 255, 255, 0.06)"
|
||||
},
|
||||
"properties": {
|
||||
"color_scheme": "dark",
|
||||
"panel_active": "color-mix(in srgb, currentColor 14%, transparent)",
|
||||
"toolbar_field_icon_opacity": "1",
|
||||
"zap_gradient": "linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%)"
|
||||
},
|
||||
"images": {
|
||||
"additional_backgrounds": ["background.gif"]
|
||||
}
|
||||
},
|
||||
|
||||
"theme_experiment": {
|
||||
"stylesheet": "experiment.css",
|
||||
"colors": {
|
||||
"button": "--button-bgcolor",
|
||||
"button_hover": "--button-hover-bgcolor",
|
||||
"button_active": "--button-active-bgcolor",
|
||||
"button_primary": "--button-primary-bgcolor",
|
||||
"button_primary_hover": "--button-primary-hover-bgcolor",
|
||||
"button_primary_active": "--button-primary-active-bgcolor",
|
||||
"button_primary_color": "--button-primary-color",
|
||||
"input_background": "--input-bgcolor",
|
||||
"input_color": "--input-color",
|
||||
"urlbar_popup_separator": "--urlbarView-separator-color",
|
||||
"zoom_controls": "--zoom-controls-bgcolor",
|
||||
"tab_icon_overlay_stroke": "--tab-icon-overlay-stroke",
|
||||
"tab_icon_overlay_fill": "--tab-icon-overlay-fill"
|
||||
},
|
||||
"properties": {
|
||||
"panel_active": "--arrowpanel-dimmed-further",
|
||||
"toolbar_field_icon_opacity": "--urlbar-icon-fill-opacity",
|
||||
"zap_gradient": "--panel-separator-zap-gradient"
|
||||
}
|
||||
}
|
||||
}
|
||||
157
additions/dom/mask/MaskConfig.hpp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
Helper to extract values from the Mask Config JSON file.
|
||||
Written by daijro.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <optional>
|
||||
#include <codecvt>
|
||||
#include "mozilla/glue/Debug.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace MaskConfig {
|
||||
|
||||
inline const nlohmann::json& GetJson() {
|
||||
static const nlohmann::json jsonConfig = []() {
|
||||
const char* jsonString = getenv("CAMOU_CONFIG");
|
||||
if (!jsonString) return nlohmann::json{};
|
||||
// Validate
|
||||
if (!nlohmann::json::accept(jsonString)) {
|
||||
printf_stderr("ERROR: Invalid JSON passed to CAMOU_CONFIG!\n");
|
||||
return nlohmann::json{};
|
||||
}
|
||||
nlohmann::json result = nlohmann::json::parse(jsonString);
|
||||
return result;
|
||||
}();
|
||||
return jsonConfig;
|
||||
}
|
||||
|
||||
inline bool HasKey(const std::string& key, nlohmann::json& data) {
|
||||
if (!data.contains(key)) {
|
||||
// printf_stderr("WARNING: Key not found: %s\n", key.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::optional<std::string> GetString(const std::string& key) {
|
||||
// printf_stderr("GetString: %s\n", key.c_str());
|
||||
auto data = GetJson();
|
||||
if (!HasKey(key, data)) return std::nullopt;
|
||||
return std::make_optional(data[key].get<std::string>());
|
||||
}
|
||||
|
||||
inline std::vector<std::string> GetStringList(const std::string& key) {
|
||||
std::vector<std::string> result;
|
||||
|
||||
auto data = GetJson();
|
||||
if (!HasKey(key, data)) return {};
|
||||
// Build vector
|
||||
for (const auto& item : data[key]) {
|
||||
result.push_back(item.get<std::string>());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::vector<std::string> GetStringListLower(const std::string& key) {
|
||||
std::vector<std::string> result = GetStringList(key);
|
||||
for (auto& str : result) {
|
||||
std::transform(str.begin(), str.end(), str.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline std::optional<T> GetUintImpl(const std::string& key) {
|
||||
auto data = GetJson();
|
||||
if (!HasKey(key, data)) return std::nullopt;
|
||||
if (data[key].is_number_unsigned())
|
||||
return std::make_optional(data[key].get<T>());
|
||||
printf_stderr("ERROR: Value for key '%s' is not an unsigned integer\n",
|
||||
key.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline std::optional<uint64_t> GetUint64(const std::string& key) {
|
||||
return GetUintImpl<uint64_t>(key);
|
||||
}
|
||||
|
||||
inline std::optional<uint32_t> GetUint32(const std::string& key) {
|
||||
return GetUintImpl<uint32_t>(key);
|
||||
}
|
||||
|
||||
inline std::optional<int32_t> GetInt32(const std::string& key) {
|
||||
auto data = GetJson();
|
||||
if (!HasKey(key, data)) return std::nullopt;
|
||||
if (data[key].is_number_integer())
|
||||
return std::make_optional(data[key].get<int32_t>());
|
||||
printf_stderr("ERROR: Value for key '%s' is not an integer\n", key.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline std::optional<double> GetDouble(const std::string& key) {
|
||||
auto data = GetJson();
|
||||
if (!HasKey(key, data)) return std::nullopt;
|
||||
if (data[key].is_number_float())
|
||||
return std::make_optional(data[key].get<double>());
|
||||
if (data[key].is_number_unsigned() || data[key].is_number_integer())
|
||||
return std::make_optional(static_cast<double>(data[key].get<int64_t>()));
|
||||
printf_stderr("ERROR: Value for key '%s' is not a double\n", key.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline std::optional<bool> GetBool(const std::string& key) {
|
||||
auto data = GetJson();
|
||||
if (!HasKey(key, data)) return std::nullopt;
|
||||
if (data[key].is_boolean()) return std::make_optional(data[key].get<bool>());
|
||||
printf_stderr("ERROR: Value for key '%s' is not a boolean\n", key.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline std::optional<std::array<uint32_t, 4>> GetRect(
|
||||
const std::string& top, const std::string& left, const std::string& height,
|
||||
const std::string& width) {
|
||||
// Make top and left default to 0
|
||||
std::array<std::optional<uint32_t>, 4> values = {
|
||||
GetUint32(top).value_or(0), GetUint32(left).value_or(0),
|
||||
GetUint32(height), GetUint32(width)};
|
||||
|
||||
// If height or width is std::nullopt, return std::nullopt
|
||||
if (!values[2].has_value() || !values[3].has_value()) {
|
||||
if (values[2].has_value() ^ values[3].has_value())
|
||||
printf_stderr(
|
||||
"Both %s and %s must be provided. Using default "
|
||||
"behavior.\n",
|
||||
height.c_str(), width.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Convert std::optional<uint32_t> to uint32_t
|
||||
std::array<uint32_t, 4> result;
|
||||
std::transform(values.begin(), values.end(), result.begin(),
|
||||
[](const auto& value) { return value.value(); });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::optional<std::array<int32_t, 4>> GetInt32Rect(
|
||||
const std::string& top, const std::string& left, const std::string& height,
|
||||
const std::string& width) {
|
||||
// Calls GetRect but casts to int32_t
|
||||
if (auto optValue = GetRect(top, left, height, width)) {
|
||||
std::array<int32_t, 4> result;
|
||||
std::transform(optValue->begin(), optValue->end(), result.begin(),
|
||||
[](const auto& val) { return static_cast<int32_t>(val); });
|
||||
return result;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace MaskConfig
|
||||
24765
additions/dom/mask/json.hpp
Normal file
19
additions/dom/mask/moz.build
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
|
||||
|
||||
EXPORTS += [
|
||||
"MaskConfig.hpp",
|
||||
]
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
"/dom/base",
|
||||
"/dom/mask",
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = "xul"
|
||||
239
additions/juggler/Helper.js
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
|
||||
class Helper {
|
||||
decorateAsEventEmitter(objectToDecorate) {
|
||||
const { EventEmitter } = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
|
||||
const emitter = new EventEmitter();
|
||||
objectToDecorate.on = emitter.on.bind(emitter);
|
||||
objectToDecorate.addEventListener = emitter.on.bind(emitter);
|
||||
objectToDecorate.off = emitter.off.bind(emitter);
|
||||
objectToDecorate.removeEventListener = emitter.off.bind(emitter);
|
||||
objectToDecorate.once = emitter.once.bind(emitter);
|
||||
objectToDecorate.emit = emitter.emit.bind(emitter);
|
||||
}
|
||||
|
||||
collectAllBrowsingContexts(rootBrowsingContext, allBrowsingContexts = []) {
|
||||
allBrowsingContexts.push(rootBrowsingContext);
|
||||
for (const child of rootBrowsingContext.children)
|
||||
this.collectAllBrowsingContexts(child, allBrowsingContexts);
|
||||
return allBrowsingContexts;
|
||||
}
|
||||
|
||||
awaitTopic(topic) {
|
||||
return new Promise(resolve => {
|
||||
const listener = () => {
|
||||
Services.obs.removeObserver(listener, topic);
|
||||
resolve();
|
||||
}
|
||||
Services.obs.addObserver(listener, topic);
|
||||
});
|
||||
}
|
||||
|
||||
toProtocolNavigationId(loadIdentifier) {
|
||||
return `nav-${loadIdentifier}`;
|
||||
}
|
||||
|
||||
addObserver(handler, topic) {
|
||||
Services.obs.addObserver(handler, topic);
|
||||
return () => Services.obs.removeObserver(handler, topic);
|
||||
}
|
||||
|
||||
addMessageListener(receiver, eventName, handler) {
|
||||
receiver.addMessageListener(eventName, handler);
|
||||
return () => receiver.removeMessageListener(eventName, handler);
|
||||
}
|
||||
|
||||
addEventListener(receiver, eventName, handler, options) {
|
||||
receiver.addEventListener(eventName, handler, options);
|
||||
return () => {
|
||||
try {
|
||||
receiver.removeEventListener(eventName, handler, options);
|
||||
} catch (e) {
|
||||
// This could fail when window has navigated cross-process
|
||||
// and we remove the listener from WindowProxy.
|
||||
// Nothing we can do here - so ignore the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
awaitEvent(receiver, eventName) {
|
||||
return new Promise(resolve => {
|
||||
receiver.addEventListener(eventName, function listener() {
|
||||
receiver.removeEventListener(eventName, listener);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
on(receiver, eventName, handler, options) {
|
||||
// The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument.
|
||||
// Fire event listeners without it for convenience.
|
||||
const handlerWrapper = (_, ...args) => handler(...args);
|
||||
receiver.on(eventName, handlerWrapper, options);
|
||||
return () => receiver.off(eventName, handlerWrapper);
|
||||
}
|
||||
|
||||
addProgressListener(progress, listener, flags) {
|
||||
progress.addProgressListener(listener, flags);
|
||||
return () => progress.removeProgressListener(listener);
|
||||
}
|
||||
|
||||
removeListeners(listeners) {
|
||||
for (const tearDown of listeners)
|
||||
tearDown.call(null);
|
||||
listeners.splice(0, listeners.length);
|
||||
}
|
||||
|
||||
generateId() {
|
||||
const string = uuidGen.generateUUID().toString();
|
||||
return string.substring(1, string.length - 1);
|
||||
}
|
||||
|
||||
getLoadContext(channel) {
|
||||
let loadContext = null;
|
||||
try {
|
||||
if (channel.notificationCallbacks)
|
||||
loadContext = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (!loadContext && channel.loadGroup)
|
||||
loadContext = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
||||
} catch (e) { }
|
||||
return loadContext;
|
||||
}
|
||||
|
||||
getNetworkErrorStatusText(status) {
|
||||
if (!status)
|
||||
return null;
|
||||
for (const key of Object.keys(Cr)) {
|
||||
if (Cr[key] === status)
|
||||
return key;
|
||||
}
|
||||
// Security module. The following is taken from
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL
|
||||
if ((status & 0xff0000) === 0x5a0000) {
|
||||
// NSS_SEC errors (happen below the base value because of negative vals)
|
||||
if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
|
||||
// The bases are actually negative, so in our positive numeric space, we
|
||||
// need to subtract the base off our value.
|
||||
const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
|
||||
switch (nssErr) {
|
||||
case 11:
|
||||
return 'SEC_ERROR_EXPIRED_CERTIFICATE';
|
||||
case 12:
|
||||
return 'SEC_ERROR_REVOKED_CERTIFICATE';
|
||||
case 13:
|
||||
return 'SEC_ERROR_UNKNOWN_ISSUER';
|
||||
case 20:
|
||||
return 'SEC_ERROR_UNTRUSTED_ISSUER';
|
||||
case 21:
|
||||
return 'SEC_ERROR_UNTRUSTED_CERT';
|
||||
case 36:
|
||||
return 'SEC_ERROR_CA_CERT_INVALID';
|
||||
case 90:
|
||||
return 'SEC_ERROR_INADEQUATE_KEY_USAGE';
|
||||
case 176:
|
||||
return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED';
|
||||
default:
|
||||
return 'SEC_ERROR_UNKNOWN';
|
||||
}
|
||||
}
|
||||
const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
|
||||
switch (sslErr) {
|
||||
case 3:
|
||||
return 'SSL_ERROR_NO_CERTIFICATE';
|
||||
case 4:
|
||||
return 'SSL_ERROR_BAD_CERTIFICATE';
|
||||
case 8:
|
||||
return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE';
|
||||
case 9:
|
||||
return 'SSL_ERROR_UNSUPPORTED_VERSION';
|
||||
case 12:
|
||||
return 'SSL_ERROR_BAD_CERT_DOMAIN';
|
||||
default:
|
||||
return 'SSL_ERROR_UNKNOWN';
|
||||
}
|
||||
}
|
||||
return '<unknown error>';
|
||||
}
|
||||
|
||||
browsingContextToFrameId(browsingContext) {
|
||||
if (!browsingContext)
|
||||
return undefined;
|
||||
if (!browsingContext.parent)
|
||||
return 'mainframe-' + browsingContext.browserId;
|
||||
return 'subframe-' + browsingContext.id;
|
||||
}
|
||||
}
|
||||
|
||||
const helper = new Helper();
|
||||
|
||||
class EventWatcher {
|
||||
constructor(receiver, eventNames, pendingEventWatchers = new Set()) {
|
||||
this._pendingEventWatchers = pendingEventWatchers;
|
||||
this._pendingEventWatchers.add(this);
|
||||
|
||||
this._events = [];
|
||||
this._pendingPromises = [];
|
||||
this._eventListeners = eventNames.map(eventName =>
|
||||
helper.on(receiver, eventName, this._onEvent.bind(this, eventName)),
|
||||
);
|
||||
}
|
||||
|
||||
_onEvent(eventName, eventObject) {
|
||||
this._events.push({eventName, eventObject});
|
||||
for (const promise of this._pendingPromises)
|
||||
promise.resolve();
|
||||
this._pendingPromises = [];
|
||||
}
|
||||
|
||||
async ensureEvent(aEventName, predicate) {
|
||||
if (typeof aEventName !== 'string')
|
||||
throw new Error('ERROR: ensureEvent expects a "string" as its first argument');
|
||||
while (true) {
|
||||
const result = this.getEvent(aEventName, predicate);
|
||||
if (result)
|
||||
return result;
|
||||
await new Promise((resolve, reject) => this._pendingPromises.push({resolve, reject}));
|
||||
}
|
||||
}
|
||||
|
||||
async ensureEvents(eventNames, predicate) {
|
||||
if (!Array.isArray(eventNames))
|
||||
throw new Error('ERROR: ensureEvents expects an array of event names as its first argument');
|
||||
return await Promise.all(eventNames.map(eventName => this.ensureEvent(eventName, predicate)));
|
||||
}
|
||||
|
||||
async ensureEventsAndDispose(eventNames, predicate) {
|
||||
if (!Array.isArray(eventNames))
|
||||
throw new Error('ERROR: ensureEventsAndDispose expects an array of event names as its first argument');
|
||||
const result = await this.ensureEvents(eventNames, predicate);
|
||||
this.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
getEvent(aEventName, predicate = (eventObject) => true) {
|
||||
return this._events.find(({eventName, eventObject}) => eventName === aEventName && predicate(eventObject))?.eventObject;
|
||||
}
|
||||
|
||||
hasEvent(aEventName, predicate) {
|
||||
return !!this.getEvent(aEventName, predicate);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._pendingEventWatchers.delete(this);
|
||||
for (const promise of this._pendingPromises)
|
||||
promise.reject(new Error('EventWatcher is being disposed'));
|
||||
this._pendingPromises = [];
|
||||
helper.removeListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = [ "Helper", "EventWatcher" ];
|
||||
this.Helper = Helper;
|
||||
this.EventWatcher = EventWatcher;
|
||||
|
||||
42
additions/juggler/JugglerFrameParent.jsm
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use strict";
|
||||
|
||||
const { TargetRegistry } = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
|
||||
const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
|
||||
const helper = new Helper();
|
||||
|
||||
var EXPORTED_SYMBOLS = ['JugglerFrameParent'];
|
||||
|
||||
class JugglerFrameParent extends JSWindowActorParent {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
receiveMessage() { }
|
||||
|
||||
async actorCreated() {
|
||||
// Actors are registered per the WindowGlobalParent / WindowGlobalChild pair. We are only
|
||||
// interested in those WindowGlobalParent actors that are matching current browsingContext
|
||||
// window global.
|
||||
// See https://github.com/mozilla/gecko-dev/blob/cd2121e7d83af1b421c95e8c923db70e692dab5f/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15
|
||||
if (!this.manager?.isCurrentGlobal)
|
||||
return;
|
||||
|
||||
// Only interested in main frames for now.
|
||||
if (this.browsingContext.parent)
|
||||
return;
|
||||
|
||||
this._target = TargetRegistry.instance()?.targetForBrowserId(this.browsingContext.browserId);
|
||||
if (!this._target)
|
||||
return;
|
||||
|
||||
this.actorName = `browser::page[${this._target.id()}]/${this.browsingContext.browserId}/${this.browsingContext.id}/${this._target.nextActorSequenceNumber()}`;
|
||||
this._target.setActor(this);
|
||||
}
|
||||
|
||||
didDestroy() {
|
||||
if (!this._target)
|
||||
return;
|
||||
this._target.removeActor(this);
|
||||
}
|
||||
}
|
||||
955
additions/juggler/NetworkObserver.js
Normal file
|
|
@ -0,0 +1,955 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||
const { ChannelEventSinkFactory } = ChromeUtils.import("chrome://remote/content/cdp/observers/ChannelEventSink.jsm");
|
||||
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
const Cm = Components.manager;
|
||||
const CC = Components.Constructor;
|
||||
const helper = new Helper();
|
||||
|
||||
const UINT32_MAX = Math.pow(2, 32)-1;
|
||||
|
||||
const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
|
||||
const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream');
|
||||
const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init');
|
||||
|
||||
// Cap response storage with 100Mb per tracked tab.
|
||||
const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024;
|
||||
|
||||
const pageNetworkSymbol = Symbol('PageNetwork');
|
||||
|
||||
class PageNetwork {
|
||||
static forPageTarget(target) {
|
||||
if (!target)
|
||||
return undefined;
|
||||
let result = target[pageNetworkSymbol];
|
||||
if (!result) {
|
||||
result = new PageNetwork(target);
|
||||
target[pageNetworkSymbol] = result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(target) {
|
||||
helper.decorateAsEventEmitter(this);
|
||||
this._target = target;
|
||||
this._extraHTTPHeaders = null;
|
||||
this._responseStorage = new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10);
|
||||
this._requestInterceptionEnabled = false;
|
||||
// This is requestId => NetworkRequest map, only contains requests that are
|
||||
// awaiting interception action (abort, resume, fulfill) over the protocol.
|
||||
this._interceptedRequests = new Map();
|
||||
}
|
||||
|
||||
setExtraHTTPHeaders(headers) {
|
||||
this._extraHTTPHeaders = headers;
|
||||
}
|
||||
|
||||
combinedExtraHTTPHeaders() {
|
||||
return [
|
||||
...(this._target.browserContext().extraHTTPHeaders || []),
|
||||
...(this._extraHTTPHeaders || []),
|
||||
];
|
||||
}
|
||||
|
||||
enableRequestInterception() {
|
||||
this._requestInterceptionEnabled = true;
|
||||
}
|
||||
|
||||
disableRequestInterception() {
|
||||
this._requestInterceptionEnabled = false;
|
||||
for (const intercepted of this._interceptedRequests.values())
|
||||
intercepted.resume();
|
||||
this._interceptedRequests.clear();
|
||||
}
|
||||
|
||||
resumeInterceptedRequest(requestId, url, method, headers, postData) {
|
||||
this._takeIntercepted(requestId).resume(url, method, headers, postData);
|
||||
}
|
||||
|
||||
fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) {
|
||||
this._takeIntercepted(requestId).fulfill(status, statusText, headers, base64body);
|
||||
}
|
||||
|
||||
abortInterceptedRequest(requestId, errorCode) {
|
||||
this._takeIntercepted(requestId).abort(errorCode);
|
||||
}
|
||||
|
||||
getResponseBody(requestId) {
|
||||
if (!this._responseStorage)
|
||||
throw new Error('Responses are not tracked for the given browser');
|
||||
return this._responseStorage.getBase64EncodedResponse(requestId);
|
||||
}
|
||||
|
||||
_takeIntercepted(requestId) {
|
||||
const intercepted = this._interceptedRequests.get(requestId);
|
||||
if (!intercepted)
|
||||
throw new Error(`Cannot find request "${requestId}"`);
|
||||
this._interceptedRequests.delete(requestId);
|
||||
return intercepted;
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkRequest {
|
||||
constructor(networkObserver, httpChannel, redirectedFrom) {
|
||||
this._networkObserver = networkObserver;
|
||||
this.httpChannel = httpChannel;
|
||||
|
||||
const loadInfo = this.httpChannel.loadInfo;
|
||||
const browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.workerAssociatedBrowsingContext || loadInfo?.browsingContext;
|
||||
|
||||
this._frameId = helper.browsingContextToFrameId(browsingContext);
|
||||
|
||||
this.requestId = httpChannel.channelId + '';
|
||||
this.navigationId = httpChannel.isMainDocumentChannel && loadInfo ? helper.toProtocolNavigationId(loadInfo.jugglerLoadIdentifier) : undefined;
|
||||
|
||||
this._redirectedIndex = 0;
|
||||
if (redirectedFrom) {
|
||||
this.redirectedFromId = redirectedFrom.requestId;
|
||||
this._redirectedIndex = redirectedFrom._redirectedIndex + 1;
|
||||
this.requestId = this.requestId + '-redirect' + this._redirectedIndex;
|
||||
this.navigationId = redirectedFrom.navigationId;
|
||||
// Finish previous request now. Since we inherit the listener, we could in theory
|
||||
// use onStopRequest, but that will only happen after the last redirect has finished.
|
||||
redirectedFrom._sendOnRequestFinished();
|
||||
}
|
||||
// In case of proxy auth, we get two requests with the same channel:
|
||||
// - one is pre-auth
|
||||
// - second is with auth header.
|
||||
//
|
||||
// In this case, we create this NetworkRequest object with a `redirectedFrom`
|
||||
// object, and they both share the same httpChannel.
|
||||
//
|
||||
// Since we want to maintain _channelToRequest map without clashes,
|
||||
// we must call `_sendOnRequestFinished` **before** we update it with a new object
|
||||
// here.
|
||||
if (this._networkObserver._channelToRequest.has(this.httpChannel))
|
||||
throw new Error(`Internal Error: invariant is broken for _channelToRequest map`);
|
||||
this._networkObserver._channelToRequest.set(this.httpChannel, this);
|
||||
|
||||
if (redirectedFrom) {
|
||||
this._pageNetwork = redirectedFrom._pageNetwork;
|
||||
} else if (browsingContext) {
|
||||
const target = this._networkObserver._targetRegistry.targetForBrowserId(browsingContext.browserId);
|
||||
this._pageNetwork = PageNetwork.forPageTarget(target);
|
||||
}
|
||||
this._expectingInterception = false;
|
||||
this._expectingResumedRequest = undefined; // { method, headers, postData }
|
||||
this._sentOnResponse = false;
|
||||
this._fulfilled = false;
|
||||
|
||||
if (this._pageNetwork)
|
||||
appendExtraHTTPHeaders(httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
|
||||
|
||||
this._responseBodyChunks = [];
|
||||
|
||||
httpChannel.QueryInterface(Ci.nsITraceableChannel);
|
||||
this._originalListener = httpChannel.setNewListener(this);
|
||||
if (redirectedFrom) {
|
||||
// Listener is inherited for regular redirects, so we'd like to avoid
|
||||
// calling into previous NetworkRequest.
|
||||
this._originalListener = redirectedFrom._originalListener;
|
||||
}
|
||||
|
||||
this._previousCallbacks = httpChannel.notificationCallbacks;
|
||||
httpChannel.notificationCallbacks = this;
|
||||
|
||||
this.QueryInterface = ChromeUtils.generateQI([
|
||||
Ci.nsIAuthPrompt2,
|
||||
Ci.nsIAuthPromptProvider,
|
||||
Ci.nsIInterfaceRequestor,
|
||||
Ci.nsINetworkInterceptController,
|
||||
Ci.nsIStreamListener,
|
||||
]);
|
||||
|
||||
if (this.redirectedFromId) {
|
||||
// Redirects are not interceptable.
|
||||
this._sendOnRequest(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Public interception API.
|
||||
resume(url, method, headers, postData) {
|
||||
this._expectingResumedRequest = { method, headers, postData };
|
||||
const newUri = url ? Services.io.newURI(url) : null;
|
||||
this._interceptedChannel.resetInterceptionWithURI(newUri);
|
||||
this._interceptedChannel = undefined;
|
||||
}
|
||||
|
||||
// Public interception API.
|
||||
abort(errorCode) {
|
||||
const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
|
||||
this._interceptedChannel.cancelInterception(error);
|
||||
this._interceptedChannel = undefined;
|
||||
}
|
||||
|
||||
// Public interception API.
|
||||
fulfill(status, statusText, headers, base64body) {
|
||||
this._fulfilled = true;
|
||||
this._interceptedChannel.synthesizeStatus(status, statusText);
|
||||
for (const header of headers) {
|
||||
this._interceptedChannel.synthesizeHeader(header.name, header.value);
|
||||
if (header.name.toLowerCase() === 'set-cookie') {
|
||||
Services.cookies.QueryInterface(Ci.nsICookieService);
|
||||
Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, header.value, this.httpChannel);
|
||||
}
|
||||
}
|
||||
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
||||
synthesized.data = base64body ? atob(base64body) : '';
|
||||
this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false);
|
||||
this._interceptedChannel.finishSynthesizedResponse();
|
||||
this._interceptedChannel = undefined;
|
||||
}
|
||||
|
||||
// Instrumentation called by NetworkObserver.
|
||||
_onInternalRedirect(newChannel) {
|
||||
// Intercepted requests produce "internal redirects" - this is both for our own
|
||||
// interception and service workers.
|
||||
// An internal redirect does not necessarily have the same channelId,
|
||||
// but inherits notificationCallbacks and the listener,
|
||||
// and should be used instead of an old channel.
|
||||
this._networkObserver._channelToRequest.delete(this.httpChannel);
|
||||
this.httpChannel = newChannel;
|
||||
this._networkObserver._channelToRequest.set(this.httpChannel, this);
|
||||
}
|
||||
|
||||
// Instrumentation called by NetworkObserver.
|
||||
_onInternalRedirectReady() {
|
||||
// Resumed request is first internally redirected to a new request,
|
||||
// and then the new request is ready to be updated.
|
||||
if (!this._expectingResumedRequest)
|
||||
return;
|
||||
const { method, headers, postData } = this._expectingResumedRequest;
|
||||
this._expectingResumedRequest = undefined;
|
||||
|
||||
if (headers) {
|
||||
for (const header of requestHeaders(this.httpChannel)) {
|
||||
// We cannot remove the "host" header.
|
||||
if (header.name.toLowerCase() === 'host')
|
||||
continue;
|
||||
this.httpChannel.setRequestHeader(header.name, '', false /* merge */);
|
||||
}
|
||||
for (const header of headers)
|
||||
this.httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
|
||||
} else if (this._pageNetwork) {
|
||||
appendExtraHTTPHeaders(this.httpChannel, this._pageNetwork.combinedExtraHTTPHeaders());
|
||||
}
|
||||
if (method)
|
||||
this.httpChannel.requestMethod = method;
|
||||
if (postData !== undefined)
|
||||
setPostData(this.httpChannel, postData, headers);
|
||||
}
|
||||
|
||||
// nsIInterfaceRequestor
|
||||
getInterface(iid) {
|
||||
if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsINetworkInterceptController))
|
||||
return this;
|
||||
if (iid.equals(Ci.nsIAuthPrompt)) // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead.
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
if (this._previousCallbacks)
|
||||
return this._previousCallbacks.getInterface(iid);
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
}
|
||||
|
||||
// nsIAuthPromptProvider
|
||||
getAuthPrompt(aPromptReason, iid) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// nsIAuthPrompt2
|
||||
asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) {
|
||||
let canceled = false;
|
||||
Promise.resolve().then(() => {
|
||||
if (canceled)
|
||||
return;
|
||||
const hasAuth = this.promptAuth(aChannel, level, authInfo);
|
||||
if (hasAuth)
|
||||
aCallback.onAuthAvailable(aContext, authInfo);
|
||||
else
|
||||
aCallback.onAuthCancelled(aContext, true);
|
||||
});
|
||||
return {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
|
||||
cancel: () => {
|
||||
aCallback.onAuthCancelled(aContext, false);
|
||||
canceled = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// nsIAuthPrompt2
|
||||
promptAuth(aChannel, level, authInfo) {
|
||||
if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED)
|
||||
return false;
|
||||
const pageNetwork = this._pageNetwork;
|
||||
if (!pageNetwork)
|
||||
return false;
|
||||
let credentials = null;
|
||||
if (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
|
||||
const proxy = this._networkObserver._targetRegistry.getProxyInfo(aChannel);
|
||||
credentials = proxy ? {username: proxy.username, password: proxy.password} : null;
|
||||
} else {
|
||||
credentials = pageNetwork._target.browserContext().httpCredentials;
|
||||
}
|
||||
if (!credentials)
|
||||
return false;
|
||||
const origin = aChannel.URI.scheme + '://' + aChannel.URI.hostPort;
|
||||
if (credentials.origin && origin.toLowerCase() !== credentials.origin.toLowerCase())
|
||||
return false;
|
||||
authInfo.username = credentials.username;
|
||||
authInfo.password = credentials.password;
|
||||
// This will produce a new request with respective auth header set.
|
||||
// It will have the same id as ours. We expect it to arrive as new request and
|
||||
// will treat it as our own redirect.
|
||||
this._networkObserver._expectRedirect(this.httpChannel.channelId + '', this);
|
||||
return true;
|
||||
}
|
||||
|
||||
// nsINetworkInterceptController
|
||||
shouldPrepareForIntercept(aURI, channel) {
|
||||
const interceptController = this._fallThroughInterceptController();
|
||||
if (interceptController && interceptController.shouldPrepareForIntercept(aURI, channel)) {
|
||||
// We assume that interceptController is a service worker if there is one,
|
||||
// and yield interception to it. We are not going to intercept ourselves,
|
||||
// so we send onRequest now.
|
||||
this._sendOnRequest(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (channel !== this.httpChannel) {
|
||||
// Not our channel? Just in case this happens, don't do anything.
|
||||
return false;
|
||||
}
|
||||
|
||||
// We do not want to intercept any redirects, because we are not able
|
||||
// to intercept subresource redirects, and it's unreliable for main requests.
|
||||
// We do not sendOnRequest here, because redirects do that in constructor.
|
||||
if (this.redirectedFromId)
|
||||
return false;
|
||||
|
||||
const shouldIntercept = this._shouldIntercept();
|
||||
if (!shouldIntercept) {
|
||||
// We are not intercepting - ready to issue onRequest.
|
||||
this._sendOnRequest(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
this._expectingInterception = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// nsINetworkInterceptController
|
||||
channelIntercepted(intercepted) {
|
||||
if (!this._expectingInterception) {
|
||||
// We are not intercepting, fall-through.
|
||||
const interceptController = this._fallThroughInterceptController();
|
||||
if (interceptController)
|
||||
interceptController.channelIntercepted(intercepted);
|
||||
return;
|
||||
}
|
||||
|
||||
this._expectingInterception = false;
|
||||
this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
|
||||
|
||||
const pageNetwork = this._pageNetwork;
|
||||
if (!pageNetwork) {
|
||||
// Just in case we disabled instrumentation while intercepting, resume and forget.
|
||||
this.resume();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ok, so now we have intercepted the request, let's issue onRequest.
|
||||
// If interception has been disabled while we were intercepting, resume and forget.
|
||||
const interceptionEnabled = this._shouldIntercept();
|
||||
this._sendOnRequest(!!interceptionEnabled);
|
||||
if (interceptionEnabled)
|
||||
pageNetwork._interceptedRequests.set(this.requestId, this);
|
||||
else
|
||||
this.resume();
|
||||
}
|
||||
|
||||
// nsIStreamListener
|
||||
onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
|
||||
// Turns out webcompat shims might redirect to
|
||||
// SimpleChannel, so we get requests from a different channel.
|
||||
// See https://github.com/microsoft/playwright/issues/9418#issuecomment-944836244
|
||||
if (aRequest !== this.httpChannel)
|
||||
return;
|
||||
// For requests with internal redirect (e.g. intercepted by Service Worker),
|
||||
// we do not get onResponse normally, but we do get nsIStreamListener notifications.
|
||||
this._sendOnResponse(false);
|
||||
|
||||
const iStream = new BinaryInputStream(aInputStream);
|
||||
const sStream = new StorageStream(8192, aCount, null);
|
||||
const oStream = new BinaryOutputStream(sStream.getOutputStream(0));
|
||||
|
||||
// Copy received data as they come.
|
||||
const data = iStream.readBytes(aCount);
|
||||
this._responseBodyChunks.push(data);
|
||||
|
||||
oStream.writeBytes(data, aCount);
|
||||
try {
|
||||
this._originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount);
|
||||
} catch (e) {
|
||||
// Be ready to original listener exceptions.
|
||||
}
|
||||
}
|
||||
|
||||
// nsIStreamListener
|
||||
onStartRequest(aRequest) {
|
||||
// Turns out webcompat shims might redirect to
|
||||
// SimpleChannel, so we get requests from a different channel.
|
||||
// See https://github.com/microsoft/playwright/issues/9418#issuecomment-944836244
|
||||
if (aRequest !== this.httpChannel)
|
||||
return;
|
||||
try {
|
||||
this._originalListener.onStartRequest(aRequest);
|
||||
} catch (e) {
|
||||
// Be ready to original listener exceptions.
|
||||
}
|
||||
}
|
||||
|
||||
// nsIStreamListener
|
||||
onStopRequest(aRequest, aStatusCode) {
|
||||
// Turns out webcompat shims might redirect to
|
||||
// SimpleChannel, so we get requests from a different channel.
|
||||
// See https://github.com/microsoft/playwright/issues/9418#issuecomment-944836244
|
||||
if (aRequest !== this.httpChannel)
|
||||
return;
|
||||
try {
|
||||
this._originalListener.onStopRequest(aRequest, aStatusCode);
|
||||
} catch (e) {
|
||||
// Be ready to original listener exceptions.
|
||||
}
|
||||
|
||||
if (aStatusCode === 0) {
|
||||
// For requests with internal redirect (e.g. intercepted by Service Worker),
|
||||
// we do not get onResponse normally, but we do get nsIRequestObserver notifications.
|
||||
this._sendOnResponse(false);
|
||||
const body = this._responseBodyChunks.join('');
|
||||
const pageNetwork = this._pageNetwork;
|
||||
if (pageNetwork)
|
||||
pageNetwork._responseStorage.addResponseBody(this, body);
|
||||
this._sendOnRequestFinished();
|
||||
} else {
|
||||
this._sendOnRequestFailed(aStatusCode);
|
||||
}
|
||||
|
||||
delete this._responseBodyChunks;
|
||||
}
|
||||
|
||||
_shouldIntercept() {
|
||||
const pageNetwork = this._pageNetwork;
|
||||
if (!pageNetwork)
|
||||
return false;
|
||||
if (pageNetwork._requestInterceptionEnabled)
|
||||
return true;
|
||||
const browserContext = pageNetwork._target.browserContext();
|
||||
if (browserContext.requestInterceptionEnabled)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
_fallThroughInterceptController() {
|
||||
try {
|
||||
return this._previousCallbacks?.getInterface(Ci.nsINetworkInterceptController);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
_sendOnRequest(isIntercepted) {
|
||||
// Note: we call _sendOnRequest either after we intercepted the request,
|
||||
// or at the first moment we know that we are not going to intercept.
|
||||
const pageNetwork = this._pageNetwork;
|
||||
if (!pageNetwork)
|
||||
return;
|
||||
const loadInfo = this.httpChannel.loadInfo;
|
||||
const causeType = loadInfo?.externalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER;
|
||||
const internalCauseType = loadInfo?.internalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER;
|
||||
pageNetwork.emit(PageNetwork.Events.Request, {
|
||||
url: this.httpChannel.URI.spec,
|
||||
frameId: this._frameId,
|
||||
isIntercepted,
|
||||
requestId: this.requestId,
|
||||
redirectedFrom: this.redirectedFromId,
|
||||
postData: readRequestPostData(this.httpChannel),
|
||||
headers: requestHeaders(this.httpChannel),
|
||||
method: this.httpChannel.requestMethod,
|
||||
navigationId: this.navigationId,
|
||||
cause: causeTypeToString(causeType),
|
||||
internalCause: causeTypeToString(internalCauseType),
|
||||
}, this._frameId);
|
||||
}
|
||||
|
||||
_sendOnResponse(fromCache, opt_statusCode, opt_statusText) {
|
||||
if (this._sentOnResponse) {
|
||||
// We can come here twice because of internal redirects, e.g. service workers.
|
||||
return;
|
||||
}
|
||||
this._sentOnResponse = true;
|
||||
const pageNetwork = this._pageNetwork;
|
||||
if (!pageNetwork)
|
||||
return;
|
||||
|
||||
this.httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
|
||||
this.httpChannel.QueryInterface(Ci.nsITimedChannel);
|
||||
const timing = {
|
||||
startTime: this.httpChannel.channelCreationTime,
|
||||
domainLookupStart: this.httpChannel.domainLookupStartTime,
|
||||
domainLookupEnd: this.httpChannel.domainLookupEndTime,
|
||||
connectStart: this.httpChannel.connectStartTime,
|
||||
secureConnectionStart: this.httpChannel.secureConnectionStartTime,
|
||||
connectEnd: this.httpChannel.connectEndTime,
|
||||
requestStart: this.httpChannel.requestStartTime,
|
||||
responseStart: this.httpChannel.responseStartTime,
|
||||
};
|
||||
|
||||
const { status, statusText, headers } = responseHead(this.httpChannel, opt_statusCode, opt_statusText);
|
||||
let remoteIPAddress = undefined;
|
||||
let remotePort = undefined;
|
||||
try {
|
||||
remoteIPAddress = this.httpChannel.remoteAddress;
|
||||
remotePort = this.httpChannel.remotePort;
|
||||
} catch (e) {
|
||||
// remoteAddress is not defined for cached requests.
|
||||
}
|
||||
|
||||
const fromServiceWorker = this._networkObserver._channelIdsFulfilledByServiceWorker.has(this.requestId);
|
||||
this._networkObserver._channelIdsFulfilledByServiceWorker.delete(this.requestId);
|
||||
|
||||
pageNetwork.emit(PageNetwork.Events.Response, {
|
||||
requestId: this.requestId,
|
||||
securityDetails: getSecurityDetails(this.httpChannel),
|
||||
fromCache,
|
||||
headers,
|
||||
remoteIPAddress,
|
||||
remotePort,
|
||||
status,
|
||||
statusText,
|
||||
timing,
|
||||
fromServiceWorker,
|
||||
}, this._frameId);
|
||||
}
|
||||
|
||||
_sendOnRequestFailed(error) {
|
||||
const pageNetwork = this._pageNetwork;
|
||||
if (pageNetwork) {
|
||||
pageNetwork.emit(PageNetwork.Events.RequestFailed, {
|
||||
requestId: this.requestId,
|
||||
errorCode: helper.getNetworkErrorStatusText(error),
|
||||
}, this._frameId);
|
||||
}
|
||||
this._networkObserver._channelToRequest.delete(this.httpChannel);
|
||||
}
|
||||
|
||||
_sendOnRequestFinished() {
|
||||
const pageNetwork = this._pageNetwork;
|
||||
// Undefined |responseEndTime| means there has been no response yet.
|
||||
// This happens when request interception API is used to redirect
|
||||
// the request to a different URL.
|
||||
// In this case, we should not emit "requestFinished" event.
|
||||
if (pageNetwork && this.httpChannel.responseEndTime !== undefined) {
|
||||
let protocolVersion = undefined;
|
||||
try {
|
||||
protocolVersion = this.httpChannel.protocolVersion;
|
||||
} catch (e) {
|
||||
// protocolVersion is unavailable in certain cases.
|
||||
};
|
||||
pageNetwork.emit(PageNetwork.Events.RequestFinished, {
|
||||
requestId: this.requestId,
|
||||
responseEndTime: this.httpChannel.responseEndTime,
|
||||
transferSize: this.httpChannel.transferSize,
|
||||
encodedBodySize: this.httpChannel.encodedBodySize,
|
||||
protocolVersion,
|
||||
}, this._frameId);
|
||||
}
|
||||
this._networkObserver._channelToRequest.delete(this.httpChannel);
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkObserver {
|
||||
static instance() {
|
||||
return NetworkObserver._instance || null;
|
||||
}
|
||||
|
||||
constructor(targetRegistry) {
|
||||
helper.decorateAsEventEmitter(this);
|
||||
NetworkObserver._instance = this;
|
||||
|
||||
this._targetRegistry = targetRegistry;
|
||||
|
||||
this._channelToRequest = new Map(); // http channel -> network request
|
||||
this._expectedRedirect = new Map(); // expected redirect channel id (string) -> network request
|
||||
this._channelIdsFulfilledByServiceWorker = new Set(); // http channel ids that were fulfilled by service worker
|
||||
|
||||
const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService();
|
||||
this._channelProxyFilter = {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolProxyChannelFilter]),
|
||||
applyFilter: (channel, defaultProxyInfo, proxyFilter) => {
|
||||
const proxy = this._targetRegistry.getProxyInfo(channel);
|
||||
if (!proxy) {
|
||||
proxyFilter.onProxyFilterResult(defaultProxyInfo);
|
||||
return;
|
||||
}
|
||||
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
|
||||
proxy.type,
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
'', /* aProxyAuthorizationHeader */
|
||||
'', /* aConnectionIsolationKey */
|
||||
Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST, /* aFlags */
|
||||
UINT32_MAX, /* aFailoverTimeout */
|
||||
null, /* failover proxy */
|
||||
));
|
||||
},
|
||||
};
|
||||
protocolProxyService.registerChannelFilter(this._channelProxyFilter, 0 /* position */);
|
||||
|
||||
// Register self as ChannelEventSink to track redirects.
|
||||
ChannelEventSinkFactory.getService().registerCollector({
|
||||
_onChannelRedirect: this._onRedirect.bind(this),
|
||||
});
|
||||
|
||||
this._eventListeners = [
|
||||
helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'),
|
||||
helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'),
|
||||
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'),
|
||||
helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'),
|
||||
helper.addObserver(this._onServiceWorkerResponse.bind(this), 'service-worker-synthesized-response'),
|
||||
];
|
||||
}
|
||||
|
||||
_expectRedirect(channelId, previous) {
|
||||
this._expectedRedirect.set(channelId, previous);
|
||||
}
|
||||
|
||||
_onRedirect(oldChannel, newChannel, flags) {
|
||||
if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel))
|
||||
return;
|
||||
const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
|
||||
const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel);
|
||||
const request = this._channelToRequest.get(oldHttpChannel);
|
||||
if (flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL) {
|
||||
if (request)
|
||||
request._onInternalRedirect(newHttpChannel);
|
||||
} else if (flags & Ci.nsIChannelEventSink.REDIRECT_STS_UPGRADE) {
|
||||
if (request) {
|
||||
// This is an internal HSTS upgrade. The original http request is canceled, and a new
|
||||
// equivalent https request is sent. We forge 307 redirect to follow Chromium here:
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:net/url_request/url_request_http_job.cc;l=211
|
||||
request._sendOnResponse(false, 307, 'Temporary Redirect');
|
||||
this._expectRedirect(newHttpChannel.channelId + '', request);
|
||||
}
|
||||
} else {
|
||||
if (request)
|
||||
this._expectRedirect(newHttpChannel.channelId + '', request);
|
||||
}
|
||||
}
|
||||
|
||||
_onRequest(channel, topic) {
|
||||
if (!(channel instanceof Ci.nsIHttpChannel))
|
||||
return;
|
||||
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
||||
const channelId = httpChannel.channelId + '';
|
||||
const redirectedFrom = this._expectedRedirect.get(channelId);
|
||||
if (redirectedFrom) {
|
||||
this._expectedRedirect.delete(channelId);
|
||||
new NetworkRequest(this, httpChannel, redirectedFrom);
|
||||
} else {
|
||||
const redirectedRequest = this._channelToRequest.get(httpChannel);
|
||||
if (redirectedRequest)
|
||||
redirectedRequest._onInternalRedirectReady();
|
||||
else
|
||||
new NetworkRequest(this, httpChannel);
|
||||
}
|
||||
}
|
||||
|
||||
_onResponse(fromCache, httpChannel, topic) {
|
||||
const request = this._channelToRequest.get(httpChannel);
|
||||
if (request)
|
||||
request._sendOnResponse(fromCache);
|
||||
}
|
||||
|
||||
_onServiceWorkerResponse(channel, topic) {
|
||||
if (!(channel instanceof Ci.nsIHttpChannel))
|
||||
return;
|
||||
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
||||
const channelId = httpChannel.channelId + '';
|
||||
this._channelIdsFulfilledByServiceWorker.add(channelId);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._activityDistributor.removeObserver(this);
|
||||
ChannelEventSinkFactory.unregister();
|
||||
helper.removeListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
const protocolVersionNames = {
|
||||
[Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1',
|
||||
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1',
|
||||
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2',
|
||||
[Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3',
|
||||
};
|
||||
|
||||
function getSecurityDetails(httpChannel) {
|
||||
const securityInfo = httpChannel.securityInfo;
|
||||
if (!securityInfo)
|
||||
return null;
|
||||
securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
|
||||
if (!securityInfo.serverCert)
|
||||
return null;
|
||||
return {
|
||||
protocol: protocolVersionNames[securityInfo.protocolVersion] || '<unknown>',
|
||||
subjectName: securityInfo.serverCert.commonName,
|
||||
issuer: securityInfo.serverCert.issuerCommonName,
|
||||
// Convert to seconds.
|
||||
validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000,
|
||||
validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
function readRequestPostData(httpChannel) {
|
||||
if (!(httpChannel instanceof Ci.nsIUploadChannel))
|
||||
return undefined;
|
||||
let iStream = httpChannel.uploadStream;
|
||||
if (!iStream)
|
||||
return undefined;
|
||||
const isSeekableStream = iStream instanceof Ci.nsISeekableStream;
|
||||
const isTellableStream = iStream instanceof Ci.nsITellableStream;
|
||||
|
||||
// For some reason, we cannot rewind back big streams,
|
||||
// so instead we should clone them.
|
||||
const isCloneable = iStream instanceof Ci.nsICloneableInputStream;
|
||||
if (isCloneable)
|
||||
iStream = iStream.clone();
|
||||
|
||||
let prevOffset;
|
||||
// Surprisingly, stream might implement `nsITellableStream` without
|
||||
// implementing the `tell` method.
|
||||
if (isSeekableStream && isTellableStream && iStream.tell) {
|
||||
prevOffset = iStream.tell();
|
||||
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
||||
}
|
||||
|
||||
// Read data from the stream.
|
||||
let result = undefined;
|
||||
try {
|
||||
const maxLen = iStream.available();
|
||||
// Cap at 10Mb.
|
||||
if (maxLen <= 10 * 1024 * 1024) {
|
||||
const buffer = NetUtil.readInputStreamToString(iStream, maxLen);
|
||||
result = btoa(buffer);
|
||||
}
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
// Seek locks the file, so seek to the beginning only if necko hasn't
|
||||
// read it yet, since necko doesn't seek to 0 before reading (at lest
|
||||
// not till 459384 is fixed).
|
||||
if (isSeekableStream && prevOffset == 0 && !isCloneable)
|
||||
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function requestHeaders(httpChannel) {
|
||||
const headers = [];
|
||||
httpChannel.visitRequestHeaders({
|
||||
visitHeader: (name, value) => headers.push({name, value}),
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
function causeTypeToString(causeType) {
|
||||
for (let key in Ci.nsIContentPolicy) {
|
||||
if (Ci.nsIContentPolicy[key] === causeType)
|
||||
return key;
|
||||
}
|
||||
return 'TYPE_OTHER';
|
||||
}
|
||||
|
||||
function appendExtraHTTPHeaders(httpChannel, headers) {
|
||||
if (!headers)
|
||||
return;
|
||||
for (const header of headers)
|
||||
httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
|
||||
}
|
||||
|
||||
class ResponseStorage {
|
||||
constructor(maxTotalSize, maxResponseSize) {
|
||||
this._totalSize = 0;
|
||||
this._maxResponseSize = maxResponseSize;
|
||||
this._maxTotalSize = maxTotalSize;
|
||||
this._responses = new Map();
|
||||
}
|
||||
|
||||
addResponseBody(request, body) {
|
||||
if (body.length > this._maxResponseSize) {
|
||||
this._responses.set(request.requestId, {
|
||||
evicted: true,
|
||||
body: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
let encodings = [];
|
||||
// Note: fulfilled request comes with decoded body right away.
|
||||
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion && !request._fulfilled) {
|
||||
const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding");
|
||||
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
|
||||
}
|
||||
this._responses.set(request.requestId, {body, encodings});
|
||||
this._totalSize += body.length;
|
||||
if (this._totalSize > this._maxTotalSize) {
|
||||
for (let [requestId, response] of this._responses) {
|
||||
this._totalSize -= response.body.length;
|
||||
response.body = '';
|
||||
response.evicted = true;
|
||||
if (this._totalSize < this._maxTotalSize)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBase64EncodedResponse(requestId) {
|
||||
const response = this._responses.get(requestId);
|
||||
if (!response)
|
||||
throw new Error(`Request "${requestId}" is not found`);
|
||||
if (response.evicted)
|
||||
return {base64body: '', evicted: true};
|
||||
let result = response.body;
|
||||
if (response.encodings && response.encodings.length) {
|
||||
for (const encoding of response.encodings)
|
||||
result = convertString(result, encoding, 'uncompressed');
|
||||
}
|
||||
return {base64body: btoa(result)};
|
||||
}
|
||||
}
|
||||
|
||||
function responseHead(httpChannel, opt_statusCode, opt_statusText) {
|
||||
const headers = [];
|
||||
let status = opt_statusCode || 0;
|
||||
let statusText = opt_statusText || '';
|
||||
try {
|
||||
status = httpChannel.responseStatus;
|
||||
statusText = httpChannel.responseStatusText;
|
||||
httpChannel.visitResponseHeaders({
|
||||
visitHeader: (name, value) => headers.push({name, value}),
|
||||
});
|
||||
} catch (e) {
|
||||
// Response headers, status and/or statusText are not available
|
||||
// when redirect did not actually hit the network.
|
||||
}
|
||||
return { status, statusText, headers };
|
||||
}
|
||||
|
||||
function setPostData(httpChannel, postData, headers) {
|
||||
if (!(httpChannel instanceof Ci.nsIUploadChannel2))
|
||||
return;
|
||||
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
||||
const body = atob(postData);
|
||||
synthesized.setData(body, body.length);
|
||||
|
||||
const overriddenHeader = (lowerCaseName) => {
|
||||
if (headers) {
|
||||
for (const header of headers) {
|
||||
if (header.name.toLowerCase() === lowerCaseName) {
|
||||
return header.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
// Clear content-length, so that upload stream resets it.
|
||||
httpChannel.setRequestHeader('content-length', '', false /* merge */);
|
||||
let contentType = overriddenHeader('content-type');
|
||||
if (contentType === undefined) {
|
||||
try {
|
||||
contentType = httpChannel.getRequestHeader('content-type');
|
||||
} catch (e) {
|
||||
if (e.result == Cr.NS_ERROR_NOT_AVAILABLE)
|
||||
contentType = 'application/octet-stream';
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
httpChannel.explicitSetUploadStream(synthesized, contentType, -1, httpChannel.requestMethod, false);
|
||||
}
|
||||
|
||||
function convertString(s, source, dest) {
|
||||
const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
|
||||
Ci.nsIStringInputStream
|
||||
);
|
||||
is.setData(s, s.length);
|
||||
const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
|
||||
Ci.nsIStreamLoader
|
||||
);
|
||||
let result = [];
|
||||
listener.init({
|
||||
onStreamComplete: function onStreamComplete(
|
||||
loader,
|
||||
context,
|
||||
status,
|
||||
length,
|
||||
data
|
||||
) {
|
||||
const array = Array.from(data);
|
||||
const kChunk = 100000;
|
||||
for (let i = 0; i < length; i += kChunk) {
|
||||
const len = Math.min(kChunk, length - i);
|
||||
const chunk = String.fromCharCode.apply(this, array.slice(i, i + len));
|
||||
result.push(chunk);
|
||||
}
|
||||
},
|
||||
});
|
||||
const converter = Cc["@mozilla.org/streamConverters;1"].getService(
|
||||
Ci.nsIStreamConverterService
|
||||
).asyncConvertData(
|
||||
source,
|
||||
dest,
|
||||
listener,
|
||||
null
|
||||
);
|
||||
converter.onStartRequest(null, null);
|
||||
converter.onDataAvailable(null, is, 0, s.length);
|
||||
converter.onStopRequest(null, null, null);
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
const errorMap = {
|
||||
'aborted': Cr.NS_ERROR_ABORT,
|
||||
'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED,
|
||||
'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST,
|
||||
'blockedbyclient': Cr.NS_ERROR_FAILURE,
|
||||
'blockedbyresponse': Cr.NS_ERROR_FAILURE,
|
||||
'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT,
|
||||
'connectionclosed': Cr.NS_ERROR_FAILURE,
|
||||
'connectionfailed': Cr.NS_ERROR_FAILURE,
|
||||
'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED,
|
||||
'connectionreset': Cr.NS_ERROR_NET_RESET,
|
||||
'internetdisconnected': Cr.NS_ERROR_OFFLINE,
|
||||
'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST,
|
||||
'timedout': Cr.NS_ERROR_NET_TIMEOUT,
|
||||
'failed': Cr.NS_ERROR_FAILURE,
|
||||
};
|
||||
|
||||
PageNetwork.Events = {
|
||||
Request: Symbol('PageNetwork.Events.Request'),
|
||||
Response: Symbol('PageNetwork.Events.Response'),
|
||||
RequestFinished: Symbol('PageNetwork.Events.RequestFinished'),
|
||||
RequestFailed: Symbol('PageNetwork.Events.RequestFailed'),
|
||||
};
|
||||
|
||||
var EXPORTED_SYMBOLS = ['NetworkObserver', 'PageNetwork'];
|
||||
this.NetworkObserver = NetworkObserver;
|
||||
this.PageNetwork = PageNetwork;
|
||||
256
additions/juggler/SimpleChannel.js
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
// Note: this file should be loadabale with eval() into worker environment.
|
||||
// Avoid Components.*, ChromeUtils and global const variables.
|
||||
|
||||
const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
|
||||
|
||||
class SimpleChannel {
|
||||
constructor(name, uid) {
|
||||
this._name = name;
|
||||
this._messageId = 0;
|
||||
this._connectorId = 0;
|
||||
this._pendingMessages = new Map();
|
||||
this._handlers = new Map();
|
||||
this._bufferedIncomingMessages = [];
|
||||
this.transport = {
|
||||
sendMessage: null,
|
||||
dispose: () => {},
|
||||
};
|
||||
this._ready = false;
|
||||
this._paused = false;
|
||||
this._disposed = false;
|
||||
|
||||
this._bufferedResponses = new Map();
|
||||
// This is a "unique" identifier of this end of the channel. Two SimpleChannel instances
|
||||
// on the same end of the channel (e.g. two content processes) must not have the same id.
|
||||
// This way, the other end can distinguish between the old peer with a new transport and a new peer.
|
||||
this._uid = uid;
|
||||
this._connectedToUID = undefined;
|
||||
}
|
||||
|
||||
bindToActor(actor) {
|
||||
this.resetTransport();
|
||||
this._name = actor.actorName;
|
||||
const oldReceiveMessage = actor.receiveMessage;
|
||||
actor.receiveMessage = message => this._onMessage(message.data);
|
||||
this.setTransport({
|
||||
sendMessage: obj => actor.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj),
|
||||
dispose: () => actor.receiveMessage = oldReceiveMessage,
|
||||
});
|
||||
}
|
||||
|
||||
resetTransport() {
|
||||
this.transport.dispose();
|
||||
this.transport = {
|
||||
sendMessage: null,
|
||||
dispose: () => {},
|
||||
};
|
||||
this._ready = false;
|
||||
}
|
||||
|
||||
setTransport(transport) {
|
||||
this.transport = transport;
|
||||
// connection handshake:
|
||||
// 1. There are two channel ends in different processes.
|
||||
// 2. Both ends start in the `ready = false` state, meaning that they will
|
||||
// not send any messages over transport.
|
||||
// 3. Once channel end is created, it sends { ack: `READY` } message to the other end.
|
||||
// 4. Eventually, at least one of the ends receives { ack: `READY` } message and responds with
|
||||
// { ack: `READY_ACK` }. We assume at least one of the ends will receive { ack: "READY" } event from the other, since
|
||||
// channel ends have a "parent-child" relation, i.e. one end is always created before the other one.
|
||||
// 5. Once channel end receives either { ack: `READY` } or { ack: `READY_ACK` }, it transitions to `ready` state.
|
||||
this.transport.sendMessage({ ack: 'READY', uid: this._uid });
|
||||
}
|
||||
|
||||
pause() {
|
||||
this._paused = true;
|
||||
}
|
||||
|
||||
resumeSoon() {
|
||||
if (!this._paused)
|
||||
return;
|
||||
this._paused = false;
|
||||
this._setTimeout(() => this._deliverBufferedIncomingMessages(), 0);
|
||||
}
|
||||
|
||||
_setTimeout(cb, timeout) {
|
||||
// Lazy load on first call.
|
||||
this._setTimeout = ChromeUtils.import('resource://gre/modules/Timer.jsm').setTimeout;
|
||||
this._setTimeout(cb, timeout);
|
||||
}
|
||||
|
||||
_markAsReady() {
|
||||
if (this._ready)
|
||||
return;
|
||||
this._ready = true;
|
||||
for (const { message } of this._pendingMessages.values())
|
||||
this.transport.sendMessage(message);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._disposed)
|
||||
return;
|
||||
this._disposed = true;
|
||||
for (const {resolve, reject, methodName} of this._pendingMessages.values())
|
||||
reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`));
|
||||
this._pendingMessages.clear();
|
||||
this._handlers.clear();
|
||||
this.transport.dispose();
|
||||
}
|
||||
|
||||
_rejectCallbacksFromConnector(connectorId) {
|
||||
for (const [messageId, callback] of this._pendingMessages) {
|
||||
if (callback.connectorId === connectorId) {
|
||||
callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`));
|
||||
this._pendingMessages.delete(messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect(namespace) {
|
||||
const connectorId = ++this._connectorId;
|
||||
return {
|
||||
send: (...args) => this._send(namespace, connectorId, ...args),
|
||||
emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}),
|
||||
dispose: () => this._rejectCallbacksFromConnector(connectorId),
|
||||
};
|
||||
}
|
||||
|
||||
register(namespace, handler) {
|
||||
if (this._handlers.has(namespace))
|
||||
throw new Error('ERROR: double-register for namespace ' + namespace);
|
||||
this._handlers.set(namespace, handler);
|
||||
this._deliverBufferedIncomingMessages();
|
||||
return () => this.unregister(namespace);
|
||||
}
|
||||
|
||||
_deliverBufferedIncomingMessages() {
|
||||
const bufferedRequests = this._bufferedIncomingMessages;
|
||||
this._bufferedIncomingMessages = [];
|
||||
for (const data of bufferedRequests) {
|
||||
this._onMessage(data);
|
||||
}
|
||||
}
|
||||
|
||||
unregister(namespace) {
|
||||
this._handlers.delete(namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} namespace
|
||||
* @param {number} connectorId
|
||||
* @param {string} methodName
|
||||
* @param {...*} params
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
async _send(namespace, connectorId, methodName, ...params) {
|
||||
if (this._disposed)
|
||||
throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`);
|
||||
const id = ++this._messageId;
|
||||
const message = {requestId: id, methodName, params, namespace};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace, message});
|
||||
});
|
||||
if (this._ready)
|
||||
this.transport.sendMessage(message);
|
||||
return promise;
|
||||
}
|
||||
|
||||
_onMessage(data) {
|
||||
if (data?.ack === 'READY') {
|
||||
// The "READY" and "READY_ACK" messages are a part of initialization sequence.
|
||||
// This sequence happens when:
|
||||
// 1. A new SimpleChannel instance is getting initialized on the other end.
|
||||
// In this case, it will have a different UID and we must clear
|
||||
// `this._bufferedResponses` since they are no longer relevant.
|
||||
// 2. A new transport is assigned to communicate between 2 SimpleChannel instances.
|
||||
// In this case, we MUST NOT clear `this._bufferedResponses` since they are used
|
||||
// to address the double-dispatch issue.
|
||||
if (this._connectedToUID !== data.uid)
|
||||
this._bufferedResponses.clear();
|
||||
this._connectedToUID = data.uid;
|
||||
this.transport.sendMessage({ ack: 'READY_ACK', uid: this._uid });
|
||||
this._markAsReady();
|
||||
return;
|
||||
}
|
||||
if (data?.ack === 'READY_ACK') {
|
||||
if (this._connectedToUID !== data.uid)
|
||||
this._bufferedResponses.clear();
|
||||
this._connectedToUID = data.uid;
|
||||
this._markAsReady();
|
||||
return;
|
||||
}
|
||||
if (data?.ack === 'RESPONSE_ACK') {
|
||||
this._bufferedResponses.delete(data.responseId);
|
||||
return;
|
||||
}
|
||||
if (this._paused)
|
||||
this._bufferedIncomingMessages.push(data);
|
||||
else
|
||||
this._onMessageInternal(data);
|
||||
}
|
||||
|
||||
async _onMessageInternal(data) {
|
||||
if (data.responseId) {
|
||||
this.transport.sendMessage({ ack: 'RESPONSE_ACK', responseId: data.responseId });
|
||||
const message = this._pendingMessages.get(data.responseId);
|
||||
if (!message) {
|
||||
// During cross-process navigation, we might receive a response for
|
||||
// the message sent by another process.
|
||||
return;
|
||||
}
|
||||
this._pendingMessages.delete(data.responseId);
|
||||
if (data.error)
|
||||
message.reject(new Error(data.error));
|
||||
else
|
||||
message.resolve(data.result);
|
||||
} else if (data.requestId) {
|
||||
// When the underlying transport gets replaced, some responses might
|
||||
// not get delivered. As a result, sender will repeat the same request once
|
||||
// a new transport gets set.
|
||||
//
|
||||
// If this request was already processed, we can fulfill it with the cached response
|
||||
// and fast-return.
|
||||
if (this._bufferedResponses.has(data.requestId)) {
|
||||
this.transport.sendMessage(this._bufferedResponses.get(data.requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
const namespace = data.namespace;
|
||||
const handler = this._handlers.get(namespace);
|
||||
if (!handler) {
|
||||
this._bufferedIncomingMessages.push(data);
|
||||
return;
|
||||
}
|
||||
const method = handler[data.methodName];
|
||||
if (!method) {
|
||||
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
|
||||
return;
|
||||
}
|
||||
let response;
|
||||
const connectedToUID = this._connectedToUID;
|
||||
try {
|
||||
const result = await method.call(handler, ...data.params);
|
||||
response = {responseId: data.requestId, result};
|
||||
} catch (error) {
|
||||
response = {responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`};
|
||||
}
|
||||
// The connection might have changed during the ASYNCHRONOUS handler execution.
|
||||
// We only need to buffer & send response if we are connected to the same
|
||||
// end.
|
||||
if (connectedToUID === this._connectedToUID) {
|
||||
this._bufferedResponses.set(data.requestId, response);
|
||||
this.transport.sendMessage(response);
|
||||
}
|
||||
} else {
|
||||
dump(`WARNING: unknown message in channel "${this._name}": ${JSON.stringify(data)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = ['SimpleChannel'];
|
||||
this.SimpleChannel = SimpleChannel;
|
||||
1200
additions/juggler/TargetRegistry.js
Normal file
157
additions/juggler/components/Juggler.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Juggler", "JugglerFactory"];
|
||||
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const {ComponentUtils} = ChromeUtils.import("resource://gre/modules/ComponentUtils.jsm");
|
||||
const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js");
|
||||
const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js");
|
||||
const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
|
||||
const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {ActorManagerParent} = ChromeUtils.import('resource://gre/modules/ActorManagerParent.jsm');
|
||||
const helper = new Helper();
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
// Register JSWindowActors that will be instantiated for each frame.
|
||||
ActorManagerParent.addJSWindowActors({
|
||||
JugglerFrame: {
|
||||
parent: {
|
||||
moduleURI: 'chrome://juggler/content/JugglerFrameParent.jsm',
|
||||
},
|
||||
child: {
|
||||
moduleURI: 'chrome://juggler/content/content/JugglerFrameChild.jsm',
|
||||
events: {
|
||||
// Normally, we instantiate an actor when a new window is created.
|
||||
DOMWindowCreated: {},
|
||||
// However, for same-origin iframes, the navigation from about:blank
|
||||
// to the URL will share the same window, so we need to also create
|
||||
// an actor for a new document via DOMDocElementInserted.
|
||||
DOMDocElementInserted: {},
|
||||
// Also, listening to DOMContentLoaded.
|
||||
DOMContentLoaded: {},
|
||||
DOMWillOpenModalDialog: {},
|
||||
DOMModalDialogClosed: {},
|
||||
},
|
||||
},
|
||||
allFrames: true,
|
||||
},
|
||||
});
|
||||
|
||||
let browserStartupFinishedCallback;
|
||||
let browserStartupFinishedPromise = new Promise(x => browserStartupFinishedCallback = x);
|
||||
|
||||
class Juggler {
|
||||
get classDescription() { return "Sample command-line handler"; }
|
||||
get classID() { return Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'); }
|
||||
get contractID() { return "@mozilla.org/remote/juggler;1" }
|
||||
get QueryInterface() {
|
||||
return ChromeUtils.generateQI([ Ci.nsICommandLineHandler, Ci.nsIObserver ]);
|
||||
}
|
||||
get helpInfo() {
|
||||
return " --juggler Enable Juggler automation\n";
|
||||
}
|
||||
|
||||
handle(cmdLine) {
|
||||
// flag has to be consumed in nsICommandLineHandler:handle
|
||||
// to avoid issues on macos. See Marionette.jsm::handle() for more details.
|
||||
// TODO: remove after Bug 1724251 is fixed.
|
||||
cmdLine.handleFlag("juggler-pipe", false);
|
||||
}
|
||||
|
||||
// This flow is taken from Remote agent and Marionette.
|
||||
// See https://github.com/mozilla/gecko-dev/blob/0c1b4921830e6af8bc951da01d7772de2fe60a08/remote/components/RemoteAgent.jsm#L302
|
||||
async observe(subject, topic) {
|
||||
switch (topic) {
|
||||
case "profile-after-change":
|
||||
Services.obs.addObserver(this, "command-line-startup");
|
||||
Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
|
||||
break;
|
||||
case "command-line-startup":
|
||||
Services.obs.removeObserver(this, topic);
|
||||
const cmdLine = subject;
|
||||
const jugglerPipeFlag = cmdLine.handleFlag('juggler-pipe', false);
|
||||
if (!jugglerPipeFlag)
|
||||
return;
|
||||
|
||||
this._silent = cmdLine.findFlag('silent', false) >= 0;
|
||||
if (this._silent) {
|
||||
Services.startup.enterLastWindowClosingSurvivalArea();
|
||||
browserStartupFinishedCallback();
|
||||
}
|
||||
Services.obs.addObserver(this, "final-ui-startup");
|
||||
break;
|
||||
case "browser-idle-startup-tasks-finished":
|
||||
browserStartupFinishedCallback();
|
||||
break;
|
||||
// Used to wait until the initial application window has been opened.
|
||||
case "final-ui-startup":
|
||||
Services.obs.removeObserver(this, topic);
|
||||
|
||||
const targetRegistry = new TargetRegistry();
|
||||
new NetworkObserver(targetRegistry);
|
||||
|
||||
const loadStyleSheet = () => {
|
||||
if (Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless) {
|
||||
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService);
|
||||
const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
|
||||
const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null);
|
||||
styleSheetService.loadAndRegisterSheet(uri, styleSheetService.AGENT_SHEET);
|
||||
}
|
||||
};
|
||||
|
||||
// Force create hidden window here, otherwise its creation later closes the web socket!
|
||||
Services.appShell.hiddenDOMWindow;
|
||||
|
||||
let pipeStopped = false;
|
||||
let browserHandler;
|
||||
const pipe = Cc['@mozilla.org/juggler/remotedebuggingpipe;1'].getService(Ci.nsIRemoteDebuggingPipe);
|
||||
const connection = {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIRemoteDebuggingPipeClient]),
|
||||
receiveMessage(message) {
|
||||
if (this.onmessage)
|
||||
this.onmessage({ data: message });
|
||||
},
|
||||
disconnected() {
|
||||
if (browserHandler)
|
||||
browserHandler['Browser.close']();
|
||||
},
|
||||
send(message) {
|
||||
if (pipeStopped) {
|
||||
// We are missing the response to Browser.close,
|
||||
// but everything works fine. Once we actually need it,
|
||||
// we have to stop the pipe after the response is sent.
|
||||
return;
|
||||
}
|
||||
pipe.sendMessage(message);
|
||||
},
|
||||
};
|
||||
pipe.init(connection);
|
||||
const dispatcher = new Dispatcher(connection);
|
||||
browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, browserStartupFinishedPromise, () => {
|
||||
if (this._silent)
|
||||
Services.startup.exitLastWindowClosingSurvivalArea();
|
||||
connection.onclose();
|
||||
pipe.stop();
|
||||
pipeStopped = true;
|
||||
});
|
||||
dispatcher.rootSession().setHandler(browserHandler);
|
||||
loadStyleSheet();
|
||||
dump(`\nJuggler listening to the pipe\n`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const jugglerInstance = new Juggler();
|
||||
|
||||
// This is used by the XPCOM codepath which expects a constructor
|
||||
var JugglerFactory = function() {
|
||||
return jugglerInstance;
|
||||
};
|
||||
|
||||
18
additions/juggler/components/components.conf
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
Classes = [
|
||||
# Juggler
|
||||
{
|
||||
"cid": "{f7a74a33-e2ab-422d-b022-4fb213dd2639}",
|
||||
"contract_ids": ["@mozilla.org/remote/juggler;1"],
|
||||
"categories": {
|
||||
"command-line-handler": "m-remote",
|
||||
"profile-after-change": "Juggler",
|
||||
},
|
||||
"jsm": "chrome://juggler/content/components/Juggler.js",
|
||||
"constructor": "JugglerFactory",
|
||||
},
|
||||
]
|
||||
|
||||
6
additions/juggler/components/moz.build
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
XPCOM_MANIFESTS += ["components.conf"]
|
||||
|
||||
709
additions/juggler/content/FrameTree.js
Normal file
|
|
@ -0,0 +1,709 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
||||
const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js');
|
||||
|
||||
const helper = new Helper();
|
||||
|
||||
class FrameTree {
|
||||
constructor(rootBrowsingContext) {
|
||||
helper.decorateAsEventEmitter(this);
|
||||
|
||||
this._rootBrowsingContext = rootBrowsingContext;
|
||||
|
||||
this._browsingContextGroup = rootBrowsingContext.group;
|
||||
if (!this._browsingContextGroup.__jugglerFrameTrees)
|
||||
this._browsingContextGroup.__jugglerFrameTrees = new Set();
|
||||
this._browsingContextGroup.__jugglerFrameTrees.add(this);
|
||||
this._isolatedWorlds = new Map();
|
||||
|
||||
this._webSocketEventService = Cc[
|
||||
"@mozilla.org/websocketevent/service;1"
|
||||
].getService(Ci.nsIWebSocketEventService);
|
||||
|
||||
this._runtime = new Runtime(false /* isWorker */);
|
||||
this._workers = new Map();
|
||||
this._frameIdToFrame = new Map();
|
||||
this._pageReady = false;
|
||||
this._javaScriptDisabled = false;
|
||||
for (const browsingContext of helper.collectAllBrowsingContexts(rootBrowsingContext))
|
||||
this._createFrame(browsingContext);
|
||||
this._mainFrame = this.frameForBrowsingContext(rootBrowsingContext);
|
||||
|
||||
const webProgress = rootBrowsingContext.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebProgress);
|
||||
this.QueryInterface = ChromeUtils.generateQI([
|
||||
Ci.nsIWebProgressListener,
|
||||
Ci.nsIWebProgressListener2,
|
||||
Ci.nsISupportsWeakReference,
|
||||
]);
|
||||
|
||||
this._addedScrollbarsStylesheetSymbol = Symbol('_addedScrollbarsStylesheetSymbol');
|
||||
|
||||
this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
|
||||
this._wdmListener = {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
|
||||
onRegister: this._onWorkerCreated.bind(this),
|
||||
onUnregister: this._onWorkerDestroyed.bind(this),
|
||||
};
|
||||
this._wdm.addListener(this._wdmListener);
|
||||
for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator())
|
||||
this._onWorkerCreated(workerDebugger);
|
||||
|
||||
const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
|
||||
Ci.nsIWebProgress.NOTIFY_LOCATION;
|
||||
this._eventListeners = [
|
||||
helper.addObserver((docShell, topic, loadIdentifier) => {
|
||||
const frame = this.frameForDocShell(docShell);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._pendingNavigationId = helper.toProtocolNavigationId(loadIdentifier);
|
||||
this.emit(FrameTree.Events.NavigationStarted, frame);
|
||||
}, 'juggler-navigation-started-renderer'),
|
||||
helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
|
||||
helper.addObserver(this._onDOMWindowCreated.bind(this), 'juggler-dom-window-reused'),
|
||||
helper.addObserver((browsingContext, topic, why) => {
|
||||
this._onBrowsingContextAttached(browsingContext);
|
||||
}, 'browsing-context-attached'),
|
||||
helper.addObserver((browsingContext, topic, why) => {
|
||||
this._onBrowsingContextDetached(browsingContext);
|
||||
}, 'browsing-context-discarded'),
|
||||
helper.addObserver((subject, topic, eventInfo) => {
|
||||
const [type, jugglerEventId] = eventInfo.split(' ');
|
||||
this.emit(FrameTree.Events.InputEvent, { type, jugglerEventId: +(jugglerEventId ?? '0') });
|
||||
}, 'juggler-mouse-event-hit-renderer'),
|
||||
helper.addProgressListener(webProgress, this, flags),
|
||||
];
|
||||
|
||||
this._dragEventListeners = [];
|
||||
}
|
||||
|
||||
workers() {
|
||||
return [...this._workers.values()];
|
||||
}
|
||||
|
||||
runtime() {
|
||||
return this._runtime;
|
||||
}
|
||||
|
||||
setInitScripts(scripts) {
|
||||
for (const world of this._isolatedWorlds.values())
|
||||
world._scriptsToEvaluateOnNewDocument = [];
|
||||
|
||||
for (let { worldName, script } of scripts) {
|
||||
worldName = worldName || '';
|
||||
const existing = this._isolatedWorlds.has(worldName);
|
||||
const world = this._ensureWorld(worldName);
|
||||
world._scriptsToEvaluateOnNewDocument.push(script);
|
||||
// FIXME: 'should inherit http credentials from browser context' fails without this
|
||||
if (worldName && !existing) {
|
||||
for (const frame of this.frames())
|
||||
frame._createIsolatedContext(worldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ensureWorld(worldName) {
|
||||
worldName = worldName || '';
|
||||
let world = this._isolatedWorlds.get(worldName);
|
||||
if (!world) {
|
||||
world = new IsolatedWorld(worldName);
|
||||
this._isolatedWorlds.set(worldName, world);
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
_frameForWorker(workerDebugger) {
|
||||
if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED)
|
||||
return null;
|
||||
if (!workerDebugger.window)
|
||||
return null;
|
||||
return this.frameForDocShell(workerDebugger.window.docShell);
|
||||
}
|
||||
|
||||
_onDOMWindowCreated(window) {
|
||||
if (!window[this._addedScrollbarsStylesheetSymbol] && this.scrollbarsHidden) {
|
||||
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService);
|
||||
const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
|
||||
const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null);
|
||||
const sheet = styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
|
||||
window.windowUtils.addSheet(sheet, styleSheetService.AGENT_SHEET);
|
||||
window[this._addedScrollbarsStylesheetSymbol] = true;
|
||||
}
|
||||
const frame = this.frameForDocShell(window.docShell);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._onGlobalObjectCleared();
|
||||
}
|
||||
|
||||
setScrollbarsHidden(hidden) {
|
||||
this.scrollbarsHidden = hidden;
|
||||
}
|
||||
|
||||
setJavaScriptDisabled(javaScriptDisabled) {
|
||||
this._javaScriptDisabled = javaScriptDisabled;
|
||||
for (const frame of this.frames())
|
||||
frame._updateJavaScriptDisabled();
|
||||
}
|
||||
|
||||
_onWorkerCreated(workerDebugger) {
|
||||
// Note: we do not interoperate with firefox devtools.
|
||||
if (workerDebugger.isInitialized)
|
||||
return;
|
||||
const frame = this._frameForWorker(workerDebugger);
|
||||
if (!frame)
|
||||
return;
|
||||
const worker = new Worker(frame, workerDebugger);
|
||||
this._workers.set(workerDebugger, worker);
|
||||
this.emit(FrameTree.Events.WorkerCreated, worker);
|
||||
}
|
||||
|
||||
_onWorkerDestroyed(workerDebugger) {
|
||||
const worker = this._workers.get(workerDebugger);
|
||||
if (!worker)
|
||||
return;
|
||||
worker.dispose();
|
||||
this._workers.delete(workerDebugger);
|
||||
this.emit(FrameTree.Events.WorkerDestroyed, worker);
|
||||
}
|
||||
|
||||
allFramesInBrowsingContextGroup(group) {
|
||||
const frames = [];
|
||||
for (const frameTree of (group.__jugglerFrameTrees || [])) {
|
||||
for (const frame of frameTree.frames()) {
|
||||
try {
|
||||
// Try accessing docShell and domWindow to filter out dead frames.
|
||||
// This might happen for print-preview frames, but maybe for something else as well.
|
||||
frame.docShell();
|
||||
frame.domWindow();
|
||||
frames.push(frame);
|
||||
} catch (e) {
|
||||
dump(`WARNING: unable to access docShell and domWindow of the frame[id=${frame.id()}]\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
isPageReady() {
|
||||
return this._pageReady;
|
||||
}
|
||||
|
||||
forcePageReady() {
|
||||
if (this._pageReady)
|
||||
return false;
|
||||
this._pageReady = true;
|
||||
this.emit(FrameTree.Events.PageReady);
|
||||
return true;
|
||||
}
|
||||
|
||||
addBinding(worldName, name, script) {
|
||||
worldName = worldName || '';
|
||||
const world = this._ensureWorld(worldName);
|
||||
world._bindings.set(name, script);
|
||||
for (const frame of this.frames())
|
||||
frame._addBinding(worldName, name, script);
|
||||
}
|
||||
|
||||
frameForBrowsingContext(browsingContext) {
|
||||
if (!browsingContext)
|
||||
return null;
|
||||
const frameId = helper.browsingContextToFrameId(browsingContext);
|
||||
return this._frameIdToFrame.get(frameId) ?? null;
|
||||
}
|
||||
|
||||
frameForDocShell(docShell) {
|
||||
if (!docShell)
|
||||
return null;
|
||||
const frameId = helper.browsingContextToFrameId(docShell.browsingContext);
|
||||
return this._frameIdToFrame.get(frameId) ?? null;
|
||||
}
|
||||
|
||||
frame(frameId) {
|
||||
return this._frameIdToFrame.get(frameId) || null;
|
||||
}
|
||||
|
||||
frames() {
|
||||
let result = [];
|
||||
collect(this._mainFrame);
|
||||
return result;
|
||||
|
||||
function collect(frame) {
|
||||
result.push(frame);
|
||||
for (const subframe of frame._children)
|
||||
collect(subframe);
|
||||
}
|
||||
}
|
||||
|
||||
mainFrame() {
|
||||
return this._mainFrame;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._browsingContextGroup.__jugglerFrameTrees.delete(this);
|
||||
this._wdm.removeListener(this._wdmListener);
|
||||
this._runtime.dispose();
|
||||
helper.removeListeners(this._eventListeners);
|
||||
helper.removeListeners(this._dragEventListeners);
|
||||
}
|
||||
|
||||
onWindowEvent(event) {
|
||||
if (event.type !== 'DOMDocElementInserted' || !event.target.ownerGlobal)
|
||||
return;
|
||||
|
||||
const docShell = event.target.ownerGlobal.docShell;
|
||||
const frame = this.frameForDocShell(docShell);
|
||||
if (!frame) {
|
||||
dump(`WARNING: ${event.type} for unknown frame ${helper.browsingContextToFrameId(docShell.browsingContext)}\n`);
|
||||
return;
|
||||
}
|
||||
if (frame._pendingNavigationId) {
|
||||
docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
this._frameNavigationCommitted(frame, docShell.currentURI.spec);
|
||||
}
|
||||
|
||||
if (frame === this._mainFrame) {
|
||||
helper.removeListeners(this._dragEventListeners);
|
||||
const chromeEventHandler = docShell.chromeEventHandler;
|
||||
const options = {
|
||||
mozSystemGroup: true,
|
||||
capture: true,
|
||||
};
|
||||
const emitInputEvent = (event) => this.emit(FrameTree.Events.InputEvent, { type: event.type, jugglerEventId: 0 });
|
||||
// Drag events are dispatched from content process, so these we don't see in the
|
||||
// `juggler-mouse-event-hit-renderer` instrumentation.
|
||||
this._dragEventListeners = [
|
||||
helper.addEventListener(chromeEventHandler, 'dragstart', emitInputEvent, options),
|
||||
helper.addEventListener(chromeEventHandler, 'dragover', emitInputEvent, options),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
_frameNavigationCommitted(frame, url) {
|
||||
for (const subframe of frame._children)
|
||||
this._detachFrame(subframe);
|
||||
const navigationId = frame._pendingNavigationId;
|
||||
frame._pendingNavigationId = null;
|
||||
frame._lastCommittedNavigationId = navigationId;
|
||||
frame._url = url;
|
||||
this.emit(FrameTree.Events.NavigationCommitted, frame);
|
||||
if (frame === this._mainFrame)
|
||||
this.forcePageReady();
|
||||
}
|
||||
|
||||
onStateChange(progress, request, flag, status) {
|
||||
if (!(request instanceof Ci.nsIChannel))
|
||||
return;
|
||||
const channel = request.QueryInterface(Ci.nsIChannel);
|
||||
const docShell = progress.DOMWindow.docShell;
|
||||
const frame = this.frameForDocShell(docShell);
|
||||
if (!frame)
|
||||
return;
|
||||
|
||||
if (!channel.isDocument) {
|
||||
// Somehow, we can get worker requests here,
|
||||
// while we are only interested in frame documents.
|
||||
return;
|
||||
}
|
||||
|
||||
const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
|
||||
if (isStop && frame._pendingNavigationId && status) {
|
||||
// Navigation is aborted.
|
||||
const navigationId = frame._pendingNavigationId;
|
||||
frame._pendingNavigationId = null;
|
||||
// Always report download navigation as failure to match other browsers.
|
||||
const errorText = helper.getNetworkErrorStatusText(status);
|
||||
this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText);
|
||||
if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED)
|
||||
this.forcePageReady();
|
||||
}
|
||||
}
|
||||
|
||||
onLocationChange(progress, request, location, flags) {
|
||||
const docShell = progress.DOMWindow.docShell;
|
||||
const frame = this.frameForDocShell(docShell);
|
||||
const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
|
||||
if (frame && sameDocumentNavigation) {
|
||||
frame._url = location.spec;
|
||||
this.emit(FrameTree.Events.SameDocumentNavigation, frame);
|
||||
}
|
||||
}
|
||||
|
||||
_onBrowsingContextAttached(browsingContext) {
|
||||
// If this browsing context doesn't belong to our frame tree - do nothing.
|
||||
if (browsingContext.top !== this._rootBrowsingContext)
|
||||
return;
|
||||
this._createFrame(browsingContext);
|
||||
}
|
||||
|
||||
_onBrowsingContextDetached(browsingContext) {
|
||||
const frame = this.frameForBrowsingContext(browsingContext);
|
||||
if (frame)
|
||||
this._detachFrame(frame);
|
||||
}
|
||||
|
||||
_createFrame(browsingContext) {
|
||||
const parentFrame = this.frameForBrowsingContext(browsingContext.parent);
|
||||
if (!parentFrame && this._mainFrame) {
|
||||
dump(`WARNING: found docShell with the same root, but no parent!\n`);
|
||||
return;
|
||||
}
|
||||
const frame = new Frame(this, this._runtime, browsingContext, parentFrame);
|
||||
this._frameIdToFrame.set(frame.id(), frame);
|
||||
if (browsingContext.docShell?.domWindow && browsingContext.docShell?.domWindow.location)
|
||||
frame._url = browsingContext.docShell.domWindow.location.href;
|
||||
this.emit(FrameTree.Events.FrameAttached, frame);
|
||||
// Create execution context **after** reporting frame.
|
||||
// This is our protocol contract.
|
||||
if (frame.domWindow())
|
||||
frame._onGlobalObjectCleared();
|
||||
return frame;
|
||||
}
|
||||
|
||||
_detachFrame(frame) {
|
||||
// Detach all children first
|
||||
for (const subframe of frame._children)
|
||||
this._detachFrame(subframe);
|
||||
if (frame === this._mainFrame) {
|
||||
// Do not detach main frame (happens during cross-process navigation),
|
||||
// as it confuses the client.
|
||||
return;
|
||||
}
|
||||
this._frameIdToFrame.delete(frame.id());
|
||||
if (frame._parentFrame)
|
||||
frame._parentFrame._children.delete(frame);
|
||||
frame._parentFrame = null;
|
||||
frame.dispose();
|
||||
this.emit(FrameTree.Events.FrameDetached, frame);
|
||||
}
|
||||
}
|
||||
|
||||
FrameTree.Events = {
|
||||
FrameAttached: 'frameattached',
|
||||
FrameDetached: 'framedetached',
|
||||
WorkerCreated: 'workercreated',
|
||||
WorkerDestroyed: 'workerdestroyed',
|
||||
WebSocketCreated: 'websocketcreated',
|
||||
WebSocketOpened: 'websocketopened',
|
||||
WebSocketClosed: 'websocketclosed',
|
||||
WebSocketFrameReceived: 'websocketframereceived',
|
||||
WebSocketFrameSent: 'websocketframesent',
|
||||
NavigationStarted: 'navigationstarted',
|
||||
NavigationCommitted: 'navigationcommitted',
|
||||
NavigationAborted: 'navigationaborted',
|
||||
SameDocumentNavigation: 'samedocumentnavigation',
|
||||
PageReady: 'pageready',
|
||||
InputEvent: 'inputevent',
|
||||
};
|
||||
|
||||
class IsolatedWorld {
|
||||
constructor(name) {
|
||||
this._name = name;
|
||||
this._scriptsToEvaluateOnNewDocument = [];
|
||||
this._bindings = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
class Frame {
|
||||
constructor(frameTree, runtime, browsingContext, parentFrame) {
|
||||
this._frameTree = frameTree;
|
||||
this._runtime = runtime;
|
||||
this._browsingContext = browsingContext;
|
||||
this._children = new Set();
|
||||
this._frameId = helper.browsingContextToFrameId(browsingContext);
|
||||
this._parentFrame = null;
|
||||
this._url = '';
|
||||
if (parentFrame) {
|
||||
this._parentFrame = parentFrame;
|
||||
parentFrame._children.add(this);
|
||||
}
|
||||
|
||||
this._lastCommittedNavigationId = null;
|
||||
this._pendingNavigationId = null;
|
||||
|
||||
this._textInputProcessor = null;
|
||||
|
||||
this._worldNameToContext = new Map();
|
||||
this._initialNavigationDone = false;
|
||||
|
||||
this._webSocketListenerInnerWindowId = 0;
|
||||
// WebSocketListener calls frameReceived event before webSocketOpened.
|
||||
// To avoid this, serialize event reporting.
|
||||
this._webSocketInfos = new Map();
|
||||
|
||||
const dispatchWebSocketFrameReceived = (webSocketSerialID, frame) => this._frameTree.emit(FrameTree.Events.WebSocketFrameReceived, {
|
||||
frameId: this._frameId,
|
||||
wsid: webSocketSerialID + '',
|
||||
opcode: frame.opCode,
|
||||
data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload,
|
||||
});
|
||||
this._webSocketListener = {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebSocketEventListener, ]),
|
||||
|
||||
webSocketCreated: (webSocketSerialID, uri, protocols) => {
|
||||
this._frameTree.emit(FrameTree.Events.WebSocketCreated, {
|
||||
frameId: this._frameId,
|
||||
wsid: webSocketSerialID + '',
|
||||
requestURL: uri,
|
||||
});
|
||||
this._webSocketInfos.set(webSocketSerialID, {
|
||||
opened: false,
|
||||
pendingIncomingFrames: [],
|
||||
});
|
||||
},
|
||||
|
||||
webSocketOpened: (webSocketSerialID, effectiveURI, protocols, extensions, httpChannelId) => {
|
||||
this._frameTree.emit(FrameTree.Events.WebSocketOpened, {
|
||||
frameId: this._frameId,
|
||||
requestId: httpChannelId + '',
|
||||
wsid: webSocketSerialID + '',
|
||||
effectiveURL: effectiveURI,
|
||||
});
|
||||
const info = this._webSocketInfos.get(webSocketSerialID);
|
||||
info.opened = true;
|
||||
for (const frame of info.pendingIncomingFrames)
|
||||
dispatchWebSocketFrameReceived(webSocketSerialID, frame);
|
||||
},
|
||||
|
||||
webSocketMessageAvailable: (webSocketSerialID, data, messageType) => {
|
||||
// We don't use this event.
|
||||
},
|
||||
|
||||
webSocketClosed: (webSocketSerialID, wasClean, code, reason) => {
|
||||
this._webSocketInfos.delete(webSocketSerialID);
|
||||
let error = '';
|
||||
if (!wasClean) {
|
||||
const keys = Object.keys(Ci.nsIWebSocketChannel);
|
||||
for (const key of keys) {
|
||||
if (Ci.nsIWebSocketChannel[key] === code)
|
||||
error = key;
|
||||
}
|
||||
}
|
||||
this._frameTree.emit(FrameTree.Events.WebSocketClosed, {
|
||||
frameId: this._frameId,
|
||||
wsid: webSocketSerialID + '',
|
||||
error,
|
||||
});
|
||||
},
|
||||
|
||||
frameReceived: (webSocketSerialID, frame) => {
|
||||
// Report only text and binary frames.
|
||||
if (frame.opCode !== 1 && frame.opCode !== 2)
|
||||
return;
|
||||
const info = this._webSocketInfos.get(webSocketSerialID);
|
||||
if (info.opened)
|
||||
dispatchWebSocketFrameReceived(webSocketSerialID, frame);
|
||||
else
|
||||
info.pendingIncomingFrames.push(frame);
|
||||
},
|
||||
|
||||
frameSent: (webSocketSerialID, frame) => {
|
||||
// Report only text and binary frames.
|
||||
if (frame.opCode !== 1 && frame.opCode !== 2)
|
||||
return;
|
||||
this._frameTree.emit(FrameTree.Events.WebSocketFrameSent, {
|
||||
frameId: this._frameId,
|
||||
wsid: webSocketSerialID + '',
|
||||
opcode: frame.opCode,
|
||||
data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_createIsolatedContext(name) {
|
||||
const principal = [this.domWindow()]; // extended principal
|
||||
const sandbox = Cu.Sandbox(principal, {
|
||||
sandboxPrototype: this.domWindow(),
|
||||
wantComponents: false,
|
||||
wantExportHelpers: false,
|
||||
wantXrays: true,
|
||||
});
|
||||
const world = this._runtime.createExecutionContext(this.domWindow(), sandbox, {
|
||||
frameId: this.id(),
|
||||
name,
|
||||
});
|
||||
this._worldNameToContext.set(name, world);
|
||||
return world;
|
||||
}
|
||||
|
||||
unsafeObject(objectId) {
|
||||
for (const context of this._worldNameToContext.values()) {
|
||||
const result = context.unsafeObject(objectId);
|
||||
if (result)
|
||||
return result.object;
|
||||
}
|
||||
throw new Error('Cannot find object with id = ' + objectId);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const context of this._worldNameToContext.values())
|
||||
this._runtime.destroyExecutionContext(context);
|
||||
this._worldNameToContext.clear();
|
||||
}
|
||||
|
||||
_addBinding(worldName, name, script) {
|
||||
let executionContext = this._worldNameToContext.get(worldName);
|
||||
if (worldName && !executionContext)
|
||||
executionContext = this._createIsolatedContext(worldName);
|
||||
if (executionContext)
|
||||
executionContext.addBinding(name, script);
|
||||
}
|
||||
|
||||
_onGlobalObjectCleared() {
|
||||
const webSocketService = this._frameTree._webSocketEventService;
|
||||
if (this._webSocketListenerInnerWindowId && webSocketService.hasListenerFor(this._webSocketListenerInnerWindowId))
|
||||
webSocketService.removeListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
|
||||
this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId;
|
||||
webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
|
||||
|
||||
for (const context of this._worldNameToContext.values())
|
||||
this._runtime.destroyExecutionContext(context);
|
||||
this._worldNameToContext.clear();
|
||||
|
||||
this._worldNameToContext.set('', this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), {
|
||||
frameId: this._frameId,
|
||||
name: '',
|
||||
}));
|
||||
for (const [name, world] of this._frameTree._isolatedWorlds) {
|
||||
if (name)
|
||||
this._createIsolatedContext(name);
|
||||
const executionContext = this._worldNameToContext.get(name);
|
||||
// Add bindings before evaluating scripts.
|
||||
for (const [name, script] of world._bindings)
|
||||
executionContext.addBinding(name, script);
|
||||
for (const script of world._scriptsToEvaluateOnNewDocument)
|
||||
executionContext.evaluateScriptSafely(script);
|
||||
}
|
||||
|
||||
const url = this.domWindow().location?.href;
|
||||
if (url === 'about:blank' && !this._url) {
|
||||
// Sometimes FrameTree is created too early, before the location has been set.
|
||||
this._url = url;
|
||||
this._frameTree.emit(FrameTree.Events.NavigationCommitted, this);
|
||||
}
|
||||
|
||||
this._updateJavaScriptDisabled();
|
||||
}
|
||||
|
||||
_updateJavaScriptDisabled() {
|
||||
if (this._browsingContext.currentWindowContext)
|
||||
this._browsingContext.currentWindowContext.allowJavascript = !this._frameTree._javaScriptDisabled;
|
||||
}
|
||||
|
||||
mainExecutionContext() {
|
||||
return this._worldNameToContext.get('');
|
||||
}
|
||||
|
||||
textInputProcessor() {
|
||||
if (!this._textInputProcessor) {
|
||||
this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor);
|
||||
}
|
||||
this._textInputProcessor.beginInputTransactionForTests(this.docShell().DOMWindow);
|
||||
return this._textInputProcessor;
|
||||
}
|
||||
|
||||
pendingNavigationId() {
|
||||
return this._pendingNavigationId;
|
||||
}
|
||||
|
||||
lastCommittedNavigationId() {
|
||||
return this._lastCommittedNavigationId;
|
||||
}
|
||||
|
||||
docShell() {
|
||||
return this._browsingContext.docShell;
|
||||
}
|
||||
|
||||
domWindow() {
|
||||
return this.docShell()?.domWindow;
|
||||
}
|
||||
|
||||
name() {
|
||||
const frameElement = this.domWindow()?.frameElement;
|
||||
let name = '';
|
||||
if (frameElement)
|
||||
name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || '';
|
||||
return name;
|
||||
}
|
||||
|
||||
parentFrame() {
|
||||
return this._parentFrame;
|
||||
}
|
||||
|
||||
id() {
|
||||
return this._frameId;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Worker {
|
||||
constructor(frame, workerDebugger) {
|
||||
this._frame = frame;
|
||||
this._workerId = helper.generateId();
|
||||
this._workerDebugger = workerDebugger;
|
||||
|
||||
workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js');
|
||||
|
||||
this._channel = new SimpleChannel(`content::worker[${this._workerId}]`, 'worker-' + this._workerId);
|
||||
this._channel.setTransport({
|
||||
sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)),
|
||||
dispose: () => {},
|
||||
});
|
||||
this._workerDebuggerListener = {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]),
|
||||
onMessage: msg => void this._channel._onMessage(JSON.parse(msg)),
|
||||
onClose: () => void this._channel.dispose(),
|
||||
onError: (filename, lineno, message) => {
|
||||
dump(`WARNING: Error in worker: ${message} @${filename}:${lineno}\n`);
|
||||
},
|
||||
};
|
||||
workerDebugger.addListener(this._workerDebuggerListener);
|
||||
}
|
||||
|
||||
channel() {
|
||||
return this._channel;
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
id() {
|
||||
return this._workerId;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._workerDebugger.url;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._channel.dispose();
|
||||
this._workerDebugger.removeListener(this._workerDebuggerListener);
|
||||
}
|
||||
}
|
||||
|
||||
function channelId(channel) {
|
||||
if (channel instanceof Ci.nsIIdentChannel) {
|
||||
const identChannel = channel.QueryInterface(Ci.nsIIdentChannel);
|
||||
return String(identChannel.channelId);
|
||||
}
|
||||
return helper.generateId();
|
||||
}
|
||||
|
||||
|
||||
var EXPORTED_SYMBOLS = ['FrameTree'];
|
||||
this.FrameTree = FrameTree;
|
||||
|
||||
64
additions/juggler/content/JugglerFrameChild.jsm
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"use strict";
|
||||
|
||||
const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const { initialize } = ChromeUtils.import('chrome://juggler/content/content/main.js');
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const helper = new Helper();
|
||||
|
||||
let sameProcessInstanceNumber = 0;
|
||||
|
||||
class JugglerFrameChild extends JSWindowActorChild {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._eventListeners = [];
|
||||
}
|
||||
|
||||
handleEvent(aEvent) {
|
||||
if (this._agents && aEvent.type === 'DOMWillOpenModalDialog') {
|
||||
this._agents.channel.pause();
|
||||
return;
|
||||
}
|
||||
if (this._agents && aEvent.type === 'DOMModalDialogClosed') {
|
||||
this._agents.channel.resumeSoon();
|
||||
return;
|
||||
}
|
||||
if (this._agents && aEvent.target === this.document)
|
||||
this._agents.pageAgent.onWindowEvent(aEvent);
|
||||
if (this._agents && aEvent.target === this.document)
|
||||
this._agents.frameTree.onWindowEvent(aEvent);
|
||||
}
|
||||
|
||||
actorCreated() {
|
||||
this.actorName = `content::${this.browsingContext.browserId}/${this.browsingContext.id}/${++sameProcessInstanceNumber}`;
|
||||
|
||||
this._eventListeners.push(helper.addEventListener(this.contentWindow, 'load', event => {
|
||||
this._agents?.pageAgent.onWindowEvent(event);
|
||||
}));
|
||||
|
||||
if (this.document.documentURI.startsWith('moz-extension://'))
|
||||
return;
|
||||
this._agents = initialize(this.browsingContext, this.docShell, this);
|
||||
}
|
||||
|
||||
_dispose() {
|
||||
helper.removeListeners(this._eventListeners);
|
||||
// We do not cleanup since agents are shared for all frames in the process.
|
||||
|
||||
// TODO: restore the cleanup.
|
||||
// Reset transport so that all messages will be pending and will not throw any errors.
|
||||
// this._channel.resetTransport();
|
||||
// this._agents.pageAgent.dispose();
|
||||
// this._agents.frameTree.dispose();
|
||||
// this._agents = undefined;
|
||||
}
|
||||
|
||||
didDestroy() {
|
||||
this._dispose();
|
||||
}
|
||||
|
||||
receiveMessage() { }
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = ['JugglerFrameChild'];
|
||||
703
additions/juggler/content/PageAgent.js
Normal file
|
|
@ -0,0 +1,703 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cr = Components.results;
|
||||
const Cu = Components.utils;
|
||||
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||
const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm');
|
||||
|
||||
const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
|
||||
Ci.nsIDragService
|
||||
);
|
||||
const obs = Cc["@mozilla.org/observer-service;1"].getService(
|
||||
Ci.nsIObserverService
|
||||
);
|
||||
|
||||
const helper = new Helper();
|
||||
|
||||
class WorkerData {
|
||||
constructor(pageAgent, browserChannel, worker) {
|
||||
this._workerRuntime = worker.channel().connect('runtime');
|
||||
this._browserWorker = browserChannel.connect(worker.id());
|
||||
this._worker = worker;
|
||||
const emit = name => {
|
||||
return (...args) => this._browserWorker.emit(name, ...args);
|
||||
};
|
||||
this._eventListeners = [
|
||||
worker.channel().register('runtime', {
|
||||
runtimeConsole: emit('runtimeConsole'),
|
||||
runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'),
|
||||
runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'),
|
||||
}),
|
||||
browserChannel.register(worker.id(), {
|
||||
evaluate: (options) => this._workerRuntime.send('evaluate', options),
|
||||
callFunction: (options) => this._workerRuntime.send('callFunction', options),
|
||||
getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options),
|
||||
disposeObject: (options) => this._workerRuntime.send('disposeObject', options),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._workerRuntime.dispose();
|
||||
this._browserWorker.dispose();
|
||||
helper.removeListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
class PageAgent {
|
||||
constructor(browserChannel, frameTree) {
|
||||
this._browserChannel = browserChannel;
|
||||
this._browserPage = browserChannel.connect('page');
|
||||
this._frameTree = frameTree;
|
||||
this._runtime = frameTree.runtime();
|
||||
|
||||
this._workerData = new Map();
|
||||
|
||||
const docShell = frameTree.mainFrame().docShell();
|
||||
this._docShell = docShell;
|
||||
|
||||
// Dispatch frameAttached events for all initial frames
|
||||
for (const frame of this._frameTree.frames()) {
|
||||
this._onFrameAttached(frame);
|
||||
if (frame.url())
|
||||
this._onNavigationCommitted(frame);
|
||||
if (frame.pendingNavigationId())
|
||||
this._onNavigationStarted(frame);
|
||||
}
|
||||
|
||||
// Report created workers.
|
||||
for (const worker of this._frameTree.workers())
|
||||
this._onWorkerCreated(worker);
|
||||
|
||||
// Report execution contexts.
|
||||
this._browserPage.emit('runtimeExecutionContextsCleared', {});
|
||||
for (const context of this._runtime.executionContexts())
|
||||
this._onExecutionContextCreated(context);
|
||||
|
||||
if (this._frameTree.isPageReady()) {
|
||||
this._browserPage.emit('pageReady', {});
|
||||
const mainFrame = this._frameTree.mainFrame();
|
||||
const domWindow = mainFrame.domWindow();
|
||||
const document = domWindow ? domWindow.document : null;
|
||||
const readyState = document ? document.readyState : null;
|
||||
// Sometimes we initialize later than the first about:blank page is opened.
|
||||
// In this case, the page might've been loaded already, and we need to issue
|
||||
// the `DOMContentLoaded` and `load` events.
|
||||
if (mainFrame.url() === 'about:blank' && readyState === 'complete')
|
||||
this._emitAllEvents(this._frameTree.mainFrame());
|
||||
}
|
||||
|
||||
this._eventListeners = [
|
||||
helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'),
|
||||
helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'),
|
||||
helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'),
|
||||
helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
|
||||
helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
|
||||
helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
|
||||
helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
|
||||
helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
|
||||
helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
|
||||
helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
|
||||
helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
|
||||
helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})),
|
||||
helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)),
|
||||
helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)),
|
||||
helper.on(this._frameTree, 'websocketcreated', event => this._browserPage.emit('webSocketCreated', event)),
|
||||
helper.on(this._frameTree, 'websocketopened', event => this._browserPage.emit('webSocketOpened', event)),
|
||||
helper.on(this._frameTree, 'websocketframesent', event => this._browserPage.emit('webSocketFrameSent', event)),
|
||||
helper.on(this._frameTree, 'websocketframereceived', event => this._browserPage.emit('webSocketFrameReceived', event)),
|
||||
helper.on(this._frameTree, 'websocketclosed', event => this._browserPage.emit('webSocketClosed', event)),
|
||||
helper.on(this._frameTree, 'inputevent', inputEvent => {
|
||||
this._browserPage.emit('pageInputEvent', inputEvent);
|
||||
if (inputEvent.type === 'dragstart') {
|
||||
// After the dragStart event is dispatched and handled by Web,
|
||||
// it might or might not create a new drag session, depending on its preventing default.
|
||||
setTimeout(() => {
|
||||
this._browserPage.emit('pageInputEvent', { type: 'juggler-drag-finalized', dragSessionStarted: !!dragService.getCurrentSession() });
|
||||
}, 0);
|
||||
}
|
||||
}),
|
||||
helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
|
||||
this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
|
||||
const frame = this._frameTree.frameForDocShell(domWindow.docShell);
|
||||
if (!frame)
|
||||
return;
|
||||
this._browserPage.emit('pageUncaughtError', {
|
||||
frameId: frame.id(),
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
}),
|
||||
this._runtime.events.onConsoleMessage(msg => this._browserPage.emit('runtimeConsole', msg)),
|
||||
this._runtime.events.onRuntimeError(this._onRuntimeError.bind(this)),
|
||||
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
|
||||
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
|
||||
this._runtime.events.onBindingCalled(this._onBindingCalled.bind(this)),
|
||||
browserChannel.register('page', {
|
||||
adoptNode: this._adoptNode.bind(this),
|
||||
crash: this._crash.bind(this),
|
||||
describeNode: this._describeNode.bind(this),
|
||||
dispatchKeyEvent: this._dispatchKeyEvent.bind(this),
|
||||
dispatchDragEvent: this._dispatchDragEvent.bind(this),
|
||||
dispatchTouchEvent: this._dispatchTouchEvent.bind(this),
|
||||
dispatchTapEvent: this._dispatchTapEvent.bind(this),
|
||||
getContentQuads: this._getContentQuads.bind(this),
|
||||
getFullAXTree: this._getFullAXTree.bind(this),
|
||||
insertText: this._insertText.bind(this),
|
||||
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
|
||||
setFileInputFiles: this._setFileInputFiles.bind(this),
|
||||
evaluate: this._runtime.evaluate.bind(this._runtime),
|
||||
callFunction: this._runtime.callFunction.bind(this._runtime),
|
||||
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
|
||||
disposeObject: this._runtime.disposeObject.bind(this._runtime),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
_emitAllEvents(frame) {
|
||||
this._browserPage.emit('pageEventFired', {
|
||||
frameId: frame.id(),
|
||||
name: 'DOMContentLoaded',
|
||||
});
|
||||
this._browserPage.emit('pageEventFired', {
|
||||
frameId: frame.id(),
|
||||
name: 'load',
|
||||
});
|
||||
}
|
||||
|
||||
_onExecutionContextCreated(executionContext) {
|
||||
this._browserPage.emit('runtimeExecutionContextCreated', {
|
||||
executionContextId: executionContext.id(),
|
||||
auxData: executionContext.auxData(),
|
||||
});
|
||||
}
|
||||
|
||||
_onExecutionContextDestroyed(executionContext) {
|
||||
this._browserPage.emit('runtimeExecutionContextDestroyed', {
|
||||
executionContextId: executionContext.id(),
|
||||
});
|
||||
}
|
||||
|
||||
_onWorkerCreated(worker) {
|
||||
const workerData = new WorkerData(this, this._browserChannel, worker);
|
||||
this._workerData.set(worker.id(), workerData);
|
||||
this._browserPage.emit('pageWorkerCreated', {
|
||||
workerId: worker.id(),
|
||||
frameId: worker.frame().id(),
|
||||
url: worker.url(),
|
||||
});
|
||||
}
|
||||
|
||||
_onWorkerDestroyed(worker) {
|
||||
const workerData = this._workerData.get(worker.id());
|
||||
if (!workerData)
|
||||
return;
|
||||
this._workerData.delete(worker.id());
|
||||
workerData.dispose();
|
||||
this._browserPage.emit('pageWorkerDestroyed', {
|
||||
workerId: worker.id(),
|
||||
});
|
||||
}
|
||||
|
||||
_onWindowOpen(subject) {
|
||||
if (!(subject instanceof Ci.nsIPropertyBag2))
|
||||
return;
|
||||
const props = subject.QueryInterface(Ci.nsIPropertyBag2);
|
||||
const hasUrl = props.hasKey('url');
|
||||
const createdDocShell = props.getPropertyAsInterface('createdTabDocShell', Ci.nsIDocShell);
|
||||
if (!hasUrl && createdDocShell === this._docShell && this._frameTree.forcePageReady())
|
||||
this._emitAllEvents(this._frameTree.mainFrame());
|
||||
}
|
||||
|
||||
_linkClicked(sync, anchorElement) {
|
||||
if (anchorElement.ownerGlobal.docShell !== this._docShell)
|
||||
return;
|
||||
this._browserPage.emit('pageLinkClicked', { phase: sync ? 'after' : 'before' });
|
||||
}
|
||||
|
||||
_onWindowOpenInNewContext(docShell) {
|
||||
// TODO: unify this with _onWindowOpen if possible.
|
||||
const frame = this._frameTree.frameForDocShell(docShell);
|
||||
if (!frame)
|
||||
return;
|
||||
this._browserPage.emit('pageWillOpenNewWindowAsynchronously');
|
||||
}
|
||||
|
||||
_filePickerShown(inputElement) {
|
||||
const frame = this._findFrameForNode(inputElement);
|
||||
if (!frame)
|
||||
return;
|
||||
this._browserPage.emit('pageFileChooserOpened', {
|
||||
executionContextId: frame.mainExecutionContext().id(),
|
||||
element: frame.mainExecutionContext().rawValueToRemoteObject(inputElement)
|
||||
});
|
||||
}
|
||||
|
||||
_findFrameForNode(node) {
|
||||
return this._frameTree.frames().find(frame => {
|
||||
const doc = frame.domWindow().document;
|
||||
return node === doc || node.ownerDocument === doc;
|
||||
});
|
||||
}
|
||||
|
||||
onWindowEvent(event) {
|
||||
if (event.type !== 'DOMContentLoaded' && event.type !== 'load')
|
||||
return;
|
||||
if (!event.target.ownerGlobal)
|
||||
return;
|
||||
const docShell = event.target.ownerGlobal.docShell;
|
||||
const frame = this._frameTree.frameForDocShell(docShell);
|
||||
if (!frame)
|
||||
return;
|
||||
this._browserPage.emit('pageEventFired', {
|
||||
frameId: frame.id(),
|
||||
name: event.type,
|
||||
});
|
||||
}
|
||||
|
||||
_onRuntimeError({ executionContext, message, stack }) {
|
||||
this._browserPage.emit('pageUncaughtError', {
|
||||
frameId: executionContext.auxData().frameId,
|
||||
message: message.toString(),
|
||||
stack: stack.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
_onDocumentOpenLoad(document) {
|
||||
const docShell = document.ownerGlobal.docShell;
|
||||
const frame = this._frameTree.frameForDocShell(docShell);
|
||||
if (!frame)
|
||||
return;
|
||||
this._browserPage.emit('pageEventFired', {
|
||||
frameId: frame.id(),
|
||||
name: 'load'
|
||||
});
|
||||
}
|
||||
|
||||
_onNavigationStarted(frame) {
|
||||
this._browserPage.emit('pageNavigationStarted', {
|
||||
frameId: frame.id(),
|
||||
navigationId: frame.pendingNavigationId(),
|
||||
});
|
||||
}
|
||||
|
||||
_onNavigationAborted(frame, navigationId, errorText) {
|
||||
this._browserPage.emit('pageNavigationAborted', {
|
||||
frameId: frame.id(),
|
||||
navigationId,
|
||||
errorText,
|
||||
});
|
||||
if (!frame._initialNavigationDone && frame !== this._frameTree.mainFrame())
|
||||
this._emitAllEvents(frame);
|
||||
frame._initialNavigationDone = true;
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(frame) {
|
||||
this._browserPage.emit('pageSameDocumentNavigation', {
|
||||
frameId: frame.id(),
|
||||
url: frame.url(),
|
||||
});
|
||||
}
|
||||
|
||||
_onNavigationCommitted(frame) {
|
||||
this._browserPage.emit('pageNavigationCommitted', {
|
||||
frameId: frame.id(),
|
||||
navigationId: frame.lastCommittedNavigationId() || undefined,
|
||||
url: frame.url(),
|
||||
name: frame.name(),
|
||||
});
|
||||
frame._initialNavigationDone = true;
|
||||
}
|
||||
|
||||
_onFrameAttached(frame) {
|
||||
this._browserPage.emit('pageFrameAttached', {
|
||||
frameId: frame.id(),
|
||||
parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
_onFrameDetached(frame) {
|
||||
this._browserPage.emit('pageFrameDetached', {
|
||||
frameId: frame.id(),
|
||||
});
|
||||
}
|
||||
|
||||
_onBindingCalled({executionContextId, name, payload}) {
|
||||
this._browserPage.emit('pageBindingCalled', {
|
||||
executionContextId,
|
||||
name,
|
||||
payload
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const workerData of this._workerData.values())
|
||||
workerData.dispose();
|
||||
this._workerData.clear();
|
||||
helper.removeListeners(this._eventListeners);
|
||||
}
|
||||
|
||||
async _adoptNode({frameId, objectId, executionContextId}) {
|
||||
const frame = this._frameTree.frame(frameId);
|
||||
if (!frame)
|
||||
throw new Error('Failed to find frame with id = ' + frameId);
|
||||
let unsafeObject;
|
||||
if (!objectId) {
|
||||
unsafeObject = frame.domWindow().frameElement;
|
||||
} else {
|
||||
unsafeObject = frame.unsafeObject(objectId);
|
||||
}
|
||||
const context = this._runtime.findExecutionContext(executionContextId);
|
||||
const fromPrincipal = unsafeObject.nodePrincipal;
|
||||
const toFrame = this._frameTree.frame(context.auxData().frameId);
|
||||
const toPrincipal = toFrame.domWindow().document.nodePrincipal;
|
||||
if (!toPrincipal.subsumes(fromPrincipal))
|
||||
return { remoteObject: null };
|
||||
return { remoteObject: context.rawValueToRemoteObject(unsafeObject) };
|
||||
}
|
||||
|
||||
async _setFileInputFiles({objectId, frameId, files}) {
|
||||
const frame = this._frameTree.frame(frameId);
|
||||
if (!frame)
|
||||
throw new Error('Failed to find frame with id = ' + frameId);
|
||||
const unsafeObject = frame.unsafeObject(objectId);
|
||||
if (!unsafeObject)
|
||||
throw new Error('Object is not input!');
|
||||
const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
|
||||
unsafeObject.mozSetFileArray(nsFiles);
|
||||
const events = [
|
||||
new (frame.domWindow().Event)('input', { bubbles: true, cancelable: true, composed: true }),
|
||||
new (frame.domWindow().Event)('change', { bubbles: true, cancelable: true, composed: true }),
|
||||
];
|
||||
for (const event of events)
|
||||
unsafeObject.dispatchEvent(event);
|
||||
}
|
||||
|
||||
_getContentQuads({objectId, frameId}) {
|
||||
const frame = this._frameTree.frame(frameId);
|
||||
if (!frame)
|
||||
throw new Error('Failed to find frame with id = ' + frameId);
|
||||
const unsafeObject = frame.unsafeObject(objectId);
|
||||
if (!unsafeObject.getBoxQuads)
|
||||
throw new Error('RemoteObject is not a node');
|
||||
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document, recurseWhenNoFrame: true}).map(quad => {
|
||||
return {
|
||||
p1: {x: quad.p1.x, y: quad.p1.y},
|
||||
p2: {x: quad.p2.x, y: quad.p2.y},
|
||||
p3: {x: quad.p3.x, y: quad.p3.y},
|
||||
p4: {x: quad.p4.x, y: quad.p4.y},
|
||||
};
|
||||
});
|
||||
return {quads};
|
||||
}
|
||||
|
||||
_describeNode({objectId, frameId}) {
|
||||
const frame = this._frameTree.frame(frameId);
|
||||
if (!frame)
|
||||
throw new Error('Failed to find frame with id = ' + frameId);
|
||||
const unsafeObject = frame.unsafeObject(objectId);
|
||||
const browsingContextGroup = frame.docShell().browsingContext.group;
|
||||
const frames = this._frameTree.allFramesInBrowsingContextGroup(browsingContextGroup);
|
||||
let contentFrame;
|
||||
let ownerFrame;
|
||||
for (const frame of frames) {
|
||||
if (unsafeObject.contentWindow && frame.docShell() === unsafeObject.contentWindow.docShell)
|
||||
contentFrame = frame;
|
||||
const document = frame.domWindow().document;
|
||||
if (unsafeObject === document || unsafeObject.ownerDocument === document)
|
||||
ownerFrame = frame;
|
||||
}
|
||||
return {
|
||||
contentFrameId: contentFrame ? contentFrame.id() : undefined,
|
||||
ownerFrameId: ownerFrame ? ownerFrame.id() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async _scrollIntoViewIfNeeded({objectId, frameId, rect}) {
|
||||
const frame = this._frameTree.frame(frameId);
|
||||
if (!frame)
|
||||
throw new Error('Failed to find frame with id = ' + frameId);
|
||||
const unsafeObject = frame.unsafeObject(objectId);
|
||||
if (!unsafeObject.isConnected)
|
||||
throw new Error('Node is detached from document');
|
||||
if (!rect)
|
||||
rect = { x: -1, y: -1, width: -1, height: -1};
|
||||
if (unsafeObject.scrollRectIntoViewIfNeeded)
|
||||
unsafeObject.scrollRectIntoViewIfNeeded(rect.x, rect.y, rect.width, rect.height);
|
||||
else
|
||||
throw new Error('Node does not have a layout object');
|
||||
}
|
||||
|
||||
_getNodeBoundingBox(unsafeObject) {
|
||||
if (!unsafeObject.getBoxQuads)
|
||||
throw new Error('RemoteObject is not a node');
|
||||
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document});
|
||||
if (!quads.length)
|
||||
return;
|
||||
let x1 = Infinity;
|
||||
let y1 = Infinity;
|
||||
let x2 = -Infinity;
|
||||
let y2 = -Infinity;
|
||||
for (const quad of quads) {
|
||||
const boundingBox = quad.getBounds();
|
||||
x1 = Math.min(boundingBox.x, x1);
|
||||
y1 = Math.min(boundingBox.y, y1);
|
||||
x2 = Math.max(boundingBox.x + boundingBox.width, x2);
|
||||
y2 = Math.max(boundingBox.y + boundingBox.height, y2);
|
||||
}
|
||||
return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
|
||||
}
|
||||
|
||||
async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) {
|
||||
const frame = this._frameTree.mainFrame();
|
||||
const tip = frame.textInputProcessor();
|
||||
let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
|
||||
key,
|
||||
code,
|
||||
location,
|
||||
repeat,
|
||||
keyCode
|
||||
});
|
||||
if (type === 'keydown') {
|
||||
if (text && text !== key) {
|
||||
tip.commitCompositionWith(text, keyEvent);
|
||||
} else {
|
||||
const flags = 0;
|
||||
tip.keydown(keyEvent, flags);
|
||||
}
|
||||
} else if (type === 'keyup') {
|
||||
if (text)
|
||||
throw new Error(`keyup does not support text option`);
|
||||
const flags = 0;
|
||||
tip.keyup(keyEvent, flags);
|
||||
} else {
|
||||
throw new Error(`Unknown type ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async _dispatchTouchEvent({type, touchPoints, modifiers}) {
|
||||
const frame = this._frameTree.mainFrame();
|
||||
const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent(
|
||||
type.toLowerCase(),
|
||||
touchPoints.map((point, id) => id),
|
||||
touchPoints.map(point => point.x),
|
||||
touchPoints.map(point => point.y),
|
||||
touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX),
|
||||
touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
|
||||
touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
|
||||
touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
|
||||
touchPoints.map(point => 0),
|
||||
touchPoints.map(point => 0),
|
||||
touchPoints.map(point => 0),
|
||||
modifiers);
|
||||
return {defaultPrevented};
|
||||
}
|
||||
|
||||
async _dispatchTapEvent({x, y, modifiers}) {
|
||||
// Force a layout at the point in question, because touch events
|
||||
// do not seem to trigger one like mouse events.
|
||||
this._frameTree.mainFrame().domWindow().windowUtils.elementFromPoint(
|
||||
x,
|
||||
y,
|
||||
false /* aIgnoreRootScrollFrame */,
|
||||
true /* aFlushLayout */);
|
||||
|
||||
await this._dispatchTouchEvent({
|
||||
type: 'touchstart',
|
||||
modifiers,
|
||||
touchPoints: [{x, y}]
|
||||
});
|
||||
await this._dispatchTouchEvent({
|
||||
type: 'touchend',
|
||||
modifiers,
|
||||
touchPoints: [{x, y}]
|
||||
});
|
||||
}
|
||||
|
||||
async _dispatchDragEvent({type, x, y, modifiers}) {
|
||||
const session = dragService.getCurrentSession();
|
||||
const dropEffect = session.dataTransfer.dropEffect;
|
||||
|
||||
if ((type === 'drop' && dropEffect !== 'none') || type === 'dragover') {
|
||||
const win = this._frameTree.mainFrame().domWindow();
|
||||
win.windowUtils.jugglerSendMouseEvent(
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
0, /*button*/
|
||||
0, /*clickCount*/
|
||||
modifiers,
|
||||
false /*aIgnoreRootScrollFrame*/,
|
||||
0.0 /*pressure*/,
|
||||
0 /*inputSource*/,
|
||||
true /*isDOMEventSynthesized*/,
|
||||
false /*isWidgetEventSynthesized*/,
|
||||
0 /*buttons*/,
|
||||
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
|
||||
false /*disablePointerEvent*/,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (type === 'dragend') {
|
||||
const session = dragService.getCurrentSession();
|
||||
if (session)
|
||||
dragService.endDragSession(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async _insertText({text}) {
|
||||
const frame = this._frameTree.mainFrame();
|
||||
frame.textInputProcessor().commitCompositionWith(text);
|
||||
}
|
||||
|
||||
async _crash() {
|
||||
dump(`Crashing intentionally\n`);
|
||||
// This is to intentionally crash the frame.
|
||||
// We crash by using js-ctypes and dereferencing
|
||||
// a bad pointer. The crash should happen immediately
|
||||
// upon loading this frame script.
|
||||
const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm');
|
||||
ChromeUtils.privateNoteIntentionalCrash();
|
||||
const zero = new ctypes.intptr_t(8);
|
||||
const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
|
||||
badptr.contents;
|
||||
}
|
||||
|
||||
async _getFullAXTree({objectId}) {
|
||||
let unsafeObject = null;
|
||||
if (objectId) {
|
||||
unsafeObject = this._frameTree.mainFrame().unsafeObject(objectId);
|
||||
if (!unsafeObject)
|
||||
throw new Error(`No object found for id "${objectId}"`);
|
||||
}
|
||||
|
||||
const service = Cc["@mozilla.org/accessibilityService;1"]
|
||||
.getService(Ci.nsIAccessibilityService);
|
||||
const document = this._frameTree.mainFrame().domWindow().document;
|
||||
const docAcc = service.getAccessibleFor(document);
|
||||
|
||||
while (docAcc.document.isUpdatePendingForJugglerAccessibility)
|
||||
await new Promise(x => this._frameTree.mainFrame().domWindow().requestAnimationFrame(x));
|
||||
|
||||
async function waitForQuiet() {
|
||||
let state = {};
|
||||
docAcc.getState(state, {});
|
||||
if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0)
|
||||
return;
|
||||
let resolve, reject;
|
||||
const promise = new Promise((x, y) => {resolve = x, reject = y});
|
||||
let eventObserver = {
|
||||
observe(subject, topic) {
|
||||
if (topic !== "accessible-event") {
|
||||
return;
|
||||
}
|
||||
|
||||
// If event type does not match expected type, skip the event.
|
||||
let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
|
||||
if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If event's accessible does not match expected accessible,
|
||||
// skip the event.
|
||||
if (event.accessible !== docAcc) {
|
||||
return;
|
||||
}
|
||||
|
||||
Services.obs.removeObserver(this, "accessible-event");
|
||||
resolve();
|
||||
},
|
||||
};
|
||||
Services.obs.addObserver(eventObserver, "accessible-event");
|
||||
return promise;
|
||||
}
|
||||
function buildNode(accElement) {
|
||||
let a = {}, b = {};
|
||||
accElement.getState(a, b);
|
||||
const tree = {
|
||||
role: service.getStringRole(accElement.role),
|
||||
name: accElement.name || '',
|
||||
};
|
||||
if (unsafeObject && unsafeObject === accElement.DOMNode)
|
||||
tree.foundObject = true;
|
||||
for (const userStringProperty of [
|
||||
'value',
|
||||
'description'
|
||||
]) {
|
||||
tree[userStringProperty] = accElement[userStringProperty] || undefined;
|
||||
}
|
||||
|
||||
const states = {};
|
||||
for (const name of service.getStringStates(a.value, b.value))
|
||||
states[name] = true;
|
||||
for (const name of ['selected',
|
||||
'focused',
|
||||
'pressed',
|
||||
'focusable',
|
||||
'required',
|
||||
'invalid',
|
||||
'modal',
|
||||
'editable',
|
||||
'busy',
|
||||
'checked',
|
||||
'multiselectable']) {
|
||||
if (states[name])
|
||||
tree[name] = true;
|
||||
}
|
||||
|
||||
if (states['multi line'])
|
||||
tree['multiline'] = true;
|
||||
if (states['editable'] && states['readonly'])
|
||||
tree['readonly'] = true;
|
||||
if (states['checked'])
|
||||
tree['checked'] = true;
|
||||
if (states['mixed'])
|
||||
tree['checked'] = 'mixed';
|
||||
if (states['expanded'])
|
||||
tree['expanded'] = true;
|
||||
else if (states['collapsed'])
|
||||
tree['expanded'] = false;
|
||||
if (!states['enabled'])
|
||||
tree['disabled'] = true;
|
||||
|
||||
const attributes = {};
|
||||
if (accElement.attributes) {
|
||||
for (const { key, value } of accElement.attributes.enumerate()) {
|
||||
attributes[key] = value;
|
||||
}
|
||||
}
|
||||
for (const numericalProperty of ['level']) {
|
||||
if (numericalProperty in attributes)
|
||||
tree[numericalProperty] = parseFloat(attributes[numericalProperty]);
|
||||
}
|
||||
for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts', 'haspopup']) {
|
||||
if (stringProperty in attributes)
|
||||
tree[stringProperty] = attributes[stringProperty];
|
||||
}
|
||||
const children = [];
|
||||
|
||||
for (let child = accElement.firstChild; child; child = child.nextSibling) {
|
||||
children.push(buildNode(child));
|
||||
}
|
||||
if (children.length)
|
||||
tree.children = children;
|
||||
return tree;
|
||||
}
|
||||
await waitForQuiet();
|
||||
return {
|
||||
tree: buildNode(docAcc)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = ['PageAgent'];
|
||||
this.PageAgent = PageAgent;
|
||||
|
||||
600
additions/juggler/content/Runtime.js
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
// Note: this file should be loadabale with eval() into worker environment.
|
||||
// Avoid Components.*, ChromeUtils and global const variables.
|
||||
|
||||
if (!this.Debugger) {
|
||||
// Worker has a Debugger defined already.
|
||||
const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {});
|
||||
addDebuggerToGlobal(Components.utils.getGlobalForObject(this));
|
||||
}
|
||||
|
||||
let lastId = 0;
|
||||
function generateId() {
|
||||
return 'id-' + (++lastId);
|
||||
}
|
||||
|
||||
const consoleLevelToProtocolType = {
|
||||
'dir': 'dir',
|
||||
'log': 'log',
|
||||
'debug': 'debug',
|
||||
'info': 'info',
|
||||
'error': 'error',
|
||||
'warn': 'warning',
|
||||
'dirxml': 'dirxml',
|
||||
'table': 'table',
|
||||
'trace': 'trace',
|
||||
'clear': 'clear',
|
||||
'group': 'startGroup',
|
||||
'groupCollapsed': 'startGroupCollapsed',
|
||||
'groupEnd': 'endGroup',
|
||||
'assert': 'assert',
|
||||
'profile': 'profile',
|
||||
'profileEnd': 'profileEnd',
|
||||
'count': 'count',
|
||||
'countReset': 'countReset',
|
||||
'time': null,
|
||||
'timeLog': 'timeLog',
|
||||
'timeEnd': 'timeEnd',
|
||||
'timeStamp': 'timeStamp',
|
||||
};
|
||||
|
||||
const disallowedMessageCategories = new Set([
|
||||
'XPConnect JavaScript',
|
||||
'component javascript',
|
||||
'chrome javascript',
|
||||
'chrome registration',
|
||||
'XBL',
|
||||
'XBL Prototype Handler',
|
||||
'XBL Content Sink',
|
||||
'xbl javascript',
|
||||
]);
|
||||
|
||||
class Runtime {
|
||||
constructor(isWorker = false) {
|
||||
this._debugger = new Debugger();
|
||||
this._pendingPromises = new Map();
|
||||
this._executionContexts = new Map();
|
||||
this._windowToExecutionContext = new Map();
|
||||
this._eventListeners = [];
|
||||
if (isWorker) {
|
||||
this._registerWorkerConsoleHandler();
|
||||
} else {
|
||||
this._registerConsoleServiceListener(Services);
|
||||
this._registerConsoleAPIListener(Services);
|
||||
}
|
||||
// We can't use event listener here to be compatible with Worker Global Context.
|
||||
// Use plain callbacks instead.
|
||||
this.events = {
|
||||
onConsoleMessage: createEvent(),
|
||||
onRuntimeError: createEvent(),
|
||||
onErrorFromWorker: createEvent(),
|
||||
onExecutionContextCreated: createEvent(),
|
||||
onExecutionContextDestroyed: createEvent(),
|
||||
onBindingCalled: createEvent(),
|
||||
};
|
||||
}
|
||||
|
||||
executionContexts() {
|
||||
return [...this._executionContexts.values()];
|
||||
}
|
||||
|
||||
async evaluate({executionContextId, expression, returnByValue}) {
|
||||
const executionContext = this.findExecutionContext(executionContextId);
|
||||
if (!executionContext)
|
||||
throw new Error('Failed to find execution context with id = ' + executionContextId);
|
||||
const exceptionDetails = {};
|
||||
let result = await executionContext.evaluateScript(expression, exceptionDetails);
|
||||
if (!result)
|
||||
return {exceptionDetails};
|
||||
if (returnByValue)
|
||||
result = executionContext.ensureSerializedToValue(result);
|
||||
return {result};
|
||||
}
|
||||
|
||||
async callFunction({executionContextId, functionDeclaration, args, returnByValue}) {
|
||||
const executionContext = this.findExecutionContext(executionContextId);
|
||||
if (!executionContext)
|
||||
throw new Error('Failed to find execution context with id = ' + executionContextId);
|
||||
const exceptionDetails = {};
|
||||
let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails);
|
||||
if (!result)
|
||||
return {exceptionDetails};
|
||||
if (returnByValue)
|
||||
result = executionContext.ensureSerializedToValue(result);
|
||||
return {result};
|
||||
}
|
||||
|
||||
async getObjectProperties({executionContextId, objectId}) {
|
||||
const executionContext = this.findExecutionContext(executionContextId);
|
||||
if (!executionContext)
|
||||
throw new Error('Failed to find execution context with id = ' + executionContextId);
|
||||
return {properties: executionContext.getObjectProperties(objectId)};
|
||||
}
|
||||
|
||||
async disposeObject({executionContextId, objectId}) {
|
||||
const executionContext = this.findExecutionContext(executionContextId);
|
||||
if (!executionContext)
|
||||
throw new Error('Failed to find execution context with id = ' + executionContextId);
|
||||
return executionContext.disposeObject(objectId);
|
||||
}
|
||||
|
||||
_registerConsoleServiceListener(Services) {
|
||||
const Ci = Components.interfaces;
|
||||
const consoleServiceListener = {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]),
|
||||
|
||||
observe: message => {
|
||||
if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID ||
|
||||
!message.category || disallowedMessageCategories.has(message.category)) {
|
||||
return;
|
||||
}
|
||||
const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID);
|
||||
if (message.category === 'Web Worker' && message.logLevel === Ci.nsIConsoleMessage.error) {
|
||||
emitEvent(this.events.onErrorFromWorker, errorWindow, message.message, '' + message.stack);
|
||||
return;
|
||||
}
|
||||
const executionContext = this._windowToExecutionContext.get(errorWindow);
|
||||
if (!executionContext) {
|
||||
return;
|
||||
}
|
||||
const typeNames = {
|
||||
[Ci.nsIConsoleMessage.debug]: 'debug',
|
||||
[Ci.nsIConsoleMessage.info]: 'info',
|
||||
[Ci.nsIConsoleMessage.warn]: 'warn',
|
||||
[Ci.nsIConsoleMessage.error]: 'error',
|
||||
};
|
||||
if (!message.hasException) {
|
||||
emitEvent(this.events.onConsoleMessage, {
|
||||
args: [{
|
||||
value: message.message,
|
||||
}],
|
||||
type: typeNames[message.logLevel],
|
||||
executionContextId: executionContext.id(),
|
||||
location: {
|
||||
lineNumber: message.lineNumber,
|
||||
columnNumber: message.columnNumber,
|
||||
url: message.sourceName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
emitEvent(this.events.onRuntimeError, {
|
||||
executionContext,
|
||||
message: message.errorMessage,
|
||||
stack: message.stack.toString(),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
Services.console.registerListener(consoleServiceListener);
|
||||
this._eventListeners.push(() => Services.console.unregisterListener(consoleServiceListener));
|
||||
}
|
||||
|
||||
_registerConsoleAPIListener(Services) {
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(Ci.nsIConsoleAPIStorage);
|
||||
const onMessage = ({ wrappedJSObject }) => {
|
||||
const executionContext = Array.from(this._executionContexts.values()).find(context => {
|
||||
// There is no easy way to determine isolated world context and we normally don't write
|
||||
// objects to console from utility worlds so we always return main world context here.
|
||||
if (context._isIsolatedWorldContext())
|
||||
return false;
|
||||
const domWindow = context._domWindow;
|
||||
try {
|
||||
// `windowGlobalChild` might be dead already; accessing it will throw an error, message in a console,
|
||||
// and infinite recursion.
|
||||
return domWindow && domWindow.windowGlobalChild.innerWindowId === wrappedJSObject.innerID;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!executionContext)
|
||||
return;
|
||||
this._onConsoleMessage(executionContext, wrappedJSObject);
|
||||
}
|
||||
ConsoleAPIStorage.addLogEventListener(
|
||||
onMessage,
|
||||
Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
|
||||
);
|
||||
this._eventListeners.push(() => ConsoleAPIStorage.removeLogEventListener(onMessage));
|
||||
}
|
||||
|
||||
_registerWorkerConsoleHandler() {
|
||||
setConsoleEventHandler(message => {
|
||||
const executionContext = Array.from(this._executionContexts.values())[0];
|
||||
this._onConsoleMessage(executionContext, message);
|
||||
});
|
||||
this._eventListeners.push(() => setConsoleEventHandler(null));
|
||||
}
|
||||
|
||||
_onConsoleMessage(executionContext, message) {
|
||||
const type = consoleLevelToProtocolType[message.level];
|
||||
if (!type)
|
||||
return;
|
||||
const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
|
||||
emitEvent(this.events.onConsoleMessage, {
|
||||
args,
|
||||
type,
|
||||
executionContextId: executionContext.id(),
|
||||
location: {
|
||||
lineNumber: message.lineNumber - 1,
|
||||
columnNumber: message.columnNumber - 1,
|
||||
url: message.filename,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const tearDown of this._eventListeners)
|
||||
tearDown.call(null);
|
||||
this._eventListeners = [];
|
||||
}
|
||||
|
||||
async _awaitPromise(executionContext, obj, exceptionDetails = {}) {
|
||||
if (obj.promiseState === 'fulfilled')
|
||||
return {success: true, obj: obj.promiseValue};
|
||||
if (obj.promiseState === 'rejected') {
|
||||
const debuggee = executionContext._debuggee;
|
||||
exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
return {success: false, obj: null};
|
||||
}
|
||||
let resolve, reject;
|
||||
const promise = new Promise((a, b) => {
|
||||
resolve = a;
|
||||
reject = b;
|
||||
});
|
||||
this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails});
|
||||
if (this._pendingPromises.size === 1)
|
||||
this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this);
|
||||
return await promise;
|
||||
}
|
||||
|
||||
_onPromiseSettled(obj) {
|
||||
const pendingPromise = this._pendingPromises.get(obj.promiseID);
|
||||
if (!pendingPromise)
|
||||
return;
|
||||
this._pendingPromises.delete(obj.promiseID);
|
||||
if (!this._pendingPromises.size)
|
||||
this._debugger.onPromiseSettled = undefined;
|
||||
|
||||
if (obj.promiseState === 'fulfilled') {
|
||||
pendingPromise.resolve({success: true, obj: obj.promiseValue});
|
||||
return;
|
||||
};
|
||||
const debuggee = pendingPromise.executionContext._debuggee;
|
||||
pendingPromise.exceptionDetails.text = debuggee.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
pendingPromise.exceptionDetails.stack = debuggee.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}, {useInnerBindings: true}).return;
|
||||
pendingPromise.resolve({success: false, obj: null});
|
||||
}
|
||||
|
||||
createExecutionContext(domWindow, contextGlobal, auxData) {
|
||||
// Note: domWindow is null for workers.
|
||||
const context = new ExecutionContext(this, domWindow, contextGlobal, auxData);
|
||||
this._executionContexts.set(context._id, context);
|
||||
if (domWindow)
|
||||
this._windowToExecutionContext.set(domWindow, context);
|
||||
emitEvent(this.events.onExecutionContextCreated, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
findExecutionContext(executionContextId) {
|
||||
const executionContext = this._executionContexts.get(executionContextId);
|
||||
if (!executionContext)
|
||||
throw new Error('Failed to find execution context with id = ' + executionContextId);
|
||||
return executionContext;
|
||||
}
|
||||
|
||||
destroyExecutionContext(destroyedContext) {
|
||||
for (const [promiseID, {reject, executionContext}] of this._pendingPromises) {
|
||||
if (executionContext === destroyedContext) {
|
||||
reject(new Error('Execution context was destroyed!'));
|
||||
this._pendingPromises.delete(promiseID);
|
||||
}
|
||||
}
|
||||
if (!this._pendingPromises.size)
|
||||
this._debugger.onPromiseSettled = undefined;
|
||||
this._debugger.removeDebuggee(destroyedContext._contextGlobal);
|
||||
this._executionContexts.delete(destroyedContext._id);
|
||||
if (destroyedContext._domWindow)
|
||||
this._windowToExecutionContext.delete(destroyedContext._domWindow);
|
||||
emitEvent(this.events.onExecutionContextDestroyed, destroyedContext);
|
||||
}
|
||||
}
|
||||
|
||||
class ExecutionContext {
|
||||
constructor(runtime, domWindow, contextGlobal, auxData) {
|
||||
this._runtime = runtime;
|
||||
this._domWindow = domWindow;
|
||||
this._contextGlobal = contextGlobal;
|
||||
this._debuggee = runtime._debugger.addDebuggee(contextGlobal);
|
||||
this._remoteObjects = new Map();
|
||||
this._id = generateId();
|
||||
this._auxData = auxData;
|
||||
this._jsonStringifyObject = this._debuggee.executeInGlobal(`((stringify, object) => {
|
||||
const oldToJSON = Date.prototype?.toJSON;
|
||||
if (oldToJSON)
|
||||
Date.prototype.toJSON = undefined;
|
||||
const oldArrayToJSON = Array.prototype.toJSON;
|
||||
const oldArrayHadToJSON = Array.prototype.hasOwnProperty('toJSON');
|
||||
if (oldArrayHadToJSON)
|
||||
Array.prototype.toJSON = undefined;
|
||||
|
||||
let hasSymbol = false;
|
||||
const result = stringify(object, (key, value) => {
|
||||
if (typeof value === 'symbol')
|
||||
hasSymbol = true;
|
||||
return value;
|
||||
});
|
||||
|
||||
if (oldToJSON)
|
||||
Date.prototype.toJSON = oldToJSON;
|
||||
if (oldArrayHadToJSON)
|
||||
Array.prototype.toJSON = oldArrayToJSON;
|
||||
|
||||
return hasSymbol ? undefined : result;
|
||||
}).bind(null, JSON.stringify.bind(JSON))`).return;
|
||||
}
|
||||
|
||||
id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
auxData() {
|
||||
return this._auxData;
|
||||
}
|
||||
|
||||
_isIsolatedWorldContext() {
|
||||
return !!this._auxData.name;
|
||||
}
|
||||
|
||||
async evaluateScript(script, exceptionDetails = {}) {
|
||||
const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
|
||||
if (this._domWindow && this._domWindow.document)
|
||||
this._domWindow.document.notifyUserGestureActivation();
|
||||
|
||||
let {success, obj} = this._getResult(this._debuggee.executeInGlobal(script), exceptionDetails);
|
||||
userInputHelper && userInputHelper.destruct();
|
||||
if (!success)
|
||||
return null;
|
||||
if (obj && obj.isPromise) {
|
||||
const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
|
||||
if (!awaitResult.success)
|
||||
return null;
|
||||
obj = awaitResult.obj;
|
||||
}
|
||||
return this._createRemoteObject(obj);
|
||||
}
|
||||
|
||||
evaluateScriptSafely(script) {
|
||||
try {
|
||||
this._debuggee.executeInGlobal(script);
|
||||
} catch (e) {
|
||||
dump(`WARNING: ${e.message}\n${e.stack}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async evaluateFunction(functionText, args, exceptionDetails = {}) {
|
||||
const funEvaluation = this._getResult(this._debuggee.executeInGlobal('(' + functionText + ')'), exceptionDetails);
|
||||
if (!funEvaluation.success)
|
||||
return null;
|
||||
if (!funEvaluation.obj.callable)
|
||||
throw new Error('functionText does not evaluate to a function!');
|
||||
args = args.map(arg => {
|
||||
if (arg.objectId) {
|
||||
if (!this._remoteObjects.has(arg.objectId))
|
||||
throw new Error('Cannot find object with id = ' + arg.objectId);
|
||||
return this._remoteObjects.get(arg.objectId);
|
||||
}
|
||||
switch (arg.unserializableValue) {
|
||||
case 'Infinity': return Infinity;
|
||||
case '-Infinity': return -Infinity;
|
||||
case '-0': return -0;
|
||||
case 'NaN': return NaN;
|
||||
default: return this._toDebugger(arg.value);
|
||||
}
|
||||
});
|
||||
const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
|
||||
if (this._domWindow && this._domWindow.document)
|
||||
this._domWindow.document.notifyUserGestureActivation();
|
||||
let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails);
|
||||
userInputHelper && userInputHelper.destruct();
|
||||
if (!success)
|
||||
return null;
|
||||
if (obj && obj.isPromise) {
|
||||
const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
|
||||
if (!awaitResult.success)
|
||||
return null;
|
||||
obj = awaitResult.obj;
|
||||
}
|
||||
return this._createRemoteObject(obj);
|
||||
}
|
||||
|
||||
addBinding(name, script) {
|
||||
Cu.exportFunction((...args) => {
|
||||
emitEvent(this._runtime.events.onBindingCalled, {
|
||||
executionContextId: this._id,
|
||||
name,
|
||||
payload: args[0],
|
||||
});
|
||||
}, this._contextGlobal, {
|
||||
defineAs: name,
|
||||
});
|
||||
this.evaluateScriptSafely(script);
|
||||
}
|
||||
|
||||
unsafeObject(objectId) {
|
||||
if (!this._remoteObjects.has(objectId))
|
||||
return;
|
||||
return { object: this._remoteObjects.get(objectId).unsafeDereference() };
|
||||
}
|
||||
|
||||
rawValueToRemoteObject(rawValue) {
|
||||
const debuggerObj = this._debuggee.makeDebuggeeValue(rawValue);
|
||||
return this._createRemoteObject(debuggerObj);
|
||||
}
|
||||
|
||||
_instanceOf(debuggerObj, rawObj, className) {
|
||||
if (this._domWindow)
|
||||
return rawObj instanceof this._domWindow[className];
|
||||
return this._debuggee.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._debuggee.makeDebuggeeValue(className)}, {useInnerBindings: true}).return;
|
||||
}
|
||||
|
||||
_createRemoteObject(debuggerObj) {
|
||||
if (debuggerObj instanceof Debugger.Object) {
|
||||
const objectId = generateId();
|
||||
this._remoteObjects.set(objectId, debuggerObj);
|
||||
const rawObj = debuggerObj.unsafeDereference();
|
||||
const type = typeof rawObj;
|
||||
let subtype = undefined;
|
||||
if (debuggerObj.isProxy)
|
||||
subtype = 'proxy';
|
||||
else if (Array.isArray(rawObj))
|
||||
subtype = 'array';
|
||||
else if (Object.is(rawObj, null))
|
||||
subtype = 'null';
|
||||
else if (typeof Node !== 'undefined' && Node.isInstance(rawObj))
|
||||
subtype = 'node';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'RegExp'))
|
||||
subtype = 'regexp';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'Date'))
|
||||
subtype = 'date';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'Map'))
|
||||
subtype = 'map';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'Set'))
|
||||
subtype = 'set';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap'))
|
||||
subtype = 'weakmap';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet'))
|
||||
subtype = 'weakset';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'Error'))
|
||||
subtype = 'error';
|
||||
else if (this._instanceOf(debuggerObj, rawObj, 'Promise'))
|
||||
subtype = 'promise';
|
||||
else if ((this._instanceOf(debuggerObj, rawObj, 'Int8Array')) || (this._instanceOf(debuggerObj, rawObj, 'Uint8Array')) ||
|
||||
(this._instanceOf(debuggerObj, rawObj, 'Uint8ClampedArray')) || (this._instanceOf(debuggerObj, rawObj, 'Int16Array')) ||
|
||||
(this._instanceOf(debuggerObj, rawObj, 'Uint16Array')) || (this._instanceOf(debuggerObj, rawObj, 'Int32Array')) ||
|
||||
(this._instanceOf(debuggerObj, rawObj, 'Uint32Array')) || (this._instanceOf(debuggerObj, rawObj, 'Float32Array')) ||
|
||||
(this._instanceOf(debuggerObj, rawObj, 'Float64Array'))) {
|
||||
subtype = 'typedarray';
|
||||
}
|
||||
return {objectId, type, subtype};
|
||||
}
|
||||
if (typeof debuggerObj === 'symbol') {
|
||||
const objectId = generateId();
|
||||
this._remoteObjects.set(objectId, debuggerObj);
|
||||
return {objectId, type: 'symbol'};
|
||||
}
|
||||
|
||||
let unserializableValue = undefined;
|
||||
if (Object.is(debuggerObj, NaN))
|
||||
unserializableValue = 'NaN';
|
||||
else if (Object.is(debuggerObj, -0))
|
||||
unserializableValue = '-0';
|
||||
else if (Object.is(debuggerObj, Infinity))
|
||||
unserializableValue = 'Infinity';
|
||||
else if (Object.is(debuggerObj, -Infinity))
|
||||
unserializableValue = '-Infinity';
|
||||
return unserializableValue ? {unserializableValue} : {value: debuggerObj};
|
||||
}
|
||||
|
||||
ensureSerializedToValue(protocolObject) {
|
||||
if (!protocolObject.objectId)
|
||||
return protocolObject;
|
||||
const obj = this._remoteObjects.get(protocolObject.objectId);
|
||||
this._remoteObjects.delete(protocolObject.objectId);
|
||||
return {value: this._serialize(obj)};
|
||||
}
|
||||
|
||||
_toDebugger(obj) {
|
||||
if (typeof obj !== 'object')
|
||||
return obj;
|
||||
if (obj === null)
|
||||
return obj;
|
||||
const properties = {};
|
||||
for (let [key, value] of Object.entries(obj)) {
|
||||
properties[key] = {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
value: this._toDebugger(value),
|
||||
};
|
||||
}
|
||||
const baseObject = Array.isArray(obj) ? '([])' : '({})';
|
||||
const debuggerObj = this._debuggee.executeInGlobal(baseObject).return;
|
||||
debuggerObj.defineProperties(properties);
|
||||
return debuggerObj;
|
||||
}
|
||||
|
||||
_serialize(obj) {
|
||||
const result = this._debuggee.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject}, {useInnerBindings: true});
|
||||
if (result.throw)
|
||||
throw new Error('Object is not serializable');
|
||||
return result.return === undefined ? undefined : JSON.parse(result.return);
|
||||
}
|
||||
|
||||
disposeObject(objectId) {
|
||||
this._remoteObjects.delete(objectId);
|
||||
}
|
||||
|
||||
getObjectProperties(objectId) {
|
||||
if (!this._remoteObjects.has(objectId))
|
||||
throw new Error('Cannot find object with id = ' + arg.objectId);
|
||||
const result = [];
|
||||
for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) {
|
||||
for (const propertyName of obj.getOwnPropertyNames()) {
|
||||
const descriptor = obj.getOwnPropertyDescriptor(propertyName);
|
||||
if (!descriptor.enumerable)
|
||||
continue;
|
||||
result.push({
|
||||
name: propertyName,
|
||||
value: this._createRemoteObject(descriptor.value),
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_getResult(completionValue, exceptionDetails = {}) {
|
||||
if (!completionValue)
|
||||
throw new Error('evaluation terminated');
|
||||
if (completionValue.throw) {
|
||||
if (this._debuggee.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}, {useInnerBindings: true}).return) {
|
||||
exceptionDetails.text = this._debuggee.executeInGlobalWithBindings('e.message', {e: completionValue.throw}, {useInnerBindings: true}).return;
|
||||
exceptionDetails.stack = this._debuggee.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}, {useInnerBindings: true}).return;
|
||||
} else {
|
||||
exceptionDetails.value = this._serialize(completionValue.throw);
|
||||
}
|
||||
return {success: false, obj: null};
|
||||
}
|
||||
return {success: true, obj: completionValue.return};
|
||||
}
|
||||
}
|
||||
|
||||
const listenersSymbol = Symbol('listeners');
|
||||
|
||||
function createEvent() {
|
||||
const listeners = new Set();
|
||||
const subscribeFunction = listener => {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
subscribeFunction[listenersSymbol] = listeners;
|
||||
return subscribeFunction;
|
||||
}
|
||||
|
||||
function emitEvent(event, ...args) {
|
||||
let listeners = event[listenersSymbol];
|
||||
if (!listeners || !listeners.size)
|
||||
return;
|
||||
listeners = new Set(listeners);
|
||||
for (const listener of listeners)
|
||||
listener.call(null, ...args);
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = ['Runtime'];
|
||||
this.Runtime = Runtime;
|
||||
78
additions/juggler/content/WorkerMain.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
loadSubScript('chrome://juggler/content/content/Runtime.js');
|
||||
loadSubScript('chrome://juggler/content/SimpleChannel.js');
|
||||
|
||||
// SimpleChannel in worker is never replaced: its lifetime matches the lifetime
|
||||
// of the worker itself, so anything would work as a unique identifier.
|
||||
const channel = new SimpleChannel('worker::content', 'unique_identifier');
|
||||
const eventListener = event => channel._onMessage(JSON.parse(event.data));
|
||||
this.addEventListener('message', eventListener);
|
||||
channel.setTransport({
|
||||
sendMessage: msg => postMessage(JSON.stringify(msg)),
|
||||
dispose: () => this.removeEventListener('message', eventListener),
|
||||
});
|
||||
|
||||
const runtime = new Runtime(true /* isWorker */);
|
||||
|
||||
(() => {
|
||||
// Create execution context in the runtime only when the script
|
||||
// source was actually evaluated in it.
|
||||
const dbg = new Debugger(global);
|
||||
if (dbg.findScripts({global}).length) {
|
||||
runtime.createExecutionContext(null /* domWindow */, global, {});
|
||||
} else {
|
||||
dbg.onNewScript = function(s) {
|
||||
dbg.onNewScript = undefined;
|
||||
dbg.removeAllDebuggees();
|
||||
runtime.createExecutionContext(null /* domWindow */, global, {});
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
class RuntimeAgent {
|
||||
constructor(runtime, channel) {
|
||||
this._runtime = runtime;
|
||||
this._browserRuntime = channel.connect('runtime');
|
||||
|
||||
for (const context of this._runtime.executionContexts())
|
||||
this._onExecutionContextCreated(context);
|
||||
|
||||
this._eventListeners = [
|
||||
this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
|
||||
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
|
||||
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
|
||||
channel.register('runtime', {
|
||||
evaluate: this._runtime.evaluate.bind(this._runtime),
|
||||
callFunction: this._runtime.callFunction.bind(this._runtime),
|
||||
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
|
||||
disposeObject: this._runtime.disposeObject.bind(this._runtime),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
_onExecutionContextCreated(executionContext) {
|
||||
this._browserRuntime.emit('runtimeExecutionContextCreated', {
|
||||
executionContextId: executionContext.id(),
|
||||
auxData: executionContext.auxData(),
|
||||
});
|
||||
}
|
||||
|
||||
_onExecutionContextDestroyed(executionContext) {
|
||||
this._browserRuntime.emit('runtimeExecutionContextDestroyed', {
|
||||
executionContextId: executionContext.id(),
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const disposer of this._eventListeners)
|
||||
disposer();
|
||||
this._eventListeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
new RuntimeAgent(runtime, channel);
|
||||
|
||||
7
additions/juggler/content/hidden-scrollbars.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
* {
|
||||
scrollbar-width: none !important;
|
||||
}
|
||||
138
additions/juggler/content/main.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
|
||||
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
||||
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
|
||||
|
||||
const browsingContextToAgents = new Map();
|
||||
const helper = new Helper();
|
||||
|
||||
function initialize(browsingContext, docShell, actor) {
|
||||
if (browsingContext.parent) {
|
||||
// For child frames, return agents from the main frame.
|
||||
return browsingContextToAgents.get(browsingContext.top);
|
||||
}
|
||||
|
||||
let data = browsingContextToAgents.get(browsingContext);
|
||||
if (data) {
|
||||
// Rebind from one main frame actor to another one.
|
||||
data.channel.bindToActor(actor);
|
||||
return data;
|
||||
}
|
||||
|
||||
data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false };
|
||||
browsingContextToAgents.set(browsingContext, data);
|
||||
|
||||
const applySetting = {
|
||||
geolocation: (geolocation) => {
|
||||
if (geolocation) {
|
||||
docShell.setGeolocationOverride({
|
||||
coords: {
|
||||
latitude: geolocation.latitude,
|
||||
longitude: geolocation.longitude,
|
||||
accuracy: geolocation.accuracy,
|
||||
altitude: NaN,
|
||||
altitudeAccuracy: NaN,
|
||||
heading: NaN,
|
||||
speed: NaN,
|
||||
},
|
||||
address: null,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} else {
|
||||
docShell.setGeolocationOverride(null);
|
||||
}
|
||||
},
|
||||
|
||||
bypassCSP: (bypassCSP) => {
|
||||
docShell.bypassCSPEnabled = bypassCSP;
|
||||
},
|
||||
|
||||
timezoneId: (timezoneId) => {
|
||||
data.failedToOverrideTimezone = !docShell.overrideTimezone(timezoneId);
|
||||
},
|
||||
|
||||
locale: (locale) => {
|
||||
docShell.languageOverride = locale;
|
||||
},
|
||||
|
||||
scrollbarsHidden: (hidden) => {
|
||||
data.frameTree.setScrollbarsHidden(hidden);
|
||||
},
|
||||
|
||||
javaScriptDisabled: (javaScriptDisabled) => {
|
||||
data.frameTree.setJavaScriptDisabled(javaScriptDisabled);
|
||||
},
|
||||
};
|
||||
|
||||
const contextCrossProcessCookie = Services.cpmm.sharedData.get('juggler:context-cookie-' + browsingContext.originAttributes.userContextId) || { initScripts: [], bindings: [], settings: {} };
|
||||
const pageCrossProcessCookie = Services.cpmm.sharedData.get('juggler:page-cookie-' + browsingContext.browserId) || { initScripts: [], bindings: [], interceptFileChooserDialog: false };
|
||||
|
||||
// Enforce focused state for all top level documents.
|
||||
docShell.overrideHasFocus = true;
|
||||
docShell.forceActiveState = true;
|
||||
docShell.disallowBFCache = true;
|
||||
data.frameTree = new FrameTree(browsingContext);
|
||||
for (const [name, value] of Object.entries(contextCrossProcessCookie.settings)) {
|
||||
if (value !== undefined)
|
||||
applySetting[name](value);
|
||||
}
|
||||
for (const { worldName, name, script } of [...contextCrossProcessCookie.bindings, ...pageCrossProcessCookie.bindings])
|
||||
data.frameTree.addBinding(worldName, name, script);
|
||||
data.frameTree.setInitScripts([...contextCrossProcessCookie.initScripts, ...pageCrossProcessCookie.initScripts]);
|
||||
data.channel = new SimpleChannel('', 'process-' + Services.appinfo.processID);
|
||||
data.channel.bindToActor(actor);
|
||||
data.pageAgent = new PageAgent(data.channel, data.frameTree);
|
||||
docShell.fileInputInterceptionEnabled = !!pageCrossProcessCookie.interceptFileChooserDialog;
|
||||
|
||||
data.channel.register('', {
|
||||
setInitScripts(scripts) {
|
||||
data.frameTree.setInitScripts(scripts);
|
||||
},
|
||||
|
||||
addBinding({worldName, name, script}) {
|
||||
data.frameTree.addBinding(worldName, name, script);
|
||||
},
|
||||
|
||||
applyContextSetting({name, value}) {
|
||||
applySetting[name](value);
|
||||
},
|
||||
|
||||
setInterceptFileChooserDialog(enabled) {
|
||||
docShell.fileInputInterceptionEnabled = !!enabled;
|
||||
},
|
||||
|
||||
ensurePermissions() {
|
||||
// noop, just a rountrip.
|
||||
},
|
||||
|
||||
hasFailedToOverrideTimezone() {
|
||||
return data.failedToOverrideTimezone;
|
||||
},
|
||||
|
||||
async awaitViewportDimensions({width, height}) {
|
||||
const win = docShell.domWindow;
|
||||
if (win.innerWidth === width && win.innerHeight === height)
|
||||
return;
|
||||
await new Promise(resolve => {
|
||||
const listener = helper.addEventListener(win, 'resize', () => {
|
||||
if (win.innerWidth === width && win.innerHeight === height) {
|
||||
helper.removeListeners([listener]);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
dispose() {
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = ['initialize'];
|
||||
this.initialize = initialize;
|
||||
27
additions/juggler/jar.mn
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
juggler.jar:
|
||||
% content juggler %content/
|
||||
|
||||
content/components/Juggler.js (components/Juggler.js)
|
||||
|
||||
content/Helper.js (Helper.js)
|
||||
content/NetworkObserver.js (NetworkObserver.js)
|
||||
content/TargetRegistry.js (TargetRegistry.js)
|
||||
content/SimpleChannel.js (SimpleChannel.js)
|
||||
content/JugglerFrameParent.jsm (JugglerFrameParent.jsm)
|
||||
content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js)
|
||||
content/protocol/Protocol.js (protocol/Protocol.js)
|
||||
content/protocol/Dispatcher.js (protocol/Dispatcher.js)
|
||||
content/protocol/PageHandler.js (protocol/PageHandler.js)
|
||||
content/protocol/BrowserHandler.js (protocol/BrowserHandler.js)
|
||||
content/content/JugglerFrameChild.jsm (content/JugglerFrameChild.jsm)
|
||||
content/content/main.js (content/main.js)
|
||||
content/content/FrameTree.js (content/FrameTree.js)
|
||||
content/content/PageAgent.js (content/PageAgent.js)
|
||||
content/content/Runtime.js (content/Runtime.js)
|
||||
content/content/WorkerMain.js (content/WorkerMain.js)
|
||||
content/content/hidden-scrollbars.css (content/hidden-scrollbars.css)
|
||||
|
||||
10
additions/juggler/moz.build
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += ["components", "screencast", "pipe"]
|
||||
|
||||
JAR_MANIFESTS += ["jar.mn"]
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Testing", "Juggler")
|
||||
|
||||
15
additions/juggler/pipe/components.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
Classes = [
|
||||
{
|
||||
'cid': '{d69ecefe-3df7-4d11-9dc7-f604edb96da2}',
|
||||
'contract_ids': ['@mozilla.org/juggler/remotedebuggingpipe;1'],
|
||||
'type': 'nsIRemoteDebuggingPipe',
|
||||
'constructor': 'mozilla::nsRemoteDebuggingPipe::GetSingleton',
|
||||
'headers': ['/juggler/pipe/nsRemoteDebuggingPipe.h'],
|
||||
},
|
||||
]
|
||||
24
additions/juggler/pipe/moz.build
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
XPIDL_SOURCES += [
|
||||
'nsIRemoteDebuggingPipe.idl',
|
||||
]
|
||||
|
||||
XPIDL_MODULE = 'jugglerpipe'
|
||||
|
||||
SOURCES += [
|
||||
'nsRemoteDebuggingPipe.cpp',
|
||||
]
|
||||
|
||||
XPCOM_MANIFESTS += [
|
||||
'components.conf',
|
||||
]
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
20
additions/juggler/pipe/nsIRemoteDebuggingPipe.idl
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
[scriptable, uuid(7910c231-971a-4653-abdc-a8599a986c4c)]
|
||||
interface nsIRemoteDebuggingPipeClient : nsISupports
|
||||
{
|
||||
void receiveMessage(in AString message);
|
||||
void disconnected();
|
||||
};
|
||||
|
||||
[scriptable, uuid(b7bfb66b-fd46-4aa2-b4ad-396177186d94)]
|
||||
interface nsIRemoteDebuggingPipe : nsISupports
|
||||
{
|
||||
void init(in nsIRemoteDebuggingPipeClient client);
|
||||
void sendMessage(in AString message);
|
||||
void stop();
|
||||
};
|
||||
223
additions/juggler/pipe/nsRemoteDebuggingPipe.cpp
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsRemoteDebuggingPipe.h"
|
||||
|
||||
#include <cstring>
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
#include "mozilla/StaticPtr.h"
|
||||
#include "nsISupportsPrimitives.h"
|
||||
#include "nsThreadUtils.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
NS_IMPL_ISUPPORTS(nsRemoteDebuggingPipe, nsIRemoteDebuggingPipe)
|
||||
|
||||
namespace {
|
||||
|
||||
StaticRefPtr<nsRemoteDebuggingPipe> gPipe;
|
||||
|
||||
const size_t kWritePacketSize = 1 << 16;
|
||||
|
||||
#if defined(_WIN32)
|
||||
HANDLE readHandle;
|
||||
HANDLE writeHandle;
|
||||
#else
|
||||
const int readFD = 3;
|
||||
const int writeFD = 4;
|
||||
#endif
|
||||
|
||||
size_t ReadBytes(void* buffer, size_t size, bool exact_size)
|
||||
{
|
||||
size_t bytesRead = 0;
|
||||
while (bytesRead < size) {
|
||||
#if defined(_WIN32)
|
||||
DWORD sizeRead = 0;
|
||||
bool hadError = !ReadFile(readHandle, static_cast<char*>(buffer) + bytesRead,
|
||||
size - bytesRead, &sizeRead, nullptr);
|
||||
#else
|
||||
int sizeRead = read(readFD, static_cast<char*>(buffer) + bytesRead,
|
||||
size - bytesRead);
|
||||
if (sizeRead < 0 && errno == EINTR)
|
||||
continue;
|
||||
bool hadError = sizeRead <= 0;
|
||||
#endif
|
||||
if (hadError) {
|
||||
return 0;
|
||||
}
|
||||
bytesRead += sizeRead;
|
||||
if (!exact_size)
|
||||
break;
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
void WriteBytes(const char* bytes, size_t size)
|
||||
{
|
||||
size_t totalWritten = 0;
|
||||
while (totalWritten < size) {
|
||||
size_t length = size - totalWritten;
|
||||
if (length > kWritePacketSize)
|
||||
length = kWritePacketSize;
|
||||
#if defined(_WIN32)
|
||||
DWORD bytesWritten = 0;
|
||||
bool hadError = !WriteFile(writeHandle, bytes + totalWritten, static_cast<DWORD>(length), &bytesWritten, nullptr);
|
||||
#else
|
||||
int bytesWritten = write(writeFD, bytes + totalWritten, length);
|
||||
if (bytesWritten < 0 && errno == EINTR)
|
||||
continue;
|
||||
bool hadError = bytesWritten <= 0;
|
||||
#endif
|
||||
if (hadError)
|
||||
return;
|
||||
totalWritten += bytesWritten;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
already_AddRefed<nsIRemoteDebuggingPipe> nsRemoteDebuggingPipe::GetSingleton() {
|
||||
if (!gPipe) {
|
||||
gPipe = new nsRemoteDebuggingPipe();
|
||||
}
|
||||
return do_AddRef(gPipe);
|
||||
}
|
||||
|
||||
nsRemoteDebuggingPipe::nsRemoteDebuggingPipe() = default;
|
||||
|
||||
nsRemoteDebuggingPipe::~nsRemoteDebuggingPipe() = default;
|
||||
|
||||
nsresult nsRemoteDebuggingPipe::Init(nsIRemoteDebuggingPipeClient* aClient) {
|
||||
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
|
||||
if (mClient) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
mClient = aClient;
|
||||
|
||||
MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Reader", getter_AddRefs(mReaderThread)));
|
||||
MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Writer", getter_AddRefs(mWriterThread)));
|
||||
|
||||
#if defined(_WIN32)
|
||||
CHAR pipeReadStr[20];
|
||||
CHAR pipeWriteStr[20];
|
||||
GetEnvironmentVariableA("PW_PIPE_READ", pipeReadStr, 20);
|
||||
GetEnvironmentVariableA("PW_PIPE_WRITE", pipeWriteStr, 20);
|
||||
readHandle = reinterpret_cast<HANDLE>(atoi(pipeReadStr));
|
||||
writeHandle = reinterpret_cast<HANDLE>(atoi(pipeWriteStr));
|
||||
#endif
|
||||
|
||||
MOZ_ALWAYS_SUCCEEDS(mReaderThread->Dispatch(NewRunnableMethod(
|
||||
"nsRemoteDebuggingPipe::ReaderLoop",
|
||||
this, &nsRemoteDebuggingPipe::ReaderLoop), nsIThread::DISPATCH_NORMAL));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult nsRemoteDebuggingPipe::Stop() {
|
||||
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
|
||||
if (!mClient) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
m_terminated = true;
|
||||
mClient = nullptr;
|
||||
// Cancel pending synchronous read.
|
||||
#if defined(_WIN32)
|
||||
CancelIoEx(readHandle, nullptr);
|
||||
CloseHandle(readHandle);
|
||||
CloseHandle(writeHandle);
|
||||
#else
|
||||
shutdown(readFD, SHUT_RDWR);
|
||||
shutdown(writeFD, SHUT_RDWR);
|
||||
#endif
|
||||
mReaderThread->Shutdown();
|
||||
mReaderThread = nullptr;
|
||||
mWriterThread->Shutdown();
|
||||
mWriterThread = nullptr;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void nsRemoteDebuggingPipe::ReaderLoop() {
|
||||
const size_t bufSize = 256 * 1024;
|
||||
std::vector<char> buffer;
|
||||
buffer.resize(bufSize);
|
||||
std::vector<char> line;
|
||||
while (!m_terminated) {
|
||||
size_t size = ReadBytes(buffer.data(), bufSize, false);
|
||||
if (!size) {
|
||||
nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<>(
|
||||
"nsRemoteDebuggingPipe::Disconnected",
|
||||
this, &nsRemoteDebuggingPipe::Disconnected);
|
||||
NS_DispatchToMainThread(runnable.forget());
|
||||
break;
|
||||
}
|
||||
size_t start = 0;
|
||||
size_t end = line.size();
|
||||
line.insert(line.end(), buffer.begin(), buffer.begin() + size);
|
||||
while (true) {
|
||||
for (; end < line.size(); ++end) {
|
||||
if (line[end] == '\0') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (end == line.size()) {
|
||||
break;
|
||||
}
|
||||
if (end > start) {
|
||||
nsCString message;
|
||||
message.Append(line.data() + start, end - start);
|
||||
nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<nsCString>(
|
||||
"nsRemoteDebuggingPipe::ReceiveMessage",
|
||||
this, &nsRemoteDebuggingPipe::ReceiveMessage, std::move(message));
|
||||
NS_DispatchToMainThread(runnable.forget());
|
||||
}
|
||||
++end;
|
||||
start = end;
|
||||
}
|
||||
if (start != 0 && start < line.size()) {
|
||||
memmove(line.data(), line.data() + start, line.size() - start);
|
||||
}
|
||||
line.resize(line.size() - start);
|
||||
}
|
||||
}
|
||||
|
||||
void nsRemoteDebuggingPipe::ReceiveMessage(const nsCString& aMessage) {
|
||||
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
|
||||
if (mClient) {
|
||||
NS_ConvertUTF8toUTF16 utf16(aMessage);
|
||||
mClient->ReceiveMessage(utf16);
|
||||
}
|
||||
}
|
||||
|
||||
void nsRemoteDebuggingPipe::Disconnected() {
|
||||
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
|
||||
if (mClient)
|
||||
mClient->Disconnected();
|
||||
}
|
||||
|
||||
nsresult nsRemoteDebuggingPipe::SendMessage(const nsAString& aMessage) {
|
||||
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
|
||||
if (!mClient) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
NS_ConvertUTF16toUTF8 utf8(aMessage);
|
||||
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
|
||||
"nsRemoteDebuggingPipe::SendMessage",
|
||||
[message = std::move(utf8)] {
|
||||
const nsCString& flat = PromiseFlatCString(message);
|
||||
WriteBytes(flat.Data(), flat.Length());
|
||||
WriteBytes("\0", 1);
|
||||
});
|
||||
MOZ_ALWAYS_SUCCEEDS(mWriterThread->Dispatch(runnable.forget(), nsIThread::DISPATCH_NORMAL));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
34
additions/juggler/pipe/nsRemoteDebuggingPipe.h
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsIRemoteDebuggingPipe.h"
|
||||
#include "nsThread.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
class nsRemoteDebuggingPipe final : public nsIRemoteDebuggingPipe {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIREMOTEDEBUGGINGPIPE
|
||||
|
||||
static already_AddRefed<nsIRemoteDebuggingPipe> GetSingleton();
|
||||
nsRemoteDebuggingPipe();
|
||||
|
||||
private:
|
||||
void ReaderLoop();
|
||||
void ReceiveMessage(const nsCString& aMessage);
|
||||
void Disconnected();
|
||||
~nsRemoteDebuggingPipe();
|
||||
|
||||
RefPtr<nsIRemoteDebuggingPipeClient> mClient;
|
||||
nsCOMPtr<nsIThread> mReaderThread;
|
||||
nsCOMPtr<nsIThread> mWriterThread;
|
||||
std::atomic<bool> m_terminated { false };
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
318
additions/juggler/protocol/BrowserHandler.js
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
||||
const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
|
||||
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
||||
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
|
||||
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
const helper = new Helper();
|
||||
|
||||
class BrowserHandler {
|
||||
constructor(session, dispatcher, targetRegistry, startCompletePromise, onclose) {
|
||||
this._session = session;
|
||||
this._dispatcher = dispatcher;
|
||||
this._targetRegistry = targetRegistry;
|
||||
this._enabled = false;
|
||||
this._attachToDefaultContext = false;
|
||||
this._eventListeners = [];
|
||||
this._createdBrowserContextIds = new Set();
|
||||
this._attachedSessions = new Map();
|
||||
this._onclose = onclose;
|
||||
this._startCompletePromise = startCompletePromise;
|
||||
}
|
||||
|
||||
async ['Browser.enable']({attachToDefaultContext, userPrefs = []}) {
|
||||
if (this._enabled)
|
||||
return;
|
||||
await this._startCompletePromise;
|
||||
this._enabled = true;
|
||||
this._attachToDefaultContext = attachToDefaultContext;
|
||||
|
||||
for (const { name, value } of userPrefs) {
|
||||
if (value === true || value === false)
|
||||
Services.prefs.setBoolPref(name, value);
|
||||
else if (typeof value === 'string')
|
||||
Services.prefs.setStringPref(name, value);
|
||||
else if (typeof value === 'number')
|
||||
Services.prefs.setIntPref(name, value);
|
||||
else
|
||||
throw new Error(`Preference "${name}" has unsupported value: ${JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
this._eventListeners = [
|
||||
helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
|
||||
helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
|
||||
helper.on(this._targetRegistry, TargetRegistry.Events.DownloadCreated, this._onDownloadCreated.bind(this)),
|
||||
helper.on(this._targetRegistry, TargetRegistry.Events.DownloadFinished, this._onDownloadFinished.bind(this)),
|
||||
helper.on(this._targetRegistry, TargetRegistry.Events.ScreencastStopped, sessionId => {
|
||||
this._session.emitEvent('Browser.videoRecordingFinished', {screencastId: '' + sessionId});
|
||||
})
|
||||
];
|
||||
|
||||
for (const target of this._targetRegistry.targets())
|
||||
this._onTargetCreated(target);
|
||||
}
|
||||
|
||||
async ['Browser.createBrowserContext']({removeOnDetach}) {
|
||||
if (!this._enabled)
|
||||
throw new Error('Browser domain is not enabled');
|
||||
const browserContext = this._targetRegistry.createBrowserContext(removeOnDetach);
|
||||
this._createdBrowserContextIds.add(browserContext.browserContextId);
|
||||
return {browserContextId: browserContext.browserContextId};
|
||||
}
|
||||
|
||||
async ['Browser.removeBrowserContext']({browserContextId}) {
|
||||
if (!this._enabled)
|
||||
throw new Error('Browser domain is not enabled');
|
||||
await this._targetRegistry.browserContextForId(browserContextId).destroy();
|
||||
this._createdBrowserContextIds.delete(browserContextId);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeListeners(this._eventListeners);
|
||||
for (const [target, session] of this._attachedSessions)
|
||||
this._dispatcher.destroySession(session);
|
||||
this._attachedSessions.clear();
|
||||
for (const browserContextId of this._createdBrowserContextIds) {
|
||||
const browserContext = this._targetRegistry.browserContextForId(browserContextId);
|
||||
if (browserContext.removeOnDetach)
|
||||
browserContext.destroy();
|
||||
}
|
||||
this._createdBrowserContextIds.clear();
|
||||
}
|
||||
|
||||
_shouldAttachToTarget(target) {
|
||||
if (this._createdBrowserContextIds.has(target._browserContext.browserContextId))
|
||||
return true;
|
||||
return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext();
|
||||
}
|
||||
|
||||
_onTargetCreated(target) {
|
||||
if (!this._shouldAttachToTarget(target))
|
||||
return;
|
||||
const channel = target.channel();
|
||||
const session = this._dispatcher.createSession();
|
||||
this._attachedSessions.set(target, session);
|
||||
this._session.emitEvent('Browser.attachedToTarget', {
|
||||
sessionId: session.sessionId(),
|
||||
targetInfo: target.info()
|
||||
});
|
||||
session.setHandler(new PageHandler(target, session, channel));
|
||||
}
|
||||
|
||||
_onTargetDestroyed(target) {
|
||||
const session = this._attachedSessions.get(target);
|
||||
if (!session)
|
||||
return;
|
||||
this._attachedSessions.delete(target);
|
||||
this._dispatcher.destroySession(session);
|
||||
this._session.emitEvent('Browser.detachedFromTarget', {
|
||||
sessionId: session.sessionId(),
|
||||
targetId: target.id(),
|
||||
});
|
||||
}
|
||||
|
||||
_onDownloadCreated(downloadInfo) {
|
||||
this._session.emitEvent('Browser.downloadCreated', downloadInfo);
|
||||
}
|
||||
|
||||
_onDownloadFinished(downloadInfo) {
|
||||
this._session.emitEvent('Browser.downloadFinished', downloadInfo);
|
||||
}
|
||||
|
||||
async ['Browser.cancelDownload']({uuid}) {
|
||||
await this._targetRegistry.cancelDownload({uuid});
|
||||
}
|
||||
|
||||
async ['Browser.newPage']({browserContextId}) {
|
||||
const targetId = await this._targetRegistry.newPage({browserContextId});
|
||||
return {targetId};
|
||||
}
|
||||
|
||||
async ['Browser.close']() {
|
||||
let browserWindow = Services.wm.getMostRecentWindow(
|
||||
"navigator:browser"
|
||||
);
|
||||
if (browserWindow && browserWindow.gBrowserInit) {
|
||||
// idleTasksFinishedPromise does not resolve when the window
|
||||
// is closed early enough, so we race against window closure.
|
||||
await Promise.race([
|
||||
browserWindow.gBrowserInit.idleTasksFinishedPromise,
|
||||
waitForWindowClosed(browserWindow),
|
||||
]);
|
||||
}
|
||||
await this._startCompletePromise;
|
||||
this._onclose();
|
||||
Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
|
||||
}
|
||||
|
||||
async ['Browser.grantPermissions']({browserContextId, origin, permissions}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions);
|
||||
}
|
||||
|
||||
async ['Browser.resetPermissions']({browserContextId}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).resetPermissions();
|
||||
}
|
||||
|
||||
['Browser.setExtraHTTPHeaders']({browserContextId, headers}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers;
|
||||
}
|
||||
|
||||
['Browser.clearCache']() {
|
||||
// Clearing only the context cache does not work: https://bugzilla.mozilla.org/show_bug.cgi?id=1819147
|
||||
Services.cache2.clear();
|
||||
ChromeUtils.clearStyleSheetCache();
|
||||
}
|
||||
|
||||
['Browser.setHTTPCredentials']({browserContextId, credentials}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials);
|
||||
}
|
||||
|
||||
async ['Browser.setBrowserProxy']({type, host, port, bypass, username, password}) {
|
||||
this._targetRegistry.setBrowserProxy({ type, host, port, bypass, username, password});
|
||||
}
|
||||
|
||||
async ['Browser.setContextProxy']({browserContextId, type, host, port, bypass, username, password}) {
|
||||
const browserContext = this._targetRegistry.browserContextForId(browserContextId);
|
||||
browserContext.setProxy({ type, host, port, bypass, username, password });
|
||||
}
|
||||
|
||||
['Browser.setRequestInterception']({browserContextId, enabled}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled;
|
||||
}
|
||||
|
||||
['Browser.setCacheDisabled']({browserContextId, cacheDisabled}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).setCacheDisabled(cacheDisabled);
|
||||
}
|
||||
|
||||
['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors));
|
||||
}
|
||||
|
||||
['Browser.setDownloadOptions']({browserContextId, downloadOptions}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions);
|
||||
}
|
||||
|
||||
async ['Browser.setGeolocationOverride']({browserContextId, geolocation}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation));
|
||||
}
|
||||
|
||||
async ['Browser.setOnlineOverride']({browserContextId, override}) {
|
||||
const forceOffline = override === 'offline';
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setForceOffline(forceOffline);
|
||||
}
|
||||
|
||||
async ['Browser.setColorScheme']({browserContextId, colorScheme}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setColorScheme(nullToUndefined(colorScheme));
|
||||
}
|
||||
|
||||
async ['Browser.setReducedMotion']({browserContextId, reducedMotion}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setReducedMotion(nullToUndefined(reducedMotion));
|
||||
}
|
||||
|
||||
async ['Browser.setForcedColors']({browserContextId, forcedColors}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setForcedColors(nullToUndefined(forcedColors));
|
||||
}
|
||||
|
||||
async ['Browser.setVideoRecordingOptions']({browserContextId, options}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setVideoRecordingOptions(options);
|
||||
}
|
||||
|
||||
async ['Browser.setUserAgentOverride']({browserContextId, userAgent}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setDefaultUserAgent(userAgent);
|
||||
}
|
||||
|
||||
async ['Browser.setPlatformOverride']({browserContextId, platform}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setDefaultPlatform(platform);
|
||||
}
|
||||
|
||||
async ['Browser.setBypassCSP']({browserContextId, bypassCSP}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP));
|
||||
}
|
||||
|
||||
async ['Browser.setJavaScriptDisabled']({browserContextId, javaScriptDisabled}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled));
|
||||
}
|
||||
|
||||
async ['Browser.setLocaleOverride']({browserContextId, locale}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale));
|
||||
}
|
||||
|
||||
async ['Browser.setTimezoneOverride']({browserContextId, timezoneId}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId));
|
||||
}
|
||||
|
||||
async ['Browser.setTouchOverride']({browserContextId, hasTouch}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setTouchOverride(nullToUndefined(hasTouch));
|
||||
}
|
||||
|
||||
async ['Browser.setDefaultViewport']({browserContextId, viewport}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport));
|
||||
}
|
||||
|
||||
async ['Browser.setScrollbarsHidden']({browserContextId, hidden}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('scrollbarsHidden', nullToUndefined(hidden));
|
||||
}
|
||||
|
||||
async ['Browser.setInitScripts']({browserContextId, scripts}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts);
|
||||
}
|
||||
|
||||
async ['Browser.addBinding']({browserContextId, worldName, name, script}) {
|
||||
await this._targetRegistry.browserContextForId(browserContextId).addBinding(worldName, name, script);
|
||||
}
|
||||
|
||||
['Browser.setCookies']({browserContextId, cookies}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies);
|
||||
}
|
||||
|
||||
['Browser.clearCookies']({browserContextId}) {
|
||||
this._targetRegistry.browserContextForId(browserContextId).clearCookies();
|
||||
}
|
||||
|
||||
['Browser.getCookies']({browserContextId}) {
|
||||
const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies();
|
||||
return {cookies};
|
||||
}
|
||||
|
||||
async ['Browser.getInfo']() {
|
||||
const version = AppConstants.MOZ_APP_VERSION_DISPLAY;
|
||||
const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"]
|
||||
.getService(Components.interfaces.nsIHttpProtocolHandler)
|
||||
.userAgent;
|
||||
return {version: 'Firefox/' + version, userAgent};
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForWindowClosed(browserWindow) {
|
||||
if (browserWindow.closed)
|
||||
return;
|
||||
await new Promise((resolve => {
|
||||
const listener = {
|
||||
onCloseWindow: window => {
|
||||
let domWindow;
|
||||
if (window instanceof Ci.nsIAppWindow)
|
||||
domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
|
||||
else
|
||||
domWindow = window;
|
||||
if (domWindow === browserWindow) {
|
||||
Services.wm.removeListener(listener);
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
};
|
||||
Services.wm.addListener(listener);
|
||||
}));
|
||||
}
|
||||
|
||||
function nullToUndefined(value) {
|
||||
return value === null ? undefined : value;
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = ['BrowserHandler'];
|
||||
this.BrowserHandler = BrowserHandler;
|
||||