Initial release v128.0-1

This commit is contained in:
daijro.dev@gmail.com 2024-07-26 06:34:50 -05:00
commit 1090f6a212
834 changed files with 45170 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
/firefox-*
/camoufox-*
/mozilla-unified
/extra-docs
/.vscode
_old/
dist/
/bundle/fonts/extra
launch
launch.exe

373
LICENSE Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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
);
}
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"></svg>

After

Width:  |  Height:  |  Size: 72 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="336" height="48"></svg>

After

Width:  |  Height:  |  Size: 71 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="172" height="48"></svg>

After

Width:  |  Height:  |  Size: 71 B

View 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

View 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"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

View 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>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View 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 " ">

View 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 = { " " }

View file

@ -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

View 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)

View 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']

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View 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/. */

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -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;
}

View file

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View file

@ -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}",
}
}
}

View file

@ -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 cant find the file at %S.
fileAccessDenied=The file at %S is not readable.
dnsNotFound2=We cant connect to the server at %S.
unknownProtocolFound=Camoufox doesnt know how to open this address, because one of the following protocols (%S) isnt associated with any program or is not allowed in this context.
connectionFailure=Camoufox cant 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, dont 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 doesnt 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 cant 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 cant 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View 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"
}
}
}

View 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

File diff suppressed because it is too large Load diff

View 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
View 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;

View 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);
}
}

View 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;

View 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;

File diff suppressed because it is too large Load diff

View 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;
};

View 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",
},
]

View 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"]

View 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;

View 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'];

View 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;

View 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;

View 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);

View 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;
}

View 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
View 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)

View 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")

View 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'],
},
]

View 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'

View 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();
};

View 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

View 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

View 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;

Some files were not shown because too many files have changed in this diff Show more