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