mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-29 09:51:33 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4d9821fd7 | ||
|
|
221b909190 | ||
|
|
ce0cbde336 | ||
|
|
70ca9ad3bc | ||
|
|
81aece863f | ||
|
|
e0bb36ef26 | ||
|
|
1b299d0df4 | ||
|
|
928699e61c | ||
|
|
1f2fb08b09 | ||
|
|
5404c990eb | ||
|
|
47bc3dc294 | ||
|
|
eac29c4b1e | ||
|
|
cee4147767 | ||
|
|
6517dd8287 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -5,4 +5,4 @@
|
|||||||
*.patch linguist-language=C++
|
*.patch linguist-language=C++
|
||||||
*.d.ts linguist-language=TypeScript
|
*.d.ts linguist-language=TypeScript
|
||||||
|
|
||||||
src/zen/tests/*.js linguist-language=Test
|
src/zen/tests/** linguist-language=C++
|
||||||
|
|||||||
2
.github/workflows/linux-release-build.yml
vendored
2
.github/workflows/linux-release-build.yml
vendored
@@ -105,6 +105,8 @@ jobs:
|
|||||||
SURFER_COMPAT: ${{ matrix.arch }}
|
SURFER_COMPAT: ${{ matrix.arch }}
|
||||||
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
||||||
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
||||||
|
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
|
||||||
|
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
|
||||||
run: |
|
run: |
|
||||||
. "$HOME/.cargo/env"
|
. "$HOME/.cargo/env"
|
||||||
npm run import
|
npm run import
|
||||||
|
|||||||
2
.github/workflows/macos-release-build.yml
vendored
2
.github/workflows/macos-release-build.yml
vendored
@@ -122,6 +122,8 @@ jobs:
|
|||||||
SURFER_COMPAT: ${{ matrix.arch }}
|
SURFER_COMPAT: ${{ matrix.arch }}
|
||||||
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
||||||
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
||||||
|
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
|
||||||
|
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
|
||||||
SURFER_PLATFORM: darwin
|
SURFER_PLATFORM: darwin
|
||||||
run: |
|
run: |
|
||||||
. "$HOME/.cargo/env"
|
. "$HOME/.cargo/env"
|
||||||
|
|||||||
4
.github/workflows/windows-release-build.yml
vendored
4
.github/workflows/windows-release-build.yml
vendored
@@ -154,7 +154,7 @@ jobs:
|
|||||||
zlib1g-dev \
|
zlib1g-dev \
|
||||||
aria2
|
aria2
|
||||||
echo Setup wine
|
echo Setup wine
|
||||||
aria2c "https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/dQz_aHy8Rl-Lt0xf2WlrMw/artifacts/public/build/wine.tar.zst" -o wine.tar.zst
|
aria2c "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.cache.level-3.toolchains.v3.linux64-wine.latest/artifacts/public/build/wine.tar.zst" -o wine.tar.zst
|
||||||
tar --zstd -xf wine.tar.zst -C ~/win-cross
|
tar --zstd -xf wine.tar.zst -C ~/win-cross
|
||||||
rm wine.tar.zst
|
rm wine.tar.zst
|
||||||
echo Setup Visual Studio
|
echo Setup Visual Studio
|
||||||
@@ -166,6 +166,8 @@ jobs:
|
|||||||
SURFER_COMPAT: ${{ matrix.arch }}
|
SURFER_COMPAT: ${{ matrix.arch }}
|
||||||
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
|
||||||
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
|
||||||
|
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
|
||||||
|
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
|
||||||
run: |
|
run: |
|
||||||
. "$HOME/.cargo/env"
|
. "$HOME/.cargo/env"
|
||||||
npm run import -- --verbose
|
npm run import -- --verbose
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
|
|||||||
|
|
||||||
### Firefox Versions
|
### Firefox Versions
|
||||||
|
|
||||||
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `154.0`! 🚀
|
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `154.0.1`!
|
||||||
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 154.0`!
|
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 154.0.1`!
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1fb0703fa9723fd9c400ad60d18e64ee4f9bf125
|
bf4b57fd7fe94054457be7018e8e7beaf754c520
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[string][Parameter(Mandatory=$true)]$SignIdentity,
|
|
||||||
[string][Parameter(Mandatory=$true)]$SignIdentityIssuer,
|
|
||||||
[string][Parameter(Mandatory=$true)]$GithubRunId
|
[string][Parameter(Mandatory=$true)]$GithubRunId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,12 +22,23 @@ mkdir windsign-temp -ErrorAction SilentlyContinue
|
|||||||
# echo "Downloaded git objects repo to"
|
# echo "Downloaded git objects repo to"
|
||||||
#} -Verbose -ArgumentList $PWD -Debug
|
#} -Verbose -ArgumentList $PWD -Debug
|
||||||
|
|
||||||
|
$token = gh auth token
|
||||||
|
|
||||||
$env:SURFER_MOZCONFIG_ONLY="1"
|
$env:SURFER_MOZCONFIG_ONLY="1"
|
||||||
$env:SURFER_SIGNING_MODE=""
|
$env:SURFER_SIGNING_MODE=""
|
||||||
|
|
||||||
|
# Fetch SignIdentity from the github repo secrets, with the name SURFER_CERT_PATCH_ISSUER
|
||||||
|
$SignIdentityIssuer = (Invoke-RestMethod -Uri "https://api.github.com/repos/zen-browser/desktop/actions/secrets/SURFER_CERT_PATCH_ISSUER" -Headers @{Authorization = "Bearer $token"}).value
|
||||||
|
$SignIdentity = (Invoke-RestMethod -Uri "https://api.github.com/repos/zen-browser/desktop/actions/secrets/SURFER_CERT_PATCH_NAME" -Headers @{Authorization = "Bearer $token"}).value
|
||||||
|
$SignIndentityIssuerPrev = (Invoke-RestMethod -Uri "https://api.github.com/repos/zen-browser/desktop/actions/secrets/SURFER_CERT_PATCH_ISSUER_PREV" -Headers @{Authorization = "Bearer $token"}).value
|
||||||
|
$SignIdentityPrev = (Invoke-RestMethod -Uri "https://api.github.com/repos/zen-browser/desktop/actions/secrets/SURFER_CERT_PATCH_NAME_PREV" -Headers @{Authorization = "Bearer $token"}).
|
||||||
|
|
||||||
$env:SURFER_CERT_PATCH_ISSUER=$SignIdentityIssuer
|
$env:SURFER_CERT_PATCH_ISSUER=$SignIdentityIssuer
|
||||||
$env:SURFER_CERT_PATCH_NAME=$SignIdentity
|
$env:SURFER_CERT_PATCH_NAME=$SignIdentity
|
||||||
|
|
||||||
|
$env:SURFER_CERT_PATCH_NAME_PREV=$SignIdentityPrev
|
||||||
|
$env:SURFER_CERT_PATCH_ISSUER_PREV=$SignIndentityIssuerPrev
|
||||||
|
|
||||||
Start-Job -Name "DownloadGitl10n" -ScriptBlock {
|
Start-Job -Name "DownloadGitl10n" -ScriptBlock {
|
||||||
param($PWD)
|
param($PWD)
|
||||||
cd $PWD
|
cd $PWD
|
||||||
@@ -46,7 +55,6 @@ npm run build
|
|||||||
|
|
||||||
echo "Downloading artifacts info"
|
echo "Downloading artifacts info"
|
||||||
$artifactsInfo=gh api repos/zen-browser/desktop/actions/runs/$GithubRunId/artifacts
|
$artifactsInfo=gh api repos/zen-browser/desktop/actions/runs/$GithubRunId/artifacts
|
||||||
$token = gh auth token
|
|
||||||
|
|
||||||
function New-TemporaryDirectory {
|
function New-TemporaryDirectory {
|
||||||
$tmp = [System.IO.Path]::GetTempPath() # Not $env:TEMP, see https://stackoverflow.com/a/946017
|
$tmp = [System.IO.Path]::GetTempPath() # Not $env:TEMP, see https://stackoverflow.com/a/946017
|
||||||
@@ -215,21 +223,13 @@ SignAndPackage x86_64
|
|||||||
$files = Get-ChildItem .\windsign-temp\windows-x64-signed-x86_64, .\windsign-temp\windows-x64-signed-arm64 -Recurse -Include *.exe
|
$files = Get-ChildItem .\windsign-temp\windows-x64-signed-x86_64, .\windsign-temp\windows-x64-signed-arm64 -Recurse -Include *.exe
|
||||||
signtool.exe sign /n "$SignIdentity" /t http://time.certum.pl/ /fd sha256 /v $files
|
signtool.exe sign /n "$SignIdentity" /t http://time.certum.pl/ /fd sha256 /v $files
|
||||||
|
|
||||||
# Compress each signed bundle into a single gzip tarball (`-C <dir> .` so it has
|
|
||||||
# no top-level directory) and upload it as a release asset on the windows-binaries
|
|
||||||
# repo, keyed to this build's run id. Release assets keep the bundles out of git
|
|
||||||
# (they were too large to commit) while still living in the windows-binaries repo.
|
|
||||||
# The gated release job downloads and expands these from that repo's releases.
|
|
||||||
$binariesRepo = "zen-browser/windows-binaries"
|
$binariesRepo = "zen-browser/windows-binaries"
|
||||||
$stagingTag = "windows-signed-$GithubRunId"
|
$stagingTag = "windows-signed-$GithubRunId"
|
||||||
echo "Ensuring staging release $stagingTag exists on $binariesRepo"
|
echo "Ensuring staging release $stagingTag exists on $binariesRepo"
|
||||||
gh release view $stagingTag --repo $binariesRepo *> $null
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
gh release create $stagingTag --repo $binariesRepo --prerelease --title "Windows signed bundles ($GithubRunId)" --notes "Signed Windows bundles for run $GithubRunId, consumed by the release workflow. Safe to delete."
|
gh release create $stagingTag --repo $binariesRepo --prerelease --title "Windows signed bundles ($GithubRunId)" --notes "Signed Windows bundles for run $GithubRunId, consumed by the release workflow. Safe to delete."
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "Failed to create staging release $stagingTag on $binariesRepo"
|
throw "Failed to create staging release $stagingTag on $binariesRepo"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($name in @("x86_64", "arm64")) {
|
foreach ($name in @("x86_64", "arm64")) {
|
||||||
$signedDir = ".\windsign-temp\windows-x64-signed-$name"
|
$signedDir = ".\windsign-temp\windows-x64-signed-$name"
|
||||||
|
|||||||
@@ -164,5 +164,5 @@ zen-window-sync-migration-dialog-learn-more = Learn More
|
|||||||
zen-window-sync-migration-dialog-accept = Got It
|
zen-window-sync-migration-dialog-accept = Got It
|
||||||
|
|
||||||
zen-appmenu-new-blank-window =
|
zen-appmenu-new-blank-window =
|
||||||
.label = New blank window
|
.label = New Blank Window
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
tab-zen-split-tabs =
|
tab-zen-split-tabs =
|
||||||
.label =
|
.label =
|
||||||
{ $tabCount ->
|
{ $tabCount ->
|
||||||
[-1] Split out tab
|
[-1] Split Out Tab
|
||||||
[1] Add split view...
|
[1] Add Split View...
|
||||||
*[other] Join { $tabCount } Tabs
|
*[other] Join { $tabCount } Tabs
|
||||||
}
|
}
|
||||||
.accesskey = S
|
.accesskey = S
|
||||||
|
|
||||||
zen-split-link =
|
zen-split-link =
|
||||||
.label = Split link to new tab
|
.label = Split Link to New Tab
|
||||||
.accesskey = S
|
.accesskey = S
|
||||||
|
|
||||||
zen-split-view-modifier-header = Split View
|
zen-split-view-modifier-header = Split View
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -11,7 +11,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.27.0",
|
"@babel/preset-typescript": "^7.27.0",
|
||||||
"@zen-browser/prettier": "^3.9.3",
|
"@zen-browser/prettier": "^3.9.3",
|
||||||
"@zen-browser/surfer": "^1.14.7",
|
"@zen-browser/surfer": "^1.14.8",
|
||||||
"formal-git": "^1.2.9",
|
"formal-git": "^1.2.9",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
@@ -860,9 +860,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@zen-browser/surfer": {
|
"node_modules/@zen-browser/surfer": {
|
||||||
"version": "1.14.7",
|
"version": "1.14.8",
|
||||||
"resolved": "https://registry.npmjs.org/@zen-browser/surfer/-/surfer-1.14.7.tgz",
|
"resolved": "https://registry.npmjs.org/@zen-browser/surfer/-/surfer-1.14.8.tgz",
|
||||||
"integrity": "sha512-Ui2Chqvr4/+FmHM54eKOv7cQu8DRAK/uCX9o1GNi81hszxo/RSQwaw71eNW43Qcj7giK4U4x0IdCMht98iQ0rA==",
|
"integrity": "sha512-5HHcfibwHIK5n3huhJL2TLBX2+8odj6ebCMA6U2EcUj9ROSrcz7/bjY2n5MIa9oIRL9gtV3ott0jwJj6SlapxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.27.0",
|
"@babel/preset-typescript": "^7.27.0",
|
||||||
"@zen-browser/prettier": "^3.9.3",
|
"@zen-browser/prettier": "^3.9.3",
|
||||||
"@zen-browser/surfer": "^1.14.7",
|
"@zen-browser/surfer": "^1.14.8",
|
||||||
"formal-git": "^1.2.9",
|
"formal-git": "^1.2.9",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
- name: services.sync.engine.spaces
|
|
||||||
value: true
|
|
||||||
condition: "@IS_TWILIGHT@"
|
|
||||||
|
|
||||||
- name: services.sync.engine.spaces
|
|
||||||
value: false
|
|
||||||
locked: true
|
|
||||||
condition: "!@IS_TWILIGHT@"
|
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
- name: zen.workspaces.scroll-modifier-key
|
- name: zen.workspaces.scroll-modifier-key
|
||||||
value: ctrl
|
value: ctrl
|
||||||
|
|
||||||
|
- name: services.sync.engine.workspaces
|
||||||
|
value: false
|
||||||
|
|
||||||
- name: zen.workspaces.separate-essentials
|
- name: zen.workspaces.separate-essentials
|
||||||
value: true
|
value: true
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ packaging==24.2
|
|||||||
pathspec==0.12.1
|
pathspec==0.12.1
|
||||||
platformdirs==4.3.6
|
platformdirs==4.3.6
|
||||||
pycodestyle==2.12.1
|
pycodestyle==2.12.1
|
||||||
requests==2.33.0
|
requests==2.34.2
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/.prettierignore b/.prettierignore
|
diff --git a/.prettierignore b/.prettierignore
|
||||||
index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073afaebeb8a 100644
|
index bd8ab46ff7175f5039b69b04c1afe53ab8efe746..7ae12a1ff3724e99aeec2e93518640a2fa9611e6 100644
|
||||||
--- a/.prettierignore
|
--- a/.prettierignore
|
||||||
+++ b/.prettierignore
|
+++ b/.prettierignore
|
||||||
@@ -1803,3 +1803,12 @@ tools/ts/test/baselines/
|
@@ -1807,3 +1807,12 @@ tools/ts/test/baselines/
|
||||||
try_task_config.json
|
try_task_config.json
|
||||||
xpcom/idl-parser/xpidl/fixtures/xpctest.d.json
|
xpcom/idl-parser/xpidl/fixtures/xpctest.d.json
|
||||||
**/package-lock.json
|
**/package-lock.json
|
||||||
@@ -12,6 +12,6 @@ index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073a
|
|||||||
+*.min.js
|
+*.min.js
|
||||||
+*.min.mjs
|
+*.min.mjs
|
||||||
+*.inc
|
+*.inc
|
||||||
+*/mochitests/*
|
+**/mochitests/**
|
||||||
+*.svg
|
+*.svg
|
||||||
+
|
+
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
|
|
||||||
index b503987a08e2ce64bfc01c4e3a21e5e19ef834f0..b1c03a0b219fd3eddd93c1deaeb18ab79c85f168 100644
|
|
||||||
--- a/browser/components/preferences/config/account-sync.mjs
|
|
||||||
+++ b/browser/components/preferences/config/account-sync.mjs
|
|
||||||
@@ -42,6 +42,7 @@ Preferences.addAll([
|
|
||||||
{ id: "services.sync.engine.creditcards", type: "bool" },
|
|
||||||
{ id: "services.sync.engine.addons", type: "bool" },
|
|
||||||
{ id: "services.sync.engine.prefs", type: "bool" },
|
|
||||||
+ { id: "services.sync.engine.spaces", type: "bool" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
@@ -546,6 +547,7 @@ const SYNC_ENGINE_SETTINGS = [
|
|
||||||
type: "payments",
|
|
||||||
},
|
|
||||||
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
|
|
||||||
+ { id: "syncSpaces", pref: "services.sync.engine.spaces", type: "workspaces" },
|
|
||||||
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
|
|
||||||
];
|
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ index 64aa0d98a0622c01f3dcfff1a04bfcda368354d2..2013e04b0881ad2295d6897b91e1573c
|
|||||||
{ id: "services.sync.engine.passwords", type: "bool" },
|
{ id: "services.sync.engine.passwords", type: "bool" },
|
||||||
{ id: "services.sync.engine.addresses", type: "bool" },
|
{ id: "services.sync.engine.addresses", type: "bool" },
|
||||||
{ id: "services.sync.engine.creditcards", type: "bool" },
|
{ id: "services.sync.engine.creditcards", type: "bool" },
|
||||||
+ { id: "services.sync.engine.spaces", type: "bool" },
|
+ { id: "services.sync.engine.workspaces", type: "bool" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let gSyncChooseWhatToSync = {
|
let gSyncChooseWhatToSync = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ index a893c5ec3d007820d98f5d92dd039640faa2c181..9cbd00102e44ccf98b37845474d92d57
|
|||||||
+ <html:div class="sync-engine-workspaces">
|
+ <html:div class="sync-engine-workspaces">
|
||||||
+ <checkbox
|
+ <checkbox
|
||||||
+ data-l10n-id="sync-engine-workspaces"
|
+ data-l10n-id="sync-engine-workspaces"
|
||||||
+ preference="services.sync.engine.spaces"
|
+ preference="services.sync.engine.workspaces"
|
||||||
+ />
|
+ />
|
||||||
+ </html:div>
|
+ </html:div>
|
||||||
</html:div>
|
</html:div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ index c379e1a5f82692406a92d9fcd3bca2769dfac5b2..af037dd3d995813d966524ac44a3795d
|
|||||||
<image class="sync-engine-image sync-engine-prefs" alt=""/>
|
<image class="sync-engine-image sync-engine-prefs" alt=""/>
|
||||||
<label data-l10n-id="sync-currently-syncing-settings"/>
|
<label data-l10n-id="sync-currently-syncing-settings"/>
|
||||||
</html:div>
|
</html:div>
|
||||||
+ <html:div engine_preference="services.sync.engine.spaces">
|
+ <html:div engine_preference="services.sync.engine.workspaces">
|
||||||
+ <image class="sync-engine-image sync-engine-workspaces" alt=""/>
|
+ <image class="sync-engine-image sync-engine-workspaces" alt=""/>
|
||||||
+ <label data-l10n-id="sync-currently-syncing-workspaces"/>
|
+ <label data-l10n-id="sync-currently-syncing-workspaces"/>
|
||||||
+ </html:div>
|
+ </html:div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/browser/components/tabbrowser/content/tab-context-menu.js b/browser/components/tabbrowser/content/tab-context-menu.js
|
diff --git a/browser/components/tabbrowser/content/tab-context-menu.js b/browser/components/tabbrowser/content/tab-context-menu.js
|
||||||
index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..2e2404b78deb428e57d01b8b929231f675a21323 100644
|
index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..d7f75471a207cccac6ca3dec49037ac30dd76163 100644
|
||||||
--- a/browser/components/tabbrowser/content/tab-context-menu.js
|
--- a/browser/components/tabbrowser/content/tab-context-menu.js
|
||||||
+++ b/browser/components/tabbrowser/content/tab-context-menu.js
|
+++ b/browser/components/tabbrowser/content/tab-context-menu.js
|
||||||
@@ -839,7 +839,8 @@ var TabContextMenu = {
|
@@ -839,13 +839,15 @@ var TabContextMenu = {
|
||||||
);
|
);
|
||||||
contextUnpinSelectedTabs.hidden =
|
contextUnpinSelectedTabs.hidden =
|
||||||
!this.contextTab.pinned || !this.multiselected;
|
!this.contextTab.pinned || !this.multiselected;
|
||||||
@@ -12,3 +12,10 @@ index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..2e2404b78deb428e57d01b8b929231f6
|
|||||||
// Build Ask Chat items. The classic layout shows the full ask-chat submenu
|
// Build Ask Chat items. The classic layout shows the full ask-chat submenu
|
||||||
// (#context_askChat); the alt layout shows only a flat "Summarize Page" item
|
// (#context_askChat); the alt layout shows only a flat "Summarize Page" item
|
||||||
// (#context_askChatSummarize). Hide whichever one this layout doesn't use so
|
// (#context_askChatSummarize). Hide whichever one this layout doesn't use so
|
||||||
|
// a pref toggle reverses cleanly.
|
||||||
|
if (!this._altTabContextMenu) {
|
||||||
|
document.getElementById("context_askChatSummarize").hidden = true;
|
||||||
|
+ document.getElementById("context_aiSeparator").hidden = true;
|
||||||
|
TabContextMenu.GenAI.buildTabMenu(
|
||||||
|
document.getElementById("context_askChat"),
|
||||||
|
this
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||||
index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489fb04e8b7a 100644
|
index 89f0d44d462de3d7d0f581d6371606f475443822..0d45ce5f6455e9c7276c54f504906bade5b3d286 100644
|
||||||
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||||
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
||||||
@@ -462,6 +462,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
|
@@ -163,6 +163,9 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
|
||||||
|
// Feature gate pref for flight status suggestions in the urlbar.
|
||||||
|
["flightStatus.featureGate", false],
|
||||||
|
|
||||||
|
+ // Whether to close the urlbar when blurring the window while the urlbar is focused.
|
||||||
|
+ ["closeOnWindowBlur", true],
|
||||||
|
+
|
||||||
|
// The minimum prefix length of a flight status keyword the user must type to
|
||||||
|
// trigger the suggestion. 0 means the min length should be taken from Nimbus
|
||||||
|
// or remote settings.
|
||||||
|
@@ -482,6 +485,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
|
||||||
["shortcuts.tabs", true],
|
["shortcuts.tabs", true],
|
||||||
["shortcuts.history", true],
|
["shortcuts.history", true],
|
||||||
["shortcuts.actions", true],
|
["shortcuts.actions", true],
|
||||||
@@ -10,7 +20,7 @@ index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489f
|
|||||||
|
|
||||||
// Boolean to determine if the providers defined in `exposureResults`
|
// Boolean to determine if the providers defined in `exposureResults`
|
||||||
// should be displayed in search results. This can be set by a
|
// should be displayed in search results. This can be set by a
|
||||||
@@ -799,6 +800,8 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) {
|
@@ -840,6 +844,8 @@ function makeDefaultResultGroups({
|
||||||
*/
|
*/
|
||||||
let rootGroup = {
|
let rootGroup = {
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
|
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||||
index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a6182579cbaf4d 100644
|
index e7e9495e56076cd112beafb8297dece4ae4cea58..95e4333866a5f0d41fbdbe23910c396406a247e9 100644
|
||||||
--- a/browser/components/urlbar/content/UrlbarInput.mjs
|
--- a/browser/components/urlbar/content/UrlbarInput.mjs
|
||||||
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
|
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||||
@@ -101,6 +101,13 @@ const lazy = XPCOMUtils.declareLazy({
|
@@ -101,6 +101,13 @@ const lazy = XPCOMUtils.declareLazy({
|
||||||
@@ -280,7 +280,20 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
|||||||
l10nId,
|
l10nId,
|
||||||
l10nId == "urlbar-placeholder-with-name"
|
l10nId == "urlbar-placeholder-with-name"
|
||||||
? { name: engineName }
|
? { name: engineName }
|
||||||
@@ -5424,6 +5540,11 @@ ${
|
@@ -5347,6 +5463,12 @@ ${
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy.logger.debug("Blur Event");
|
||||||
|
+ if (
|
||||||
|
+ this.document.commandDispatcher.focusedElement == this.inputField &&
|
||||||
|
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
|
||||||
|
+ ) {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
// We cannot count every blur events after a missed engagement as abandoment
|
||||||
|
// because the user may have clicked on some view element that executes
|
||||||
|
// a command causing a focus change. For example opening preferences from
|
||||||
|
@@ -5424,6 +5546,11 @@ ${
|
||||||
}
|
}
|
||||||
|
|
||||||
_on_click(event) {
|
_on_click(event) {
|
||||||
@@ -292,7 +305,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
|||||||
switch (event.target) {
|
switch (event.target) {
|
||||||
case this.inputField:
|
case this.inputField:
|
||||||
case this._inputContainer:
|
case this._inputContainer:
|
||||||
@@ -5513,10 +5634,11 @@ ${
|
@@ -5513,10 +5640,11 @@ ${
|
||||||
}
|
}
|
||||||
if (untrim) {
|
if (untrim) {
|
||||||
this.setValue(this._untrimmedValue);
|
this.setValue(this._untrimmedValue);
|
||||||
@@ -305,7 +318,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
|||||||
this.view.autoOpen({ event });
|
this.view.autoOpen({ event });
|
||||||
} else {
|
} else {
|
||||||
if (this._untrimOnFocusAfterKeydown) {
|
if (this._untrimOnFocusAfterKeydown) {
|
||||||
@@ -5556,9 +5678,16 @@ ${
|
@@ -5556,9 +5684,16 @@ ${
|
||||||
}
|
}
|
||||||
|
|
||||||
_on_mousedown(event) {
|
_on_mousedown(event) {
|
||||||
@@ -323,7 +336,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
|||||||
if (
|
if (
|
||||||
event.composedTarget != this.inputField &&
|
event.composedTarget != this.inputField &&
|
||||||
event.composedTarget != this._inputContainer
|
event.composedTarget != this._inputContainer
|
||||||
@@ -5568,6 +5697,10 @@ ${
|
@@ -5568,6 +5703,10 @@ ${
|
||||||
|
|
||||||
this.focusedViaMousedown = !this.focused;
|
this.focusedViaMousedown = !this.focused;
|
||||||
this.#preventClickSelectsAll = this.focused;
|
this.#preventClickSelectsAll = this.focused;
|
||||||
@@ -334,7 +347,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
|||||||
|
|
||||||
// Keep the focus status, since the attribute may be changed
|
// Keep the focus status, since the attribute may be changed
|
||||||
// upon calling this.focus().
|
// upon calling this.focus().
|
||||||
@@ -5605,7 +5738,7 @@ ${
|
@@ -5605,7 +5744,7 @@ ${
|
||||||
// view open on tab switch, and the TabSelect event arrived earlier.
|
// view open on tab switch, and the TabSelect event arrived earlier.
|
||||||
// Also ignore mousedown on the urlbarView context menu: opening/closing the
|
// Also ignore mousedown on the urlbarView context menu: opening/closing the
|
||||||
// view is already handled by the result opening flow.
|
// view is already handled by the result opening flow.
|
||||||
@@ -343,7 +356,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5894,7 +6027,7 @@ ${
|
@@ -5894,7 +6033,7 @@ ${
|
||||||
// When we are in actions search mode we can show more results so
|
// When we are in actions search mode we can show more results so
|
||||||
// increase the limit.
|
// increase the limit.
|
||||||
let maxResults =
|
let maxResults =
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs
|
diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs
|
||||||
index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac823fedcb 100644
|
index e465806c4ae4fc372d9aa2b71aa7f0e74c65d026..c0b3781901298bedee2736819a93edbf545507a7 100644
|
||||||
--- a/browser/components/urlbar/content/UrlbarView.mjs
|
--- a/browser/components/urlbar/content/UrlbarView.mjs
|
||||||
+++ b/browser/components/urlbar/content/UrlbarView.mjs
|
+++ b/browser/components/urlbar/content/UrlbarView.mjs
|
||||||
@@ -674,7 +674,7 @@ export class UrlbarView {
|
@@ -803,7 +803,7 @@ export class UrlbarView {
|
||||||
!this.input.value ||
|
!this.input.value ||
|
||||||
this.input.getAttribute("pageproxystate") == "valid"
|
this.input.getAttribute("pageproxystate") == "valid"
|
||||||
) {
|
) {
|
||||||
@@ -11,7 +11,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
|||||||
// Try to reuse the cached top-sites context. If it's not cached, then
|
// Try to reuse the cached top-sites context. If it's not cached, then
|
||||||
// there will be a gap of time between when the input is focused and
|
// there will be a gap of time between when the input is focused and
|
||||||
// when the view opens that can be perceived as flicker.
|
// when the view opens that can be perceived as flicker.
|
||||||
@@ -812,10 +812,6 @@ export class UrlbarView {
|
@@ -941,10 +941,6 @@ export class UrlbarView {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If search mode isn't active, close the view.
|
// If search mode isn't active, close the view.
|
||||||
@@ -22,7 +22,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
|||||||
|
|
||||||
// Search mode is active. If the one-offs should be shown, make sure they
|
// Search mode is active. If the one-offs should be shown, make sure they
|
||||||
// are enabled and show the view.
|
// are enabled and show the view.
|
||||||
@@ -3031,6 +3027,8 @@ export class UrlbarView {
|
@@ -3203,6 +3199,8 @@ export class UrlbarView {
|
||||||
if (row?.hasAttribute("row-selectable")) {
|
if (row?.hasAttribute("row-selectable")) {
|
||||||
row?.toggleAttribute("selected", true);
|
row?.toggleAttribute("selected", true);
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
|||||||
if (element != row) {
|
if (element != row) {
|
||||||
row?.toggleAttribute("descendant-selected", true);
|
row?.toggleAttribute("descendant-selected", true);
|
||||||
}
|
}
|
||||||
@@ -3544,7 +3542,7 @@ export class UrlbarView {
|
@@ -3715,7 +3713,7 @@ export class UrlbarView {
|
||||||
}
|
}
|
||||||
|
|
||||||
#enableOrDisableRowWrap() {
|
#enableOrDisableRowWrap() {
|
||||||
@@ -40,3 +40,16 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
|
|||||||
this.#rows.toggleAttribute("wrap", wrap);
|
this.#rows.toggleAttribute("wrap", wrap);
|
||||||
this.oneOffSearchButtons?.container.toggleAttribute("wrap", wrap);
|
this.oneOffSearchButtons?.container.toggleAttribute("wrap", wrap);
|
||||||
}
|
}
|
||||||
|
@@ -4144,6 +4142,12 @@ export class UrlbarView {
|
||||||
|
}
|
||||||
|
|
||||||
|
on_blur() {
|
||||||
|
+ if (
|
||||||
|
+ this.document.commandDispatcher.focusedElement == this.input.inputField &&
|
||||||
|
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
|
||||||
|
+ ) {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
// If the view is open without the input being focused, it will not close
|
||||||
|
// automatically when the window loses focus. We might be in this state
|
||||||
|
// after a Search Tip is shown on an engine homepage.
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
|
|
||||||
--- a/browser/app/profile/firefox.js
|
|
||||||
+++ b/browser/app/profile/firefox.js
|
|
||||||
@@ -411,10 +411,13 @@
|
|
||||||
pref("browser.urlbar.maxRichResults", 10);
|
|
||||||
|
|
||||||
// The maximum number of historical search results to show.
|
|
||||||
pref("browser.urlbar.maxHistoricalSearchSuggestions", 2);
|
|
||||||
|
|
||||||
+// Whether to close the urlbar when blurring the window while the urlbar is focused.
|
|
||||||
+pref("browser.urlbar.closeOnWindowBlur", true);
|
|
||||||
+
|
|
||||||
// The default behavior for the urlbar can be configured to use any combination
|
|
||||||
// of the match filters with each additional filter adding more results (union).
|
|
||||||
pref("browser.urlbar.suggest.bookmark", true);
|
|
||||||
pref("browser.urlbar.suggest.clipboard", true);
|
|
||||||
pref("browser.urlbar.suggest.history", true);
|
|
||||||
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
|
||||||
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
|
||||||
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
|
|
||||||
@@ -152,10 +152,13 @@
|
|
||||||
["filter.javascript", true],
|
|
||||||
|
|
||||||
// Feature gate pref for flight status suggestions in the urlbar.
|
|
||||||
["flightStatus.featureGate", false],
|
|
||||||
|
|
||||||
+ // Whether to close the urlbar when blurring the window while the urlbar is focused.
|
|
||||||
+ ["closeOnWindowBlur", true],
|
|
||||||
+
|
|
||||||
// The minimum prefix length of a flight status keyword the user must type to
|
|
||||||
// trigger the suggestion. 0 means the min length should be taken from Nimbus
|
|
||||||
// or remote settings.
|
|
||||||
["flightStatus.minKeywordLength", 0],
|
|
||||||
|
|
||||||
diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs
|
|
||||||
--- a/browser/components/urlbar/content/UrlbarView.mjs
|
|
||||||
+++ b/browser/components/urlbar/content/UrlbarView.mjs
|
|
||||||
@@ -3909,10 +3909,16 @@
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_blur() {
|
|
||||||
+ if (
|
|
||||||
+ this.document.commandDispatcher.focusedElement == this.input.inputField &&
|
|
||||||
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
|
|
||||||
+ ) {
|
|
||||||
+ return;
|
|
||||||
+ }
|
|
||||||
// If the view is open without the input being focused, it will not close
|
|
||||||
// automatically when the window loses focus. We might be in this state
|
|
||||||
// after a Search Tip is shown on an engine homepage.
|
|
||||||
if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
|
|
||||||
this.close();
|
|
||||||
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
|
|
||||||
--- a/browser/components/urlbar/content/UrlbarInput.mjs
|
|
||||||
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
|
|
||||||
@@ -5460,11 +5460,17 @@
|
|
||||||
_on_blur(event) {
|
|
||||||
if (this.view.resultMenu.hasAttribute("open")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy.logger.debug("Blur Event");
|
|
||||||
+ if (
|
|
||||||
+ this.document.commandDispatcher.focusedElement == this.inputField &&
|
|
||||||
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
|
|
||||||
+ ) {
|
|
||||||
+ return;
|
|
||||||
+ }
|
|
||||||
// We cannot count every blur events after a missed engagement as abandoment
|
|
||||||
// because the user may have clicked on some view element that executes
|
|
||||||
// a command causing a focus change. For example opening preferences from
|
|
||||||
// the oneoff settings button.
|
|
||||||
// For now we detect that case by discarding the event on command, but we
|
|
||||||
// may want to figure out a more robust way to detect abandonment.
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
diff --git a/gfx/webrender_bindings/WebRenderAPI.cpp b/gfx/webrender_bindings/WebRenderAPI.cpp
|
|
||||||
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
|
|
||||||
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
|
|
||||||
@@ -298,11 +298,13 @@
|
|
||||||
panic_on_gl_error, picTileWidth, picTileHeight,
|
|
||||||
gfx::gfxVars::WebRenderRequiresHardwareDriver(),
|
|
||||||
StaticPrefs::gfx_webrender_low_quality_pinch_zoom_AtStartup(),
|
|
||||||
StaticPrefs::gfx_webrender_max_shared_surface_size_AtStartup(),
|
|
||||||
StaticPrefs::gfx_webrender_enable_subpixel_aa_AtStartup(),
|
|
||||||
- compositor->ShouldUseLayerCompositor())) {
|
|
||||||
+ compositor->ShouldUseLayerCompositor(),
|
|
||||||
+ StaticPrefs::
|
|
||||||
+ gfx_webrender_opaque_backdrop_fallback_AtStartup())) {
|
|
||||||
// wr_window_new puts a message into gfxCriticalNote if it returns
|
|
||||||
// false
|
|
||||||
MOZ_ASSERT(errorMessage);
|
|
||||||
error.AssignASCII(errorMessage);
|
|
||||||
wr_api_free_error_msg(errorMessage);
|
|
||||||
diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src/bindings.rs
|
|
||||||
--- a/gfx/webrender_bindings/src/bindings.rs
|
|
||||||
+++ b/gfx/webrender_bindings/src/bindings.rs
|
|
||||||
@@ -1998,10 +1998,11 @@
|
|
||||||
reject_software_rasterizer: bool,
|
|
||||||
low_quality_pinch_zoom: bool,
|
|
||||||
max_shared_surface_size: i32,
|
|
||||||
enable_subpixel_aa: bool,
|
|
||||||
use_layer_compositor: bool,
|
|
||||||
+ opaque_backdrop_fallback: bool,
|
|
||||||
) -> bool {
|
|
||||||
assert!(unsafe { is_in_render_thread() });
|
|
||||||
|
|
||||||
// Ensure the WR profiler callbacks are hooked up to the Gecko profiler.
|
|
||||||
set_profiler_hooks(Some(&PROFILER_HOOKS));
|
|
||||||
@@ -2164,10 +2165,11 @@
|
|
||||||
texture_cache_config,
|
|
||||||
reject_software_rasterizer,
|
|
||||||
low_quality_pinch_zoom,
|
|
||||||
max_shared_surface_size,
|
|
||||||
enable_dithering,
|
|
||||||
+ opaque_backdrop_fallback,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let window_size = DeviceIntSize::new(window_width, window_height);
|
|
||||||
let notifier = Box::new(CppNotifier { window_id });
|
|
||||||
diff --git a/gfx/wr/webrender/src/device/gl.rs b/gfx/wr/webrender/src/device/gl.rs
|
|
||||||
--- a/gfx/wr/webrender/src/device/gl.rs
|
|
||||||
+++ b/gfx/wr/webrender/src/device/gl.rs
|
|
||||||
@@ -3982,10 +3982,14 @@
|
|
||||||
|
|
||||||
pub fn disable_color_write(&self) {
|
|
||||||
self.gl.color_mask(false, false, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
+ pub fn set_color_mask(&self, r: bool, g: bool, b: bool, a: bool) {
|
|
||||||
+ self.gl.color_mask(r, g, b, a);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
pub fn set_blend(&mut self, enable: bool) {
|
|
||||||
if enable {
|
|
||||||
self.gl.enable(gl::BLEND);
|
|
||||||
} else {
|
|
||||||
self.gl.disable(gl::BLEND);
|
|
||||||
diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/renderer/init.rs
|
|
||||||
--- a/gfx/wr/webrender/src/renderer/init.rs
|
|
||||||
+++ b/gfx/wr/webrender/src/renderer/init.rs
|
|
||||||
@@ -204,6 +204,8 @@
|
|
||||||
pub low_quality_pinch_zoom: bool,
|
|
||||||
pub max_shared_surface_size: i32,
|
|
||||||
/// If true, open a debug socket to listen for remote debugger.
|
|
||||||
/// Relies on `debugger` cargo feature being enabled.
|
|
||||||
pub enable_debugger: bool,
|
|
||||||
+ /// See explanation of `gfx.webrender.opaque-backdrop-fallback`.
|
|
||||||
+ pub opaque_backdrop_fallback: bool,
|
|
||||||
}
|
|
||||||
@@ -277,9 +279,10 @@
|
|
||||||
enable_instancing: true,
|
|
||||||
reject_software_rasterizer: false,
|
|
||||||
low_quality_pinch_zoom: false,
|
|
||||||
max_shared_surface_size: 2048,
|
|
||||||
enable_debugger: true,
|
|
||||||
+ opaque_backdrop_fallback: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -802,10 +805,11 @@
|
|
||||||
allocated_native_surfaces: FastHashSet::default(),
|
|
||||||
debug_overlay_state: DebugOverlayState::new(),
|
|
||||||
buffer_damage_tracker: BufferDamageTracker::default(),
|
|
||||||
max_primitive_instance_count,
|
|
||||||
enable_instancing: options.enable_instancing,
|
|
||||||
+ opaque_backdrop_fallback: options.opaque_backdrop_fallback,
|
|
||||||
consecutive_oom_frames: 0,
|
|
||||||
target_frame_publish_id: None,
|
|
||||||
pending_result_msg: None,
|
|
||||||
layer_compositor_frame_state_in_prev_frame: None,
|
|
||||||
external_composite_debug_items: Vec::new(),
|
|
||||||
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
|
|
||||||
--- a/gfx/wr/webrender/src/renderer/mod.rs
|
|
||||||
+++ b/gfx/wr/webrender/src/renderer/mod.rs
|
|
||||||
@@ -867,10 +867,12 @@
|
|
||||||
buffer_damage_tracker: BufferDamageTracker,
|
|
||||||
|
|
||||||
max_primitive_instance_count: usize,
|
|
||||||
enable_instancing: bool,
|
|
||||||
|
|
||||||
+ opaque_backdrop_fallback: bool,
|
|
||||||
+
|
|
||||||
/// Count consecutive oom frames to detectif we are stuck unable to render
|
|
||||||
/// in a loop.
|
|
||||||
consecutive_oom_frames: u32,
|
|
||||||
|
|
||||||
/// update() defers processing of ResultMsg, if frame_publish_id of
|
|
||||||
@@ -2787,18 +2789,29 @@
|
|
||||||
let read_target = ReadTarget::from_texture(cache_texture);
|
|
||||||
|
|
||||||
// Should always be drawing to picture cache tiles or off-screen surface!
|
|
||||||
debug_assert!(!draw_target.is_default());
|
|
||||||
let device_to_framebuffer = Scale::new(1i32);
|
|
||||||
+ let dest_fb_rect = dest * device_to_framebuffer;
|
|
||||||
|
|
||||||
self.device.blit_render_target(
|
|
||||||
read_target,
|
|
||||||
src * device_to_framebuffer,
|
|
||||||
draw_target,
|
|
||||||
- dest * device_to_framebuffer,
|
|
||||||
+ dest_fb_rect,
|
|
||||||
TextureFilter::Linear,
|
|
||||||
);
|
|
||||||
+
|
|
||||||
+ if self.opaque_backdrop_fallback {
|
|
||||||
+ self.device.set_color_mask(false, false, false, true);
|
|
||||||
+ self.device.clear_target(
|
|
||||||
+ Some([0.0, 0.0, 0.0, 1.0]),
|
|
||||||
+ None,
|
|
||||||
+ Some(dest_fb_rect),
|
|
||||||
+ );
|
|
||||||
+ self.device.set_color_mask(true, true, true, true);
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_picture_cache_target(
|
|
||||||
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
|
|
||||||
--- a/modules/libpref/init/StaticPrefList.yaml
|
|
||||||
+++ b/modules/libpref/init/StaticPrefList.yaml
|
|
||||||
@@ -8439,10 +8439,17 @@
|
|
||||||
#else
|
|
||||||
value: false
|
|
||||||
#endif
|
|
||||||
mirror: once
|
|
||||||
|
|
||||||
+# Make backdrop-filter treat its captured backdrop as if it had been
|
|
||||||
+# composited over an opaque-black background. (See bug 2036640)
|
|
||||||
+- name: gfx.webrender.opaque-backdrop-fallback
|
|
||||||
+ type: bool
|
|
||||||
+ value: true
|
|
||||||
+ mirror: once
|
|
||||||
+
|
|
||||||
# Disable wait of GPU execution completion
|
|
||||||
- name: gfx.webrender.wait-gpu-finished.disabled
|
|
||||||
type: bool
|
|
||||||
value: false
|
|
||||||
mirror: once
|
|
||||||
|
|
||||||
166
src/external-patches/firefox/bug_2011236.patch
Normal file
166
src/external-patches/firefox/bug_2011236.patch
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
diff --git a/browser/components/sessionstore/test/browser.toml b/browser/components/sessionstore/test/browser.toml
|
||||||
|
--- a/browser/components/sessionstore/test/browser.toml
|
||||||
|
+++ b/browser/components/sessionstore/test/browser.toml
|
||||||
|
@@ -14,10 +14,12 @@
|
||||||
|
"browser_frame_history_c.html",
|
||||||
|
"browser_frame_history_c1.html",
|
||||||
|
"browser_frame_history_c2.html",
|
||||||
|
"browser_formdata_format_sample.html",
|
||||||
|
"browser_sessionHistory_slow.sjs",
|
||||||
|
+ "browser_policy_container_sample.html",
|
||||||
|
+ "browser_policy_container_sample_frame.html",
|
||||||
|
"browser_scrollPositions_sample.html",
|
||||||
|
"browser_scrollPositions_sample2.html",
|
||||||
|
"browser_scrollPositions_sample_frameset.html",
|
||||||
|
"browser_scrollPositions_readerModeArticle.html",
|
||||||
|
"browser_sessionStorage.html",
|
||||||
|
@@ -226,10 +228,12 @@
|
||||||
|
["browser_pinned_tabs.js"]
|
||||||
|
skip-if = [
|
||||||
|
"ccov", # Bug 1625525
|
||||||
|
]
|
||||||
|
|
||||||
|
+["browser_policy_container_not_stored.js"]
|
||||||
|
+
|
||||||
|
["browser_privatetabs.js"]
|
||||||
|
|
||||||
|
["browser_purge_domaindata.js"]
|
||||||
|
|
||||||
|
["browser_purge_shistory.js"]
|
||||||
|
diff --git a/browser/components/sessionstore/test/browser_policy_container_not_stored.js b/browser/components/sessionstore/test/browser_policy_container_not_stored.js
|
||||||
|
new file mode 100644
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/browser/components/sessionstore/test/browser_policy_container_not_stored.js
|
||||||
|
@@ -0,0 +1,51 @@
|
||||||
|
+/* 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";
|
||||||
|
+
|
||||||
|
+/**
|
||||||
|
+ * A session history entry only needs to retain the policy container for loads
|
||||||
|
+ * that inherit their policies (about:blank, about:srcdoc, blob:, data:, ...).
|
||||||
|
+ * Anything fetched over the network gets its policies from the response again,
|
||||||
|
+ * so storing them would only bloat the session store (bug 2011236).
|
||||||
|
+ */
|
||||||
|
+
|
||||||
|
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
|
||||||
|
+ "chrome://mochitests/content",
|
||||||
|
+ "https://example.com"
|
||||||
|
+);
|
||||||
|
+const PARENT_URL = TEST_ROOT + "browser_policy_container_sample.html";
|
||||||
|
+const FRAME_URL = TEST_ROOT + "browser_policy_container_sample_frame.html";
|
||||||
|
+
|
||||||
|
+add_task(async function test_policy_container_only_for_inheriting_loads() {
|
||||||
|
+ const tab = BrowserTestUtils.addTab(gBrowser, PARENT_URL);
|
||||||
|
+ gBrowser.selectedTab = tab;
|
||||||
|
+ await promiseBrowserLoaded(tab.linkedBrowser, true, PARENT_URL);
|
||||||
|
+ await TabStateFlusher.flush(tab.linkedBrowser);
|
||||||
|
+
|
||||||
|
+ const state = JSON.parse(ss.getTabState(tab));
|
||||||
|
+ const topEntry = state.entries.at(-1);
|
||||||
|
+ is(topEntry.url, PARENT_URL, "collected the parent entry");
|
||||||
|
+
|
||||||
|
+ const children = topEntry.children ?? [];
|
||||||
|
+ const networkFrame = children.find(child => child.url === FRAME_URL);
|
||||||
|
+ const blankFrame = children.find(child => child.url === "about:blank");
|
||||||
|
+
|
||||||
|
+ ok(networkFrame, "collected the network-scheme subframe entry");
|
||||||
|
+ ok(blankFrame, "collected the about:blank subframe entry");
|
||||||
|
+
|
||||||
|
+ // The parent has a CSP, so both subframes inherit a policy container onto
|
||||||
|
+ // their load state. Only the one that cannot recover it from a response
|
||||||
|
+ // should keep it in session history.
|
||||||
|
+ ok(
|
||||||
|
+ !("policyContainer" in networkFrame),
|
||||||
|
+ "https subframe entry does not store a policyContainer"
|
||||||
|
+ );
|
||||||
|
+ ok(
|
||||||
|
+ "policyContainer" in blankFrame,
|
||||||
|
+ "about:blank subframe entry still stores its inherited policyContainer"
|
||||||
|
+ );
|
||||||
|
+
|
||||||
|
+ await BrowserTestUtils.removeTab(tab);
|
||||||
|
+});
|
||||||
|
diff --git a/browser/components/sessionstore/test/browser_policy_container_sample.html b/browser/components/sessionstore/test/browser_policy_container_sample.html
|
||||||
|
new file mode 100644
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/browser/components/sessionstore/test/browser_policy_container_sample.html
|
||||||
|
@@ -0,0 +1,12 @@
|
||||||
|
+<!DOCTYPE html>
|
||||||
|
+<html>
|
||||||
|
+<head>
|
||||||
|
+ <meta charset="utf-8">
|
||||||
|
+ <meta http-equiv="Content-Security-Policy" content="default-src * data: blob: 'unsafe-inline'">
|
||||||
|
+ <title>policyContainer session store sample</title>
|
||||||
|
+</head>
|
||||||
|
+<body>
|
||||||
|
+ <iframe id="network" src="browser_policy_container_sample_frame.html"></iframe>
|
||||||
|
+ <iframe id="blank" src="about:blank"></iframe>
|
||||||
|
+</body>
|
||||||
|
+</html>
|
||||||
|
diff --git a/browser/components/sessionstore/test/browser_policy_container_sample_frame.html b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html
|
||||||
|
new file mode 100644
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html
|
||||||
|
@@ -0,0 +1,10 @@
|
||||||
|
+<!DOCTYPE html>
|
||||||
|
+<html>
|
||||||
|
+<head>
|
||||||
|
+ <meta charset="utf-8">
|
||||||
|
+ <title>policyContainer session store sample frame</title>
|
||||||
|
+</head>
|
||||||
|
+<body>
|
||||||
|
+ frame
|
||||||
|
+</body>
|
||||||
|
+</html>
|
||||||
|
diff --git a/docshell/shistory/SessionHistoryEntry.cpp b/docshell/shistory/SessionHistoryEntry.cpp
|
||||||
|
--- a/docshell/shistory/SessionHistoryEntry.cpp
|
||||||
|
+++ b/docshell/shistory/SessionHistoryEntry.cpp
|
||||||
|
@@ -41,10 +41,18 @@
|
||||||
|
extern mozilla::LazyLogModule gSHLog;
|
||||||
|
|
||||||
|
namespace mozilla {
|
||||||
|
namespace dom {
|
||||||
|
|
||||||
|
+// Only store policy container for loads that can't carry it themselves
|
||||||
|
+// (about:blank, about:srcdoc, blob:, data:, ...)
|
||||||
|
+// bug 1867137, bug 2011236
|
||||||
|
+static nsIPolicyContainer* PolicyContainerToStore(
|
||||||
|
+ nsIURI* aURI, nsIPolicyContainer* aPolicyContainer) {
|
||||||
|
+ return CSP_ShouldURIInheritCSP(aURI) ? aPolicyContainer : nullptr;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
SessionHistoryInfo::SessionHistoryInfo(nsDocShellLoadState* aLoadState,
|
||||||
|
nsIChannel* aChannel)
|
||||||
|
: mURI(aLoadState->URI()),
|
||||||
|
mOriginalURI(aLoadState->OriginalURI()),
|
||||||
|
mResultPrincipalURI(aLoadState->ResultPrincipalURI()),
|
||||||
|
@@ -59,9 +67,10 @@
|
||||||
|
mLoadReplace(aLoadState->LoadReplace()),
|
||||||
|
mHasUserActivation(aLoadState->HasValidUserGestureActivation()),
|
||||||
|
mSharedState(SharedState::Create(
|
||||||
|
aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(),
|
||||||
|
aLoadState->PartitionedPrincipalToInherit(),
|
||||||
|
- aLoadState->PolicyContainer(),
|
||||||
|
+ PolicyContainerToStore(aLoadState->URI(),
|
||||||
|
+ aLoadState->PolicyContainer()),
|
||||||
|
/* FIXME Is this correct? */
|
||||||
|
aLoadState->TypeHint())) {
|
||||||
|
// Pull the upload stream off of the channel instead of the load state, as
|
||||||
|
@@ -124,11 +133,12 @@
|
||||||
|
loadInfo->GetPrincipalToInherit(
|
||||||
|
getter_AddRefs(mSharedState.Get()->mPrincipalToInherit));
|
||||||
|
|
||||||
|
mSharedState.Get()->mPartitionedPrincipalToInherit =
|
||||||
|
aPartitionedPrincipalToInherit;
|
||||||
|
- mSharedState.Get()->mPolicyContainer = aPolicyContainer;
|
||||||
|
+ mSharedState.Get()->mPolicyContainer =
|
||||||
|
+ PolicyContainerToStore(mURI, aPolicyContainer);
|
||||||
|
aChannel->GetContentType(mSharedState.Get()->mContentType);
|
||||||
|
aChannel->GetOriginalURI(getter_AddRefs(mOriginalURI));
|
||||||
|
|
||||||
|
uint32_t loadFlags;
|
||||||
|
aChannel->GetLoadFlags(&loadFlags);
|
||||||
|
|
||||||
55
src/external-patches/firefox/issue_14710.patch
Normal file
55
src/external-patches/firefox/issue_14710.patch
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
diff --git a/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs b/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
|
||||||
|
--- a/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
|
||||||
|
+++ b/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
|
||||||
|
@@ -119,10 +119,19 @@
|
||||||
|
}
|
||||||
|
case "PictureInPicture:VideoTabHidden": {
|
||||||
|
if (!lazy.PIP_ENABLED || !lazy.PIP_WHEN_SWITCHING_TABS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+ // Suppress auto-toggle if the user deliberately closed this PiP window
|
||||||
|
+ if (
|
||||||
|
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.has(browser)
|
||||||
|
+ ) {
|
||||||
|
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.delete(browser);
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
// If the tab is still selected, then we can ignore this event
|
||||||
|
if (browser.documentGlobal.gBrowser.selectedBrowser == browser) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let actor = browsingContext.currentWindowGlobal.getActor(
|
||||||
|
@@ -261,10 +270,12 @@
|
||||||
|
maxConcurrentPlayerCount: 0,
|
||||||
|
|
||||||
|
// Maps auto pip browser to PictureInPictureParent actor
|
||||||
|
weakAutoPipBrowserToParent: new WeakMap(),
|
||||||
|
|
||||||
|
+ weakAutoPipBrowserClosedDeliberately: new WeakSet(),
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Returns the player window if one exists and if it hasn't yet been closed.
|
||||||
|
*
|
||||||
|
* @param {PictureInPictureParent} pipActorRef
|
||||||
|
* Reference to the calling PictureInPictureParent actor
|
||||||
|
@@ -899,10 +910,17 @@
|
||||||
|
const { reason, actorRef } = closeData;
|
||||||
|
const win = this.getWeakPipPlayer(actorRef);
|
||||||
|
if (!win) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+ const browser = this.weakWinToBrowser.get(win);
|
||||||
|
+
|
||||||
|
+ if (reason === "CloseButton") {
|
||||||
|
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.add(browser);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
this.removePiPBrowserFromWeakMap(this.weakWinToBrowser.get(win));
|
||||||
|
this.weakAutoPipBrowserToParent.delete(this.weakWinToBrowser.get(win));
|
||||||
|
|
||||||
|
Glean.pictureinpicture["closedMethod" + reason].record();
|
||||||
|
await this.closePipWindow(win);
|
||||||
|
|
||||||
@@ -7,14 +7,6 @@
|
|||||||
"id": "D299584",
|
"id": "D299584",
|
||||||
"name": "Native MacOS popovers fix"
|
"name": "Native MacOS popovers fix"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "phabricator",
|
|
||||||
"id": "D284404",
|
|
||||||
"name": "Add urlbar closeOnWindowBlur preference",
|
|
||||||
"replaces": {
|
|
||||||
"\n\n": "\n // may want to figure out a more robust way to detect abandonment."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "local",
|
"type": "local",
|
||||||
// TODO: Convert into https://phabricator.services.mozilla.com/D298079
|
// TODO: Convert into https://phabricator.services.mozilla.com/D298079
|
||||||
@@ -48,5 +40,21 @@
|
|||||||
"this._lastJsonLength = uncompressedBytes;": "this._lastJsonLength = uncompressedBytes.jsonLength;",
|
"this._lastJsonLength = uncompressedBytes;": "this._lastJsonLength = uncompressedBytes.jsonLength;",
|
||||||
" jsonLengthHint,": " lengthHint: jsonLengthHint,"
|
" jsonLengthHint,": " lengthHint: jsonLengthHint,"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "phabricator",
|
||||||
|
"id": "D320897",
|
||||||
|
"name": "Bug 2011236",
|
||||||
|
"replaces": {
|
||||||
|
// The upstream patch context includes a MOZ_DIAGNOSTIC_ASSERT that
|
||||||
|
// doesn't exist yet in our Firefox version.
|
||||||
|
"@@ -59,11 +67,12 @@": "@@ -59,9 +67,10 @@",
|
||||||
|
"aLoadState->TypeHint())) {\n MOZ_DIAGNOSTIC_ASSERT(!mURI->SchemeIs(\"javascript\"));\n \n // Pull": "aLoadState->TypeHint())) {\n // Pull"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "phabricator",
|
||||||
|
"id": "D321446",
|
||||||
|
"name": "Issue 14710"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
|
|
||||||
index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f06a4f8dbf 100644
|
|
||||||
--- a/services/sync/modules/service.sys.mjs
|
|
||||||
+++ b/services/sync/modules/service.sys.mjs
|
|
||||||
@@ -99,6 +99,10 @@ function getEngineModules() {
|
|
||||||
whenTrue: "ExtensionStorageEngineKinto",
|
|
||||||
whenFalse: "ExtensionStorageEngineBridge",
|
|
||||||
};
|
|
||||||
+ result.Spaces = {
|
|
||||||
+ module: "resource:///modules/zen/ZenWorkspacesSync.sys.mjs",
|
|
||||||
+ symbol: "ZenWorkspacesEngine",
|
|
||||||
+ };
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
14
src/testing/xpcshell/runxpcshelltests-py.patch
Normal file
14
src/testing/xpcshell/runxpcshelltests-py.patch
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py
|
||||||
|
index 403401613189ffb758037850dc6bbb1d55f194ab..cad28e245155eb54775e551885532026e56c5fa9 100755
|
||||||
|
--- a/testing/xpcshell/runxpcshelltests.py
|
||||||
|
+++ b/testing/xpcshell/runxpcshelltests.py
|
||||||
|
@@ -2275,6 +2275,9 @@ class XPCShellTests:
|
||||||
|
appDirKey = None
|
||||||
|
if "appname" in self.mozInfo:
|
||||||
|
appDirKey = self.mozInfo["appname"] + "-appdir"
|
||||||
|
+ if appDirKey == "zen-appdir":
|
||||||
|
+ # Inherited manifests use firefox-appdir.
|
||||||
|
+ appDirKey = "firefox-appdir"
|
||||||
|
|
||||||
|
# We have to do this before we run tests that depend on having the node
|
||||||
|
# http/2 server.
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
|
||||||
index 5702ff28cc22206f5ce16584dac8a78d816562ce..3d08ecc97ce5995b30d8a4af0c33df329b428008 100644
|
|
||||||
--- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
|
||||||
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
|
||||||
@@ -270,11 +270,29 @@ _ContextualIdentityService.prototype = {
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
- create(name, icon, color) {
|
|
||||||
+ create(name, icon, color, id = null) {
|
|
||||||
this.ensureDataReady();
|
|
||||||
|
|
||||||
- // Retrieve the next userContextId available.
|
|
||||||
- let userContextId = ++this._lastUserContextId;
|
|
||||||
+ if (id !== null) {
|
|
||||||
+ id = typeof id == "string" ? Number(id) : id;
|
|
||||||
+ if (!Number.isSafeInteger(id) || id <= 0) {
|
|
||||||
+ throw new Error(`Invalid userContextId '${id}'`);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ let userContextId;
|
|
||||||
+ if (
|
|
||||||
+ id !== null &&
|
|
||||||
+ !this._identities.some(identity => identity.userContextId === id)
|
|
||||||
+ ) {
|
|
||||||
+ userContextId = id;
|
|
||||||
+ this._lastUserContextId = Math.max(
|
|
||||||
+ this._lastUserContextId,
|
|
||||||
+ userContextId
|
|
||||||
+ );
|
|
||||||
+ } else {
|
|
||||||
+ userContextId = ++this._lastUserContextId;
|
|
||||||
+ }
|
|
||||||
|
|
||||||
// Throw an error if the next userContextId available is invalid (the one associated to
|
|
||||||
// MAX_USER_CONTEXT_ID is already reserved to "userContextIdInternal.webextStorageLocal", which
|
|
||||||
29
src/toolkit/content/widgets/panel-list/panel-list-mjs.patch
Normal file
29
src/toolkit/content/widgets/panel-list/panel-list-mjs.patch
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
diff --git a/toolkit/content/widgets/panel-list/panel-list.mjs b/toolkit/content/widgets/panel-list/panel-list.mjs
|
||||||
|
index 68ad2fafc8b31dfff541cf8381d9b9533e376d1c..fa62fec6bfb33338091fc06d3736fb76fd5c47c4 100644
|
||||||
|
--- a/toolkit/content/widgets/panel-list/panel-list.mjs
|
||||||
|
+++ b/toolkit/content/widgets/panel-list/panel-list.mjs
|
||||||
|
@@ -223,7 +223,12 @@ export class PanelList extends HTMLElement {
|
||||||
|
this.setAttribute("showing", "true");
|
||||||
|
// Tell the host element to hide any overflow in case the panel extends off
|
||||||
|
// the page before the alignment is set.
|
||||||
|
- hostElement.style.overflow = "hidden";
|
||||||
|
+ const hostHidesOverflow = !getComputedStyle(hostElement)
|
||||||
|
+ .overflow.split(" ")
|
||||||
|
+ .includes("visible");
|
||||||
|
+ if (!hostHidesOverflow) {
|
||||||
|
+ hostElement.style.overflow = "hidden";
|
||||||
|
+ }
|
||||||
|
|
||||||
|
// Wait for a layout flush, then find the bounds.
|
||||||
|
let {
|
||||||
|
@@ -344,7 +349,9 @@ export class PanelList extends HTMLElement {
|
||||||
|
// Set the alignments and show the panel.
|
||||||
|
this.setAttribute("align", align);
|
||||||
|
this.setAttribute("valign", valign);
|
||||||
|
- hostElement.style.overflow = "";
|
||||||
|
+ if (!hostHidesOverflow) {
|
||||||
|
+ hostElement.style.overflow = "";
|
||||||
|
+ }
|
||||||
|
// Decide positioning based on where this panel will be rendered
|
||||||
|
const offsetParentIsBody =
|
||||||
|
this.supportsPopover() ||
|
||||||
@@ -832,8 +832,9 @@ window.gZenCompactModeManager = {
|
|||||||
|
|
||||||
const onLeave = event => {
|
const onLeave = event => {
|
||||||
if (AppConstants.platform == "macosx") {
|
if (AppConstants.platform == "macosx") {
|
||||||
const buttonRect =
|
const buttonRect = window.windowUtils.getBoundsWithoutFlushing(
|
||||||
gZenVerticalTabsManager.actualWindowButtons.getBoundingClientRect();
|
gZenVerticalTabsManager.actualWindowButtons
|
||||||
|
);
|
||||||
const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x;
|
const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x;
|
||||||
const MAC_WINDOW_BUTTONS_Y_BORDER = buttonRect.height + buttonRect.y;
|
const MAC_WINDOW_BUTTONS_Y_BORDER = buttonRect.height + buttonRect.y;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ class ZenMediaCard {
|
|||||||
|
|
||||||
onToggle() {
|
onToggle() {
|
||||||
if (this.controller?.isPlaying) {
|
if (this.controller?.isPlaying) {
|
||||||
this.controller.pause();
|
this.controller.pause("user");
|
||||||
} else {
|
} else {
|
||||||
this.controller?.play();
|
this.controller?.play();
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ class ZenMediaCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSeekDrag(event) {
|
onSeekDrag(event) {
|
||||||
this.controller?.pause();
|
this.controller?.pause("user");
|
||||||
const newTime = (event.target.value / 100) * this.duration;
|
const newTime = (event.target.value / 100) * this.duration;
|
||||||
this.currentTimeEl.textContent = this.formatSecondsToTime(newTime);
|
this.currentTimeEl.textContent = this.formatSecondsToTime(newTime);
|
||||||
}
|
}
|
||||||
@@ -408,7 +408,7 @@ class ZenMediaCard {
|
|||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
if (this.controller) {
|
if (this.controller) {
|
||||||
this.controller.pause();
|
this.controller.pause("user");
|
||||||
} else {
|
} else {
|
||||||
const webRTC =
|
const webRTC =
|
||||||
this.browser.browsingContext.currentWindowGlobal.getActor("WebRTC");
|
this.browser.browsingContext.currentWindowGlobal.getActor("WebRTC");
|
||||||
|
|||||||
@@ -21,6 +21,5 @@ DIRS += [
|
|||||||
"share",
|
"share",
|
||||||
"spaces",
|
"spaces",
|
||||||
"space-routing",
|
"space-routing",
|
||||||
"sync",
|
|
||||||
"window-drag",
|
"window-drag",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const lazy = {};
|
|||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
ZenLiveFoldersManager:
|
ZenLiveFoldersManager:
|
||||||
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
|
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
|
||||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
|
||||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||||
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
||||||
@@ -611,7 +610,6 @@ export class nsZenSessionManager {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.#collectWindowData(windows);
|
this.#collectWindowData(windows);
|
||||||
lazy.ZenSyncStore.notifyAboutChanges();
|
|
||||||
// This would save the data to disk asynchronously or when quitting the app.
|
// This would save the data to disk asynchronously or when quitting the app.
|
||||||
let sidebar = this.#sidebarWithoutCloning;
|
let sidebar = this.#sidebarWithoutCloning;
|
||||||
this.#file.data = sidebar;
|
this.#file.data = sidebar;
|
||||||
@@ -625,15 +623,6 @@ export class nsZenSessionManager {
|
|||||||
this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`);
|
this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the sidebar data object.
|
|
||||||
*
|
|
||||||
* @returns {object}
|
|
||||||
*/
|
|
||||||
getSidebarData() {
|
|
||||||
return this.#sidebarWithoutCloning;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the last known backup should be deleted and a new one
|
* Called when the last known backup should be deleted and a new one
|
||||||
* created. This uses the #deferredBackupTask to debounce clusters of
|
* created. This uses the #deferredBackupTask to debounce clusters of
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class nsZenSpaceRoutingManager {
|
|||||||
const element = window.MozXULElement.parseXULToFragment(`
|
const element = window.MozXULElement.parseXULToFragment(`
|
||||||
<menuseparator/>
|
<menuseparator/>
|
||||||
<menuitem id="context_zen-add-domain-to-routing"
|
<menuitem id="context_zen-add-domain-to-routing"
|
||||||
data-lazy-l10n-id="tab-context-zen-add-domain-to-sr"
|
data-l10n-id="tab-context-zen-add-domain-to-sr"
|
||||||
data-l10n-args='{"tabCount": 1}'/>
|
data-l10n-args='{"tabCount": 1}'/>
|
||||||
`);
|
`);
|
||||||
window.document.getElementById("context_undoCloseTab").after(element);
|
window.document.getElementById("context_undoCloseTab").after(element);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const lazy = {};
|
|||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
||||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
|
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
|
||||||
@@ -153,7 +152,7 @@ class nsZenWorkspaces {
|
|||||||
if (!this.privateWindowOrDisabled) {
|
if (!this.privateWindowOrDisabled) {
|
||||||
const observerFunction = async () => {
|
const observerFunction = async () => {
|
||||||
delete this._workspaceBookmarksCache;
|
delete this._workspaceBookmarksCache;
|
||||||
await this.#initializeWorkspaceBookmarks();
|
await this.workspaceBookmarks();
|
||||||
this._invalidateBookmarkContainers();
|
this._invalidateBookmarkContainers();
|
||||||
};
|
};
|
||||||
Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
|
Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
|
||||||
@@ -173,63 +172,6 @@ class nsZenWorkspaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies live sync changes: updates workspace cache, removes deleted items,
|
|
||||||
* then creates/updates pulled items.
|
|
||||||
*
|
|
||||||
* @param {{ spaces: Array}} pulled Reconcile-pulled items.
|
|
||||||
* @param {{ spaces: Array}} removals Items to remove.
|
|
||||||
*/
|
|
||||||
async _applySyncChanges(pulled, removals = {}) {
|
|
||||||
if (!this.shouldHaveWorkspaces || this.privateWindowOrDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.promiseInitialized;
|
|
||||||
|
|
||||||
// 1. Update workspace cache (remove deleted, merge pulled)
|
|
||||||
const removedSpaceIds = new Set((removals.spaces || []).map(s => s.uuid));
|
|
||||||
if (removedSpaceIds.size || pulled.spaces?.length) {
|
|
||||||
const localMap = new Map(
|
|
||||||
this.getWorkspaces()
|
|
||||||
.filter(w => !removedSpaceIds.has(w.uuid))
|
|
||||||
.map(w => [w.uuid, w])
|
|
||||||
);
|
|
||||||
for (const space of pulled.spaces || []) {
|
|
||||||
if (!space?.uuid) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const existing = localMap.get(space.uuid);
|
|
||||||
localMap.set(space.uuid, existing ? { ...existing, ...space } : space);
|
|
||||||
}
|
|
||||||
await this.propagateWorkspaces(
|
|
||||||
this.#getOrderedWorkspacesByPosition(Array.from(localMap.values()))
|
|
||||||
);
|
|
||||||
this.#propagateWorkspaceData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#getOrderedWorkspacesByPosition(workspaces) {
|
|
||||||
return [...workspaces]
|
|
||||||
.map((workspace, index) => ({ workspace, index }))
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aPosition =
|
|
||||||
typeof a.workspace.position === "number"
|
|
||||||
? a.workspace.position
|
|
||||||
: a.index;
|
|
||||||
const bPosition =
|
|
||||||
typeof b.workspace.position === "number"
|
|
||||||
? b.workspace.position
|
|
||||||
: b.index;
|
|
||||||
return aPosition - bPosition || a.index - b.index;
|
|
||||||
})
|
|
||||||
.map(({ workspace }) => {
|
|
||||||
// strip the position property that comes from pulled workspaces
|
|
||||||
const rest = { ...workspace };
|
|
||||||
delete rest.position;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#afterLoadInit() {
|
#afterLoadInit() {
|
||||||
const onResize = (...args) => {
|
const onResize = (...args) => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -757,17 +699,17 @@ class nsZenWorkspaces {
|
|||||||
return spacesForSS;
|
return spacesForSS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initializeWorkspaceBookmarks() {
|
async workspaceBookmarks() {
|
||||||
if (this.privateWindowOrDisabled) {
|
if (this.privateWindowOrDisabled) {
|
||||||
this._workspaceBookmarksCache = {
|
this._workspaceBookmarksCache = {
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
lastChangeTimestamp: 0,
|
lastChangeTimestamp: 0,
|
||||||
};
|
};
|
||||||
return;
|
return this._workspaceBookmarksCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._workspaceBookmarksCache) {
|
if (this._workspaceBookmarksCache) {
|
||||||
return;
|
return this._workspaceBookmarksCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [bookmarks, lastChangeTimestamp] = await Promise.all([
|
const [bookmarks, lastChangeTimestamp] = await Promise.all([
|
||||||
@@ -776,6 +718,8 @@ class nsZenWorkspaces {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };
|
this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };
|
||||||
|
|
||||||
|
return this._workspaceCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreWorkspacesFromSessionStore(aWinData = {}) {
|
restoreWorkspacesFromSessionStore(aWinData = {}) {
|
||||||
@@ -836,7 +780,7 @@ class nsZenWorkspaces {
|
|||||||
return (async () => {
|
return (async () => {
|
||||||
await this.#waitForPromises();
|
await this.#waitForPromises();
|
||||||
this.#afterLoadInit();
|
this.#afterLoadInit();
|
||||||
await this.#initializeWorkspaceBookmarks();
|
await this.workspaceBookmarks();
|
||||||
await this.changeWorkspace(activeWorkspace, { onInit: true });
|
await this.changeWorkspace(activeWorkspace, { onInit: true });
|
||||||
this.#fixTabPositions();
|
this.#fixTabPositions();
|
||||||
this.onWindowResize();
|
this.onWindowResize();
|
||||||
@@ -862,13 +806,6 @@ class nsZenWorkspaces {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
#markWorkspaceChanged(workspaceId) {
|
|
||||||
lazy.ZenSyncStore.markItemChanged({
|
|
||||||
type: "space",
|
|
||||||
id: workspaceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectStartPage() {
|
async selectStartPage() {
|
||||||
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
|
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
|
||||||
return;
|
return;
|
||||||
@@ -1297,19 +1234,12 @@ class nsZenWorkspaces {
|
|||||||
} else {
|
} else {
|
||||||
workspacesData.push(workspaceData);
|
workspacesData.push(workspaceData);
|
||||||
}
|
}
|
||||||
// mark item as changed for sync
|
|
||||||
this.#markWorkspaceChanged(workspaceData.uuid);
|
|
||||||
|
|
||||||
this.#propagateWorkspaceData();
|
this.#propagateWorkspaceData();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeWorkspace(windowID) {
|
removeWorkspace(windowID) {
|
||||||
let { promise, resolve } = Promise.withResolvers();
|
let { promise, resolve } = Promise.withResolvers();
|
||||||
this.#deleteWorkspaceOwnedTabs(windowID);
|
this.#deleteWorkspaceOwnedTabs(windowID);
|
||||||
|
|
||||||
// mark item as changed for sync
|
|
||||||
this.#markWorkspaceChanged(windowID);
|
|
||||||
|
|
||||||
let workspacesData = this.getWorkspaces();
|
let workspacesData = this.getWorkspaces();
|
||||||
// Remove the workspace from the cache
|
// Remove the workspace from the cache
|
||||||
workspacesData = workspacesData.filter(
|
workspacesData = workspacesData.filter(
|
||||||
@@ -1441,50 +1371,30 @@ class nsZenWorkspaces {
|
|||||||
if (this.privateWindowOrDisabled) {
|
if (this.privateWindowOrDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspaces = this.getWorkspaces();
|
const workspaces = this._workspaceCache;
|
||||||
// Track previous positions so we only notify observers for workspaces whose
|
|
||||||
// position changed during the reorder.
|
|
||||||
const previousPositions = new Map(
|
|
||||||
workspaces.map((workspace, index) => [workspace.uuid, index])
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspace = workspaces.find(w => w.uuid === id);
|
const workspace = workspaces.find(w => w.uuid === id);
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
console.warn(`Workspace with ID ${id} not found for reordering.`);
|
console.warn(`Workspace with ID ${id} not found for reordering.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the workspace from its current position
|
// Remove the workspace from its current position
|
||||||
const currentIndex = workspaces.indexOf(workspace);
|
const currentIndex = workspaces.indexOf(workspace);
|
||||||
if (currentIndex === -1) {
|
if (currentIndex === -1) {
|
||||||
console.warn(`Workspace with ID ${id} not found in the list.`);
|
console.warn(`Workspace with ID ${id} not found in the list.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
workspaces.splice(currentIndex, 1);
|
||||||
// Insert the workspace at the new position
|
// Insert the workspace at the new position
|
||||||
if (newPosition < 0 || newPosition >= workspaces.length) {
|
if (newPosition < 0 || newPosition > workspaces.length) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Invalid position ${newPosition} for reordering workspace with ID ${id}.`
|
`Invalid position ${newPosition} for reordering workspace with ID ${id}.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaces.splice(currentIndex, 1);
|
|
||||||
workspaces.splice(newPosition, 0, workspace);
|
workspaces.splice(newPosition, 0, workspace);
|
||||||
|
|
||||||
// Propagate the changes if the order has changed
|
// Propagate the changes if the order has changed
|
||||||
if (currentIndex !== newPosition) {
|
if (currentIndex !== newPosition) {
|
||||||
this._workspaceCache = workspaces;
|
this.#propagateWorkspaceData();
|
||||||
|
|
||||||
for (const [i, ws] of workspaces.entries()) {
|
|
||||||
if (previousPositions.get(ws.uuid) === i) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// mark item as changed for sync
|
|
||||||
this.#markWorkspaceChanged(ws.uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#propagateWorkspaceData(workspaces);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1790,6 +1700,15 @@ class nsZenWorkspaces {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeSureEmptyTabIsFirst() {
|
makeSureEmptyTabIsFirst() {
|
||||||
|
if (
|
||||||
|
gZenUIManager.testingEnabled &&
|
||||||
|
this._emptyTab &&
|
||||||
|
(this._emptyTab.closing || !this._emptyTab.isConnected)
|
||||||
|
) {
|
||||||
|
this._emptyTab = gBrowser.tabs.find(
|
||||||
|
tab => tab.hasAttribute("zen-empty-tab") && !tab.closing
|
||||||
|
);
|
||||||
|
}
|
||||||
const emptyTab = this._emptyTab;
|
const emptyTab = this._emptyTab;
|
||||||
if (emptyTab) {
|
if (emptyTab) {
|
||||||
emptyTab.setAttribute("zen-workspace-id", this.activeWorkspace);
|
emptyTab.setAttribute("zen-workspace-id", this.activeWorkspace);
|
||||||
@@ -2459,7 +2378,7 @@ class nsZenWorkspaces {
|
|||||||
for (const tab of gBrowser.tabs) {
|
for (const tab of gBrowser.tabs) {
|
||||||
if (
|
if (
|
||||||
!tab.hasAttribute("zen-workspace-id") &&
|
!tab.hasAttribute("zen-workspace-id") &&
|
||||||
!tab.hasAttribute("zen-essential")
|
!tab.hasAttribute("zen-workspace-id")
|
||||||
) {
|
) {
|
||||||
tab.setAttribute("zen-workspace-id", workspace.uuid);
|
tab.setAttribute("zen-workspace-id", workspace.uuid);
|
||||||
}
|
}
|
||||||
@@ -3320,8 +3239,10 @@ class nsZenWorkspaces {
|
|||||||
return width;
|
return width;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Check if the total width exceeds the parent's width
|
// Check if the total width exceeds the parent's width.
|
||||||
if (totalWidth > parent.clientWidth) {
|
const parentWidth =
|
||||||
|
window.windowUtils.getBoundsWithoutFlushing(parent).width;
|
||||||
|
if (totalWidth > parentWidth) {
|
||||||
parent.setAttribute("icons-overflow", "true");
|
parent.setAttribute("icons-overflow", "true");
|
||||||
} else {
|
} else {
|
||||||
parent.removeAttribute("icons-overflow");
|
parent.removeAttribute("icons-overflow");
|
||||||
@@ -3329,7 +3250,7 @@ class nsZenWorkspaces {
|
|||||||
|
|
||||||
// Set the width of each icon to the maximum size they can fit on
|
// Set the width of each icon to the maximum size they can fit on
|
||||||
const widthPerButton = Math.max(
|
const widthPerButton = Math.max(
|
||||||
(parent.clientWidth - separation * (parent.children.length - 1)) /
|
(parentWidth - separation * (parent.children.length - 1)) /
|
||||||
parent.children.length,
|
parent.children.length,
|
||||||
minButtonSize
|
minButtonSize
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -230,7 +230,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
|||||||
}
|
}
|
||||||
this.removeGroup(groupIndex);
|
this.removeGroup(groupIndex);
|
||||||
if (changeTab) {
|
if (changeTab) {
|
||||||
gBrowser.selectedTab = remainingTabs[0];
|
const tabToSelect = remainingTabs.find(remaining => remaining !== tab);
|
||||||
|
if (tabToSelect) {
|
||||||
|
gBrowser.selectedTab = tabToSelect;
|
||||||
|
}
|
||||||
document
|
document
|
||||||
.getElementById("cmd_zenNewEmptySplit")
|
.getElementById("cmd_zenNewEmptySplit")
|
||||||
.removeAttribute("disabled");
|
.removeAttribute("disabled");
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
/* 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 lazy = {};
|
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
|
||||||
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
|
||||||
ContextualIdentityService:
|
|
||||||
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
|
||||||
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
|
|
||||||
});
|
|
||||||
|
|
||||||
function normalizeUserContextId(value) {
|
|
||||||
const normalized = typeof value === "string" ? Number(value) : value;
|
|
||||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ZenSyncManager {
|
|
||||||
getSidebarData() {
|
|
||||||
return lazy.ZenSessionStore.getSidebarData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to ignore changes to items. This is used to prevent
|
|
||||||
* infinite loops when applying incoming sync changes.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
#ignoreChanges = false;
|
|
||||||
|
|
||||||
#changedItems = new Map();
|
|
||||||
|
|
||||||
markItemChanged(item) {
|
|
||||||
if (item.type && item.id && !this.#ignoreChanges) {
|
|
||||||
const key = `${item.type}~${item.id}`;
|
|
||||||
this.#changedItems.set(key, { type: item.type, id: item.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#getChangedItems() {
|
|
||||||
return Array.from(this.#changedItems.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
#clearChangedItems() {
|
|
||||||
this.#changedItems.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyAboutChanges() {
|
|
||||||
const changedItems = this.#getChangedItems();
|
|
||||||
|
|
||||||
for (const item of changedItems) {
|
|
||||||
Services.obs.notifyObservers(
|
|
||||||
{ wrappedJSObject: item },
|
|
||||||
"zen-workspace-item-changed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.#clearChangedItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyIncomingBatch(pulled, removals) {
|
|
||||||
try {
|
|
||||||
this.#ignoreChanges = true;
|
|
||||||
this.#applyIncomingContainers(
|
|
||||||
pulled.containers || [],
|
|
||||||
removals.containers || []
|
|
||||||
);
|
|
||||||
|
|
||||||
const win = lazy.ZenWindowSync.firstSyncedWindow;
|
|
||||||
if (win?.gZenWorkspaces) {
|
|
||||||
await win.gZenWorkspaces._applySyncChanges(pulled, removals);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("ZenSyncManager: Failed to apply incoming sync data:", e);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
this.#ignoreChanges = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#applyIncomingContainers(pulledContainers, removedContainers) {
|
|
||||||
const localContainersById = new Map(
|
|
||||||
lazy.ContextualIdentityService.getPublicIdentities().map(container => [
|
|
||||||
container.userContextId,
|
|
||||||
container,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const container of pulledContainers) {
|
|
||||||
if (!container.name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userContextId = normalizeUserContextId(container.userContextId);
|
|
||||||
if (userContextId === null) {
|
|
||||||
console.warn(
|
|
||||||
"ZenSyncManager: Ignoring incoming container with invalid userContextId",
|
|
||||||
{ container }
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existsLocally = localContainersById.has(userContextId);
|
|
||||||
|
|
||||||
if (existsLocally) {
|
|
||||||
lazy.ContextualIdentityService.update(
|
|
||||||
userContextId,
|
|
||||||
container.name,
|
|
||||||
container.icon,
|
|
||||||
container.color
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdIdentity = lazy.ContextualIdentityService.create(
|
|
||||||
container.name,
|
|
||||||
container.icon,
|
|
||||||
container.color,
|
|
||||||
userContextId
|
|
||||||
);
|
|
||||||
if (createdIdentity) {
|
|
||||||
localContainersById.set(createdIdentity.userContextId, createdIdentity);
|
|
||||||
}
|
|
||||||
if (createdIdentity && createdIdentity.userContextId !== userContextId) {
|
|
||||||
console.warn("ZenSyncManager: Container sync created unexpected ID", {
|
|
||||||
requestedId: userContextId,
|
|
||||||
createdId: createdIdentity.userContextId,
|
|
||||||
name: container.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const container of removedContainers) {
|
|
||||||
const userContextId = normalizeUserContextId(container.userContextId);
|
|
||||||
if (userContextId === null) {
|
|
||||||
console.warn(
|
|
||||||
"ZenSyncManager: Ignoring container removal with invalid userContextId",
|
|
||||||
{ container }
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localContainersById.has(userContextId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
lazy.ContextualIdentityService.remove(userContextId);
|
|
||||||
localContainersById.delete(userContextId);
|
|
||||||
} catch {
|
|
||||||
// Container may already be gone locally.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZenSyncStore = new ZenSyncManager();
|
|
||||||
@@ -1,404 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
import {
|
|
||||||
Store,
|
|
||||||
SyncEngine,
|
|
||||||
Tracker,
|
|
||||||
} from "resource://services-sync/engines.sys.mjs";
|
|
||||||
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
|
|
||||||
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
|
|
||||||
|
|
||||||
const lazy = {};
|
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
|
||||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
|
||||||
ContextualIdentityService:
|
|
||||||
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
|
||||||
});
|
|
||||||
|
|
||||||
const RECORD_ID_PREFIX_BY_TYPE = Object.freeze({
|
|
||||||
space: "s",
|
|
||||||
container: "c",
|
|
||||||
});
|
|
||||||
|
|
||||||
const RECORD_TYPE_BY_PREFIX = Object.freeze({
|
|
||||||
s: "space",
|
|
||||||
c: "container",
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync record wrapper for workspace and container items stored in the
|
|
||||||
* Workspaces engine collection.
|
|
||||||
*/
|
|
||||||
export class ZenWorkspacesRecord extends CryptoWrapper {
|
|
||||||
_logName = "Sync.Record.ZenSpaces";
|
|
||||||
}
|
|
||||||
|
|
||||||
ZenWorkspacesRecord.prototype.type = "spaces";
|
|
||||||
|
|
||||||
function parseRecordId(id) {
|
|
||||||
const sep = id.indexOf("~");
|
|
||||||
if (sep === -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const prefix = id.slice(0, sep);
|
|
||||||
const key = id.slice(sep + 1);
|
|
||||||
return { type: RECORD_TYPE_BY_PREFIX[prefix] || prefix, key };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createRecordId(type, id) {
|
|
||||||
const prefix = RECORD_ID_PREFIX_BY_TYPE[type];
|
|
||||||
if (!prefix) {
|
|
||||||
throw new Error(`Unknown Spaces Sync record type: ${type}`);
|
|
||||||
}
|
|
||||||
return `${prefix}~${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeUserContextId(value) {
|
|
||||||
const normalized = typeof value === "string" ? Number(value) : value;
|
|
||||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips the sync-envelope fields (`id` and `type`) from incoming record data
|
|
||||||
* and restores the item's real identity key where needed
|
|
||||||
*
|
|
||||||
* @param {object} data
|
|
||||||
*/
|
|
||||||
function stripSyncFields(data) {
|
|
||||||
const rest = { ...data };
|
|
||||||
delete rest.id;
|
|
||||||
delete rest.type;
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync store implementation that serializes local workspace and container
|
|
||||||
* state into records and applies incoming remote changes.
|
|
||||||
*/
|
|
||||||
class ZenWorkspacesStore extends Store {
|
|
||||||
constructor(name, engine) {
|
|
||||||
super(name, engine);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllIDs() {
|
|
||||||
const ids = {};
|
|
||||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
|
||||||
|
|
||||||
for (const space of sidebar.spaces || []) {
|
|
||||||
if (space.uuid) {
|
|
||||||
ids[createRecordId("space", space.uuid)] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const c of lazy.ContextualIdentityService.getPublicIdentities()) {
|
|
||||||
ids[createRecordId("container", c.userContextId)] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
async itemExists(id) {
|
|
||||||
const parsed = parseRecordId(id);
|
|
||||||
if (!parsed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
|
||||||
|
|
||||||
switch (parsed.type) {
|
|
||||||
case "space":
|
|
||||||
return (sidebar.spaces || []).some(s => s.uuid === parsed.key);
|
|
||||||
case "container":
|
|
||||||
return lazy.ContextualIdentityService.getPublicIdentities().some(
|
|
||||||
c => String(c.userContextId) === parsed.key
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRecord(id, collection) {
|
|
||||||
const record = new ZenWorkspacesRecord(collection, id);
|
|
||||||
const parsed = parseRecordId(id);
|
|
||||||
if (!parsed) {
|
|
||||||
record.deleted = true;
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
|
||||||
|
|
||||||
switch (parsed.type) {
|
|
||||||
case "space": {
|
|
||||||
const spaces = sidebar.spaces || [];
|
|
||||||
const idx = spaces.findIndex(s => s.uuid === parsed.key);
|
|
||||||
if (idx === -1) {
|
|
||||||
record.deleted = true;
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
const rest = { ...spaces[idx] };
|
|
||||||
delete rest.syncStatus;
|
|
||||||
record.cleartext = { id, type: "space", ...rest, position: idx };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "container": {
|
|
||||||
const container =
|
|
||||||
lazy.ContextualIdentityService.getPublicIdentities().find(
|
|
||||||
c => String(c.userContextId) === parsed.key
|
|
||||||
);
|
|
||||||
if (!container) {
|
|
||||||
record.deleted = true;
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
record.cleartext = {
|
|
||||||
id,
|
|
||||||
type: "container",
|
|
||||||
userContextId: container.userContextId,
|
|
||||||
name: container.name,
|
|
||||||
icon: container.icon,
|
|
||||||
color: container.color,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
record.deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyIncomingBatch(records, _countTelemetry) {
|
|
||||||
const pulled = { spaces: [], containers: [] };
|
|
||||||
const removals = { spaces: [], containers: [] };
|
|
||||||
|
|
||||||
for (const record of records) {
|
|
||||||
if (record.deleted) {
|
|
||||||
this._collectRemoval(record.id, removals);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const data = record.cleartext;
|
|
||||||
if (!data?.type) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const clean = stripSyncFields(data);
|
|
||||||
switch (data.type) {
|
|
||||||
case "space":
|
|
||||||
pulled.spaces.push(clean);
|
|
||||||
break;
|
|
||||||
case "container":
|
|
||||||
pulled.containers.push(clean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress change tracking while applying incoming data to prevent
|
|
||||||
// feedback loops where applied items get re-uploaded immediately.
|
|
||||||
this.engine._tracker.ignoreAll = true;
|
|
||||||
try {
|
|
||||||
await lazy.ZenSyncStore.applyIncomingBatch(pulled, removals);
|
|
||||||
} finally {
|
|
||||||
this.engine._tracker.ignoreAll = false;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectRemoval(id, removals) {
|
|
||||||
const parsed = parseRecordId(id);
|
|
||||||
if (!parsed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (parsed.type) {
|
|
||||||
case "space":
|
|
||||||
removals.spaces.push({ uuid: parsed.key });
|
|
||||||
break;
|
|
||||||
case "container": {
|
|
||||||
const userContextId = normalizeUserContextId(parsed.key);
|
|
||||||
if (userContextId === null) {
|
|
||||||
console.warn(
|
|
||||||
"ZenWorkspacesStore: Ignoring container removal with invalid userContextId",
|
|
||||||
{ id }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
removals.containers.push({ userContextId });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(record) {
|
|
||||||
await this._applySingle(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(record) {
|
|
||||||
await this._applySingle(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _applySingle(record) {
|
|
||||||
this.engine._tracker.ignoreAll = true;
|
|
||||||
try {
|
|
||||||
if (record.deleted) {
|
|
||||||
const removals = { spaces: [], containers: [] };
|
|
||||||
this._collectRemoval(record.id, removals);
|
|
||||||
await lazy.ZenSyncStore.applyIncomingBatch(
|
|
||||||
{ spaces: [], containers: [] },
|
|
||||||
removals
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = record.cleartext;
|
|
||||||
if (!data?.type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const clean = stripSyncFields(data);
|
|
||||||
const pulled = { spaces: [], containers: [] };
|
|
||||||
switch (data.type) {
|
|
||||||
case "space":
|
|
||||||
pulled.spaces.push(clean);
|
|
||||||
break;
|
|
||||||
case "container":
|
|
||||||
pulled.containers.push(clean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await lazy.ZenSyncStore.applyIncomingBatch(pulled, {
|
|
||||||
spaces: [],
|
|
||||||
containers: [],
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.engine._tracker.ignoreAll = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove() {
|
|
||||||
// No-op: never delete user data on wipe
|
|
||||||
}
|
|
||||||
|
|
||||||
async wipe() {
|
|
||||||
// No-op: never delete user data on wipe
|
|
||||||
}
|
|
||||||
|
|
||||||
changeItemID() {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync tracker that watches workspace and contextual identity observers and
|
|
||||||
* marks the corresponding record IDs as changed.
|
|
||||||
*/
|
|
||||||
class ZenWorkspacesTracker extends Tracker {
|
|
||||||
#changedIDs = {};
|
|
||||||
#ignoreAll = false;
|
|
||||||
|
|
||||||
get ignoreAll() {
|
|
||||||
return this.#ignoreAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
set ignoreAll(value) {
|
|
||||||
this.#ignoreAll = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
onStart() {
|
|
||||||
Services.obs.addObserver(this, "zen-workspace-item-changed");
|
|
||||||
Services.obs.addObserver(this, "contextual-identity-created");
|
|
||||||
Services.obs.addObserver(this, "contextual-identity-updated");
|
|
||||||
Services.obs.addObserver(this, "contextual-identity-deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
onStop() {
|
|
||||||
Services.obs.removeObserver(this, "zen-workspace-item-changed");
|
|
||||||
Services.obs.removeObserver(this, "contextual-identity-created");
|
|
||||||
Services.obs.removeObserver(this, "contextual-identity-updated");
|
|
||||||
Services.obs.removeObserver(this, "contextual-identity-deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
observe(subject, topic, _data) {
|
|
||||||
if (this.#ignoreAll) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (topic === "zen-workspace-item-changed") {
|
|
||||||
const type = subject?.wrappedJSObject?.type;
|
|
||||||
const id = subject?.wrappedJSObject?.id;
|
|
||||||
if (type && id) {
|
|
||||||
this._trackChange({ type, id });
|
|
||||||
}
|
|
||||||
} else if (topic.startsWith("contextual-identity-")) {
|
|
||||||
const id = subject?.wrappedJSObject?.userContextId;
|
|
||||||
if (id && normalizeUserContextId(id) !== null) {
|
|
||||||
this._trackChange({ type: "container", id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_trackChange(data) {
|
|
||||||
if (data.type && data.id) {
|
|
||||||
const id = createRecordId(data.type, data.id);
|
|
||||||
this.#changedIDs[id] = Date.now() / 1000;
|
|
||||||
this.score += SCORE_INCREMENT_XLARGE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChangedIDs() {
|
|
||||||
return { ...this.#changedIDs };
|
|
||||||
}
|
|
||||||
|
|
||||||
async addChangedID(id, when) {
|
|
||||||
this.#changedIDs[id] = when;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeChangedID(...ids) {
|
|
||||||
for (const id of ids) {
|
|
||||||
delete this.#changedIDs[id];
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearChangedIDs() {
|
|
||||||
this.#changedIDs = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync engine entrypoint that wires the Workspaces record, store, and tracker
|
|
||||||
* implementations into Firefox Sync.
|
|
||||||
*/
|
|
||||||
export class ZenWorkspacesEngine extends SyncEngine {
|
|
||||||
static get name() {
|
|
||||||
return "Spaces";
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(service) {
|
|
||||||
super("Spaces", service);
|
|
||||||
}
|
|
||||||
|
|
||||||
get _storeObj() {
|
|
||||||
return ZenWorkspacesStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _trackerObj() {
|
|
||||||
return ZenWorkspacesTracker;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _recordObj() {
|
|
||||||
return ZenWorkspacesRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
get version() {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
get syncPriority() {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
get allowSkippedRecord() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
EXTRA_JS_MODULES.zen += [
|
|
||||||
"ZenSyncManager.sys.mjs",
|
|
||||||
"ZenWorkspacesSync.sys.mjs",
|
|
||||||
]
|
|
||||||
@@ -538,8 +538,7 @@
|
|||||||
|
|
||||||
:root[zen-single-toolbar="true"]:not([zen-has-empty-tab="true"]) & {
|
:root[zen-single-toolbar="true"]:not([zen-has-empty-tab="true"]) & {
|
||||||
& #urlbar:not([breakout-extend]) .urlbar-input-container {
|
& #urlbar:not([breakout-extend]) .urlbar-input-container {
|
||||||
padding-left: 8px;
|
padding-inline: 4px;
|
||||||
padding-right: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,36 @@
|
|||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# 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/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
[alerts]
|
||||||
|
source = "browser/base/content/test/alerts"
|
||||||
|
is_direct_path = true
|
||||||
|
|
||||||
|
[backforward]
|
||||||
|
source = "browser/base/content/test/backforward"
|
||||||
|
is_direct_path = true
|
||||||
|
|
||||||
|
[caps]
|
||||||
|
source = "browser/base/content/test/caps"
|
||||||
|
is_direct_path = true
|
||||||
|
|
||||||
|
[gesture]
|
||||||
|
source = "browser/base/content/test/gesture"
|
||||||
|
is_direct_path = true
|
||||||
|
|
||||||
|
[gesture.replace-manifest]
|
||||||
|
"[DEFAULT]" = '''[DEFAULT]
|
||||||
|
prefs = [
|
||||||
|
"zen.window-sync.enabled=false",
|
||||||
|
]'''
|
||||||
|
|
||||||
|
[linkHandling]
|
||||||
|
source = "browser/base/content/test/linkHandling"
|
||||||
|
is_direct_path = true
|
||||||
|
disable = [
|
||||||
|
# Glance opens with alt+click, firefox doesnt know that.
|
||||||
|
"browser_javascript_links.js",
|
||||||
|
]
|
||||||
|
|
||||||
[readermode]
|
[readermode]
|
||||||
source = "toolkit/components/reader/tests/browser"
|
source = "toolkit/components/reader/tests/browser"
|
||||||
is_direct_path = true
|
is_direct_path = true
|
||||||
@@ -13,10 +43,88 @@ disable = [
|
|||||||
"browser_readerMode_colorSchemePref.js",
|
"browser_readerMode_colorSchemePref.js",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[remote-settings]
|
||||||
|
source = "services/settings/test/unit"
|
||||||
|
is_direct_path = true
|
||||||
|
xpcshell = true
|
||||||
|
|
||||||
|
[remote-settings.replace-manifest]
|
||||||
|
"../../../" = "../../../../services/"
|
||||||
|
|
||||||
[safebrowsing]
|
[safebrowsing]
|
||||||
source = "browser/components/safebrowsing/content/test"
|
source = "browser/components/safebrowsing/content/test"
|
||||||
is_direct_path = true
|
is_direct_path = true
|
||||||
|
|
||||||
|
[services-crypto]
|
||||||
|
source = "services/crypto/tests/unit"
|
||||||
|
is_direct_path = true
|
||||||
|
xpcshell = true
|
||||||
|
|
||||||
|
[services-crypto.replace-manifest]
|
||||||
|
"../../../" = "../../../../services/"
|
||||||
|
|
||||||
|
[sessionstore]
|
||||||
|
source = "browser/components/sessionstore/test"
|
||||||
|
is_direct_path = true
|
||||||
|
disable = [
|
||||||
|
"browser_1933485_tab_groups_history.js",
|
||||||
|
"browser_1953801_tab_groups_history_close_other.js",
|
||||||
|
"browser_aboutSessionRestore.js",
|
||||||
|
"browser_bfcache_telemetry.js",
|
||||||
|
"browser_closedId.js",
|
||||||
|
"browser_closed_objects_changed_notifications_tabs.js",
|
||||||
|
"browser_closed_objects_changed_notifications_windows.js",
|
||||||
|
"browser_closed_tabs_closed_windows.js",
|
||||||
|
"browser_closed_tabs_windows.js",
|
||||||
|
"browser_cookies_partitioned.js",
|
||||||
|
"browser_crashedTabs.js",
|
||||||
|
"browser_dying_cache.js",
|
||||||
|
"browser_firefoxView_restore.js",
|
||||||
|
"browser_firefoxView_selected_restore.js",
|
||||||
|
"browser_forget_closed_tab_window_byId.js",
|
||||||
|
"browser_movePendingTabToNewWindow.js",
|
||||||
|
"browser_newtab_userTypedValue.js",
|
||||||
|
"browser_pinned_tabs.js",
|
||||||
|
"browser_purge_domaindata.js",
|
||||||
|
"browser_reopen_all_windows.js",
|
||||||
|
"browser_restoreLastActionCorrectOrder.js",
|
||||||
|
"browser_restoreLastClosedTabOrWindowOrSession.js",
|
||||||
|
"browser_restore_container_tabs_oa.js",
|
||||||
|
"browser_restore_tabless_window.js",
|
||||||
|
"browser_restore_verticalPinnedTabs.js",
|
||||||
|
"browser_restored_window_features.js",
|
||||||
|
"browser_scrollPositions.js",
|
||||||
|
"browser_scrollPositionsReaderMode.js",
|
||||||
|
"browser_searchModeSwitcher_restore.js",
|
||||||
|
"browser_speculative_connect.js",
|
||||||
|
"browser_splitview_integer_ids.js",
|
||||||
|
"browser_splitview_restore_in_closed_window.js",
|
||||||
|
"browser_splitview_string_migration.js",
|
||||||
|
"browser_swapDocShells.js",
|
||||||
|
"browser_tab_groups_restore_closed_in_closed_window.js",
|
||||||
|
"browser_tab_groups_restore_closed_in_open_window.js",
|
||||||
|
"browser_tab_groups_restore_closed_many_tabs.js",
|
||||||
|
"browser_tab_groups_closed.js",
|
||||||
|
"browser_tab_groups_restore_multiple.js",
|
||||||
|
"browser_tab_groups_restore_saved.js",
|
||||||
|
"browser_tab_groups_restore_simple.js",
|
||||||
|
"browser_tab_groups_save_on_removeAllTabsBut.js",
|
||||||
|
"browser_tab_groups_save_on_removeTabsToTheEnd.js",
|
||||||
|
"browser_tab_groups_save_on_removeTabsToTheStart.js",
|
||||||
|
"browser_tab_groups_save_on_window_close.js",
|
||||||
|
"browser_tab_groups_state.js",
|
||||||
|
"browser_tab_groups_undo.js",
|
||||||
|
"browser_tab_label_during_restore.js",
|
||||||
|
"browser_undoCloseById.js",
|
||||||
|
"browser_urlbarSearchMode.js",
|
||||||
|
"browser_windowStateContainer.js",
|
||||||
|
"browser_wireframe_basic.js",
|
||||||
|
]
|
||||||
|
|
||||||
|
[sessionstore.replace-manifest]
|
||||||
|
'prefs = [' = '''prefs = [
|
||||||
|
"zen.window-sync.enabled=false",'''
|
||||||
|
|
||||||
[shell]
|
[shell]
|
||||||
source = "browser/components/shell/test"
|
source = "browser/components/shell/test"
|
||||||
is_direct_path = true
|
is_direct_path = true
|
||||||
@@ -25,6 +133,15 @@ disable = [
|
|||||||
"browser_setDesktopBackgroundPreview.js",
|
"browser_setDesktopBackgroundPreview.js",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[sync]
|
||||||
|
source = "services/sync/tests/unit"
|
||||||
|
is_direct_path = true
|
||||||
|
xpcshell = true
|
||||||
|
|
||||||
|
[sync.replace-manifest]
|
||||||
|
"../../../" = "../../../../services/"
|
||||||
|
'"identity.fxaccounts.enabled=true"' = '"identity.fxaccounts.enabled=true", "browser.formfill.enable=true"'
|
||||||
|
|
||||||
[tabMediaIndicator]
|
[tabMediaIndicator]
|
||||||
source = "browser/components/tabbrowser/test/browser/tabMediaIndicator"
|
source = "browser/components/tabbrowser/test/browser/tabMediaIndicator"
|
||||||
is_direct_path = true
|
is_direct_path = true
|
||||||
@@ -34,3 +151,10 @@ source = "toolkit/components/tooltiptext"
|
|||||||
disable = [
|
disable = [
|
||||||
"browser_input_file_tooltips.js",
|
"browser_input_file_tooltips.js",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[zoom]
|
||||||
|
source = "browser/base/content/test/zoom"
|
||||||
|
is_direct_path = true
|
||||||
|
|
||||||
|
[zoom.replace-manifest]
|
||||||
|
"../general/" = "../../../../browser/base/content/test/general/"
|
||||||
|
|||||||
32
src/zen/tests/mochitests/alerts/browser.toml
Normal file
32
src/zen/tests/mochitests/alerts/browser.toml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
support-files = [
|
||||||
|
"head.js",
|
||||||
|
"file_dom_notifications.html",
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_notification_close.js"]
|
||||||
|
https_first_disabled = true
|
||||||
|
skip-if = [
|
||||||
|
"os == 'win'", # Bug 1227785
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_notification_do_not_disturb.js"]
|
||||||
|
https_first_disabled = true
|
||||||
|
|
||||||
|
["browser_notification_open_settings.js"]
|
||||||
|
https_first_disabled = true
|
||||||
|
skip-if = [
|
||||||
|
"os == 'win'", # Bug 1411118
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_notification_remove_permission.js"]
|
||||||
|
https_first_disabled = true
|
||||||
|
skip-if = [
|
||||||
|
"os == 'win'", # Bug 1411118
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_notification_tab_switching.js"]
|
||||||
|
https_first_disabled = true
|
||||||
|
skip-if = [
|
||||||
|
"os == 'win'", # Bug 1243263
|
||||||
|
]
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { PlacesTestUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/PlacesTestUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { PermissionTestUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/PermissionTestUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
let notificationURL =
|
||||||
|
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||||
|
|
||||||
|
add_task(async function test_notificationClose() {
|
||||||
|
await addNotificationPermission(notificationURL);
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
{
|
||||||
|
gBrowser,
|
||||||
|
url: notificationURL,
|
||||||
|
},
|
||||||
|
async function dummyTabTask(aBrowser) {
|
||||||
|
await openNotification(aBrowser, "showNotification2");
|
||||||
|
|
||||||
|
info("Notification alert showing");
|
||||||
|
|
||||||
|
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||||
|
if (!alertWindow) {
|
||||||
|
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||||
|
await closeNotification(aBrowser);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertTitleLabel =
|
||||||
|
alertWindow.document.getElementById("alertTitleLabel");
|
||||||
|
is(
|
||||||
|
alertTitleLabel.value,
|
||||||
|
"Test title",
|
||||||
|
"Title text of notification should be present"
|
||||||
|
);
|
||||||
|
let alertTextLabel =
|
||||||
|
alertWindow.document.getElementById("alertTextLabel");
|
||||||
|
is(
|
||||||
|
alertTextLabel.textContent,
|
||||||
|
"Test body 2",
|
||||||
|
"Body text of notification should be present"
|
||||||
|
);
|
||||||
|
|
||||||
|
let alertCloseButton = alertWindow.document.querySelector(".close-icon");
|
||||||
|
is(alertCloseButton.localName, "toolbarbutton", "close button found");
|
||||||
|
let promiseBeforeUnloadEvent = BrowserTestUtils.waitForEvent(
|
||||||
|
alertWindow,
|
||||||
|
"beforeunload"
|
||||||
|
);
|
||||||
|
let closedTime = alertWindow.Date.now();
|
||||||
|
alertCloseButton.click();
|
||||||
|
info("Clicked on close button");
|
||||||
|
await promiseBeforeUnloadEvent;
|
||||||
|
|
||||||
|
ok(true, "Alert should close when the close button is clicked");
|
||||||
|
let currentTime = alertWindow.Date.now();
|
||||||
|
// The notification will self-close at 12 seconds, so this checks
|
||||||
|
// that the notification closed before the timeout.
|
||||||
|
Assert.less(
|
||||||
|
currentTime - closedTime,
|
||||||
|
5000,
|
||||||
|
"Close requested at " +
|
||||||
|
closedTime +
|
||||||
|
", actually closed at " +
|
||||||
|
currentTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function cleanup() {
|
||||||
|
PermissionTestUtils.remove(notificationURL, "desktop-notification");
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that notifications can be silenced using nsIAlertsDoNotDisturb
|
||||||
|
* on systems where that interface and its methods are implemented for
|
||||||
|
* the nsIAlertService.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ALERT_SERVICE = Cc["@mozilla.org/alerts-service;1"]
|
||||||
|
.getService(Ci.nsIAlertsService)
|
||||||
|
.QueryInterface(Ci.nsIAlertsDoNotDisturb);
|
||||||
|
|
||||||
|
const PAGE =
|
||||||
|
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||||
|
|
||||||
|
// The amount of time in seconds that we will wait for a notification
|
||||||
|
// to show up before we decide that it's not coming.
|
||||||
|
const NOTIFICATION_TIMEOUT_SECS = 2000;
|
||||||
|
|
||||||
|
add_setup(async function () {
|
||||||
|
await addNotificationPermission(PAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that the manualDoNotDisturb attribute can prevent
|
||||||
|
* notifications from appearing.
|
||||||
|
*/
|
||||||
|
add_task(async function test_manualDoNotDisturb() {
|
||||||
|
try {
|
||||||
|
// Only run the test if the do-not-disturb
|
||||||
|
// interface has been implemented.
|
||||||
|
ALERT_SERVICE.manualDoNotDisturb;
|
||||||
|
ok(true, "Alert service implements do-not-disturb interface");
|
||||||
|
} catch (e) {
|
||||||
|
ok(
|
||||||
|
true,
|
||||||
|
"Alert service doesn't implement do-not-disturb interface, exiting test"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the event that something goes wrong during this test, make sure
|
||||||
|
// we put the attribute back to the default setting when this test file
|
||||||
|
// exits.
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
ALERT_SERVICE.manualDoNotDisturb = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure that do-not-disturb is not enabled before we start.
|
||||||
|
ok(
|
||||||
|
!ALERT_SERVICE.manualDoNotDisturb,
|
||||||
|
"Alert service should not be disabled when test starts"
|
||||||
|
);
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(PAGE, async browser => {
|
||||||
|
await openNotification(browser, "showNotification2");
|
||||||
|
|
||||||
|
info("Notification alert showing");
|
||||||
|
|
||||||
|
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||||
|
|
||||||
|
// For now, only the XUL alert backend implements the manualDoNotDisturb
|
||||||
|
// method for nsIAlertsDoNotDisturb, so we expect there to be a XUL alert
|
||||||
|
// window. If the method gets implemented by native backends in the future,
|
||||||
|
// we'll probably want to branch here and set the manualDoNotDisturb
|
||||||
|
// attribute manually.
|
||||||
|
ok(alertWindow, "Expected a XUL alert window.");
|
||||||
|
|
||||||
|
// We're using the XUL notification backend. This means that there's
|
||||||
|
// a menuitem for enabling manualDoNotDisturb. We exercise that
|
||||||
|
// menuitem here.
|
||||||
|
let doNotDisturbMenuItem = alertWindow.document.getElementById(
|
||||||
|
"doNotDisturbMenuItem"
|
||||||
|
);
|
||||||
|
is(doNotDisturbMenuItem.localName, "menuitem", "menuitem found");
|
||||||
|
|
||||||
|
let unloadPromise = BrowserTestUtils.waitForEvent(
|
||||||
|
alertWindow,
|
||||||
|
"beforeunload"
|
||||||
|
);
|
||||||
|
|
||||||
|
doNotDisturbMenuItem.click();
|
||||||
|
info("Clicked on do-not-disturb menuitem");
|
||||||
|
await unloadPromise;
|
||||||
|
|
||||||
|
// At this point, we should be configured to not display notifications
|
||||||
|
// to the user.
|
||||||
|
ok(
|
||||||
|
ALERT_SERVICE.manualDoNotDisturb,
|
||||||
|
"Alert service should be disabled after clicking menuitem"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The notification should not appear, but there is no way from the
|
||||||
|
// client-side to know that it was blocked, except for waiting some time
|
||||||
|
// and realizing that the "onshow" event never fired.
|
||||||
|
await Assert.rejects(
|
||||||
|
openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
|
||||||
|
/timed out/,
|
||||||
|
"The notification should never display."
|
||||||
|
);
|
||||||
|
|
||||||
|
ALERT_SERVICE.manualDoNotDisturb = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that the suppressForScreenSharing attribute can prevent
|
||||||
|
* notifications from appearing.
|
||||||
|
*/
|
||||||
|
add_task(async function test_suppressForScreenSharing() {
|
||||||
|
try {
|
||||||
|
// Only run the test if the do-not-disturb
|
||||||
|
// interface has been implemented.
|
||||||
|
ALERT_SERVICE.suppressForScreenSharing;
|
||||||
|
ok(true, "Alert service implements do-not-disturb interface");
|
||||||
|
} catch (e) {
|
||||||
|
ok(
|
||||||
|
true,
|
||||||
|
"Alert service doesn't implement do-not-disturb interface, exiting test"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the event that something goes wrong during this test, make sure
|
||||||
|
// we put the attribute back to the default setting when this test file
|
||||||
|
// exits.
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
ALERT_SERVICE.suppressForScreenSharing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure that do-not-disturb is not enabled before we start.
|
||||||
|
ok(
|
||||||
|
!ALERT_SERVICE.suppressForScreenSharing,
|
||||||
|
"Alert service should not be suppressing for screen sharing when test " +
|
||||||
|
"starts"
|
||||||
|
);
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(PAGE, async browser => {
|
||||||
|
await openNotification(browser, "showNotification2");
|
||||||
|
|
||||||
|
info("Notification alert showing");
|
||||||
|
await closeNotification(browser);
|
||||||
|
ALERT_SERVICE.suppressForScreenSharing = true;
|
||||||
|
|
||||||
|
// The notification should not appear, but there is no way from the
|
||||||
|
// client-side to know that it was blocked, except for waiting some time
|
||||||
|
// and realizing that the "onshow" event never fired.
|
||||||
|
await Assert.rejects(
|
||||||
|
openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
|
||||||
|
/timed out/,
|
||||||
|
"The notification should never display."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ALERT_SERVICE.suppressForScreenSharing = false;
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
var notificationURL =
|
||||||
|
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||||
|
// BrowserGlue passes "privacy-permissions" to openPreferences when the
|
||||||
|
// notifications-open-settings observer fires; the Settings Redesign
|
||||||
|
// LegacyPaneMappings shim routes that to the permissionsData pane.
|
||||||
|
var expectedURL = Services.prefs.getBoolPref(
|
||||||
|
"browser.settings-redesign.enabled",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
? "about:preferences#permissionsData"
|
||||||
|
: "about:preferences#privacy";
|
||||||
|
|
||||||
|
add_task(async function test_settingsOpen_observer() {
|
||||||
|
info(
|
||||||
|
"Opening a dummy tab so openPreferences=>switchToTabHavingURI doesn't use the blank tab."
|
||||||
|
);
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
{
|
||||||
|
gBrowser,
|
||||||
|
url: "about:robots",
|
||||||
|
},
|
||||||
|
async function dummyTabTask() {
|
||||||
|
// Ensure preferences is loaded before removing the tab.
|
||||||
|
let syncPaneLoadedPromise = TestUtils.topicObserved(
|
||||||
|
"sync-pane-loaded",
|
||||||
|
() => true
|
||||||
|
);
|
||||||
|
let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
|
||||||
|
info("simulate a notifications-open-settings notification");
|
||||||
|
let uri = NetUtil.newURI("https://example.com");
|
||||||
|
let principal = Services.scriptSecurityManager.createContentPrincipal(
|
||||||
|
uri,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
Services.obs.notifyObservers(principal, "notifications-open-settings");
|
||||||
|
let tab = await tabPromise;
|
||||||
|
ok(tab, "The notification settings tab opened");
|
||||||
|
await syncPaneLoadedPromise;
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_settingsOpen_button() {
|
||||||
|
info("Adding notification permission");
|
||||||
|
await addNotificationPermission(notificationURL);
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
{
|
||||||
|
gBrowser,
|
||||||
|
url: notificationURL,
|
||||||
|
},
|
||||||
|
async function tabTask(aBrowser) {
|
||||||
|
info("Waiting for notification");
|
||||||
|
await openNotification(aBrowser, "showNotification2");
|
||||||
|
|
||||||
|
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||||
|
if (!alertWindow) {
|
||||||
|
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||||
|
await closeNotification(aBrowser);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure preferences is loaded before removing the tab.
|
||||||
|
let syncPaneLoadedPromise = TestUtils.topicObserved(
|
||||||
|
"sync-pane-loaded",
|
||||||
|
() => true
|
||||||
|
);
|
||||||
|
let closePromise = promiseWindowClosed(alertWindow);
|
||||||
|
let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
|
||||||
|
let openSettingsMenuItem = alertWindow.document.getElementById(
|
||||||
|
"openSettingsMenuItem"
|
||||||
|
);
|
||||||
|
openSettingsMenuItem.click();
|
||||||
|
|
||||||
|
info("Waiting for notification settings tab");
|
||||||
|
let tab = await tabPromise;
|
||||||
|
ok(tab, "The notification settings tab opened");
|
||||||
|
|
||||||
|
await syncPaneLoadedPromise;
|
||||||
|
await closePromise;
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { PermissionTestUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/PermissionTestUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
var tab;
|
||||||
|
var notificationURL =
|
||||||
|
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||||
|
var alertWindowClosed = false;
|
||||||
|
var permRemoved = false;
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
waitForExplicitFinish();
|
||||||
|
|
||||||
|
registerCleanupFunction(function () {
|
||||||
|
gBrowser.removeTab(tab);
|
||||||
|
window.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
addNotificationPermission(notificationURL).then(function openTab() {
|
||||||
|
tab = BrowserTestUtils.addTab(gBrowser, notificationURL);
|
||||||
|
gBrowser.selectedTab = tab;
|
||||||
|
BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => onLoad());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoad() {
|
||||||
|
openNotification(tab.linkedBrowser, "showNotification2").then(onAlertShowing);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAlertShowing() {
|
||||||
|
info("Notification alert showing");
|
||||||
|
|
||||||
|
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||||
|
if (!alertWindow) {
|
||||||
|
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||||
|
closeNotification(tab.linkedBrowser).then(finish);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ok(
|
||||||
|
PermissionTestUtils.testExactPermission(
|
||||||
|
notificationURL,
|
||||||
|
"desktop-notification"
|
||||||
|
),
|
||||||
|
"Permission should exist prior to removal"
|
||||||
|
);
|
||||||
|
let disableForOriginMenuItem = alertWindow.document.getElementById(
|
||||||
|
"disableForOriginMenuItem"
|
||||||
|
);
|
||||||
|
is(disableForOriginMenuItem.localName, "menuitem", "menuitem found");
|
||||||
|
Services.obs.addObserver(permObserver, "perm-changed");
|
||||||
|
alertWindow.addEventListener("beforeunload", onAlertClosing);
|
||||||
|
disableForOriginMenuItem.click();
|
||||||
|
info("Clicked on disable-for-origin menuitem");
|
||||||
|
}
|
||||||
|
|
||||||
|
function permObserver(subject, topic, data) {
|
||||||
|
if (topic != "perm-changed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let permission = subject.QueryInterface(Ci.nsIPermission);
|
||||||
|
is(
|
||||||
|
permission.type,
|
||||||
|
"desktop-notification",
|
||||||
|
"desktop-notification permission changed"
|
||||||
|
);
|
||||||
|
is(data, "deleted", "desktop-notification permission deleted");
|
||||||
|
|
||||||
|
Services.obs.removeObserver(permObserver, "perm-changed");
|
||||||
|
permRemoved = true;
|
||||||
|
if (alertWindowClosed) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAlertClosing(event) {
|
||||||
|
event.target.removeEventListener("beforeunload", onAlertClosing);
|
||||||
|
|
||||||
|
alertWindowClosed = true;
|
||||||
|
if (permRemoved) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { PermissionTestUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/PermissionTestUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
var tab;
|
||||||
|
var notification;
|
||||||
|
var notificationURL =
|
||||||
|
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
|
||||||
|
var newWindowOpenedFromTab;
|
||||||
|
|
||||||
|
add_task(async function test_notificationPreventDefaultAndSwitchTabs() {
|
||||||
|
await addNotificationPermission(notificationURL);
|
||||||
|
|
||||||
|
let originalTab = gBrowser.selectedTab;
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
{
|
||||||
|
gBrowser,
|
||||||
|
url: notificationURL,
|
||||||
|
},
|
||||||
|
async function dummyTabTask(aBrowser) {
|
||||||
|
// Put new tab in background so it is obvious when it is re-focused.
|
||||||
|
await BrowserTestUtils.switchTab(gBrowser, originalTab);
|
||||||
|
isnot(
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
aBrowser,
|
||||||
|
"Notification page loaded as a background tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
// First, show a notification that will be have the tab-switching prevented.
|
||||||
|
function promiseNotificationEvent(evt) {
|
||||||
|
return SpecialPowers.spawn(
|
||||||
|
aBrowser,
|
||||||
|
[evt],
|
||||||
|
async function (contentEvt) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let contentNotification = content.wrappedJSObject._notification;
|
||||||
|
contentNotification.addEventListener(
|
||||||
|
contentEvt,
|
||||||
|
function (event) {
|
||||||
|
resolve({ defaultPrevented: event.defaultPrevented });
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await openNotification(aBrowser, "showNotification1");
|
||||||
|
info("Notification alert showing");
|
||||||
|
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||||
|
if (!alertWindow) {
|
||||||
|
ok(true, "Notifications don't use XUL windows on all platforms.");
|
||||||
|
await closeNotification(aBrowser);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info("Clicking on notification");
|
||||||
|
let promiseClickEvent = promiseNotificationEvent("click");
|
||||||
|
|
||||||
|
// NB: This executeSoon is needed to allow the non-e10s runs of this test
|
||||||
|
// a chance to set the event listener on the page. Otherwise, we
|
||||||
|
// synchronously fire the click event before we listen for the event.
|
||||||
|
executeSoon(() => {
|
||||||
|
EventUtils.synthesizeMouseAtCenter(
|
||||||
|
alertWindow.document.getElementById("alertTitleLabel"),
|
||||||
|
{},
|
||||||
|
alertWindow
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let clickEvent = await promiseClickEvent;
|
||||||
|
ok(
|
||||||
|
clickEvent.defaultPrevented,
|
||||||
|
"The event handler for the first notification cancels the event"
|
||||||
|
);
|
||||||
|
isnot(
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
aBrowser,
|
||||||
|
"Notification page still a background tab"
|
||||||
|
);
|
||||||
|
let notificationClosed = promiseNotificationEvent("close");
|
||||||
|
await closeNotification(aBrowser);
|
||||||
|
await notificationClosed;
|
||||||
|
|
||||||
|
// Second, show a notification that will cause the tab to get switched.
|
||||||
|
await openNotification(aBrowser, "showNotification2");
|
||||||
|
alertWindow = Services.wm.getMostRecentWindow("alert:alert");
|
||||||
|
let promiseTabSelect = BrowserTestUtils.waitForEvent(
|
||||||
|
gBrowser.tabContainer,
|
||||||
|
"TabSelect"
|
||||||
|
);
|
||||||
|
EventUtils.synthesizeMouseAtCenter(
|
||||||
|
alertWindow.document.getElementById("alertTitleLabel"),
|
||||||
|
{},
|
||||||
|
alertWindow
|
||||||
|
);
|
||||||
|
await promiseTabSelect;
|
||||||
|
is(
|
||||||
|
gBrowser.selectedBrowser.currentURI.spec,
|
||||||
|
notificationURL,
|
||||||
|
"Clicking on the second notification should select its originating tab"
|
||||||
|
);
|
||||||
|
notificationClosed = promiseNotificationEvent("close");
|
||||||
|
await closeNotification(aBrowser);
|
||||||
|
await notificationClosed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function cleanup() {
|
||||||
|
PermissionTestUtils.remove(notificationURL, "desktop-notification");
|
||||||
|
});
|
||||||
39
src/zen/tests/mochitests/alerts/file_dom_notifications.html
Normal file
39
src/zen/tests/mochitests/alerts/file_dom_notifications.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function showNotification1() {
|
||||||
|
var options = {
|
||||||
|
dir: undefined,
|
||||||
|
lang: undefined,
|
||||||
|
body: "Test body 1",
|
||||||
|
tag: "Test tag",
|
||||||
|
icon: undefined,
|
||||||
|
};
|
||||||
|
var n = new Notification("Test title", options);
|
||||||
|
n.addEventListener("click", function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification2() {
|
||||||
|
var options = {
|
||||||
|
dir: undefined,
|
||||||
|
lang: undefined,
|
||||||
|
body: "Test body 2",
|
||||||
|
tag: "Test tag",
|
||||||
|
icon: undefined,
|
||||||
|
};
|
||||||
|
return new Notification("Test title", options);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form id="notificationForm" onsubmit="showNotification();">
|
||||||
|
<input type="submit" value="Show notification" id="submit"/>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
73
src/zen/tests/mochitests/alerts/head.js
Normal file
73
src/zen/tests/mochitests/alerts/head.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Platforms may default to reducing motion. We override this to ensure the
|
||||||
|
// alert slide animation is enabled in tests.
|
||||||
|
SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["ui.prefersReducedMotion", 0]],
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addNotificationPermission(originString) {
|
||||||
|
return SpecialPowers.pushPermissions([
|
||||||
|
{
|
||||||
|
type: "desktop-notification",
|
||||||
|
allow: true,
|
||||||
|
context: originString,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to `BrowserTestUtils.closeWindow`, but
|
||||||
|
* doesn't call `window.close()`.
|
||||||
|
*/
|
||||||
|
function promiseWindowClosed(window) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
Services.ww.registerNotification(function observer(subject, topic) {
|
||||||
|
if (topic == "domwindowclosed" && subject == window) {
|
||||||
|
Services.ww.unregisterNotification(observer);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These two functions work with file_dom_notifications.html to open the
|
||||||
|
* notification and close it.
|
||||||
|
*
|
||||||
|
* |fn| can be showNotification1 or showNotification2.
|
||||||
|
* if |timeout| is passed, then the promise returned from this function is
|
||||||
|
* rejected after the requested number of miliseconds.
|
||||||
|
*/
|
||||||
|
function openNotification(aBrowser, fn, timeout) {
|
||||||
|
info(`openNotification: ${fn}`);
|
||||||
|
return SpecialPowers.spawn(
|
||||||
|
aBrowser,
|
||||||
|
[fn, timeout],
|
||||||
|
async function (contentFn, contentTimeout) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
let win = content.wrappedJSObject;
|
||||||
|
let notification = win[contentFn]();
|
||||||
|
win._notification = notification;
|
||||||
|
|
||||||
|
function listener() {
|
||||||
|
notification.removeEventListener("show", listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.addEventListener("show", listener);
|
||||||
|
|
||||||
|
if (contentTimeout) {
|
||||||
|
content.setTimeout(() => {
|
||||||
|
notification.removeEventListener("show", listener);
|
||||||
|
reject("timed out");
|
||||||
|
}, contentTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNotification(aBrowser) {
|
||||||
|
return SpecialPowers.spawn(aBrowser, [], function () {
|
||||||
|
content.wrappedJSObject._notification.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
7
src/zen/tests/mochitests/backforward/browser.toml
Normal file
7
src/zen/tests/mochitests/backforward/browser.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
|
||||||
|
["browser_history_menu.js"]
|
||||||
|
fail-if = [
|
||||||
|
"a11y_checks", # Bug 1854233 navigator-toolbox may not be focusable
|
||||||
|
]
|
||||||
|
https_first_disabled = true
|
||||||
179
src/zen/tests/mochitests/backforward/browser_history_menu.js
Normal file
179
src/zen/tests/mochitests/backforward/browser_history_menu.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
// This test verifies that the back forward button long-press menu and context menu
|
||||||
|
// shows the correct history items.
|
||||||
|
|
||||||
|
add_task(async function mousedown_back() {
|
||||||
|
await testBackForwardMenu(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function contextmenu_back() {
|
||||||
|
await testBackForwardMenu(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openHistoryMenu(useContextMenu) {
|
||||||
|
let backButton = document.getElementById("back-button");
|
||||||
|
let rect = backButton.getBoundingClientRect();
|
||||||
|
|
||||||
|
info("waiting for the history menu to open");
|
||||||
|
|
||||||
|
let popupShownPromise = BrowserTestUtils.waitForEvent(
|
||||||
|
useContextMenu ? document.getElementById("backForwardMenu") : backButton,
|
||||||
|
"popupshown"
|
||||||
|
);
|
||||||
|
if (useContextMenu) {
|
||||||
|
EventUtils.synthesizeMouseAtCenter(backButton, {
|
||||||
|
type: "contextmenu",
|
||||||
|
button: 2,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
EventUtils.synthesizeMouseAtCenter(backButton, { type: "mousedown" });
|
||||||
|
}
|
||||||
|
|
||||||
|
EventUtils.synthesizeMouse(backButton, rect.width / 2, rect.height, {
|
||||||
|
type: "mouseup",
|
||||||
|
});
|
||||||
|
let popupEvent = await popupShownPromise;
|
||||||
|
|
||||||
|
ok(true, "history menu opened");
|
||||||
|
|
||||||
|
return popupEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testBackForwardMenu(useContextMenu) {
|
||||||
|
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||||
|
gBrowser,
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
"http://example.com"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let iter = 2; iter <= 4; iter++) {
|
||||||
|
// Iterate three times. For the first two times through the loop, add a new history item.
|
||||||
|
// But for the last iteration, go back in the history instead.
|
||||||
|
await SpecialPowers.spawn(
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
[iter],
|
||||||
|
async function (iterChild) {
|
||||||
|
if (iterChild == 4) {
|
||||||
|
let popStatePromise = new Promise(function (resolve) {
|
||||||
|
content.onpopstate = resolve;
|
||||||
|
});
|
||||||
|
content.history.back();
|
||||||
|
await popStatePromise;
|
||||||
|
} else {
|
||||||
|
// With the back-button intervention enabled, we need to make sure to
|
||||||
|
// trigger a user activation on each history entry, or they won't
|
||||||
|
// show in the menu.
|
||||||
|
content.document.notifyUserGestureActivation();
|
||||||
|
content.history.pushState({}, "" + iterChild, iterChild + ".html");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for the session data to be flushed before continuing the test
|
||||||
|
await new Promise(resolve =>
|
||||||
|
SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
|
||||||
|
);
|
||||||
|
|
||||||
|
let popupEvent = await openHistoryMenu(useContextMenu);
|
||||||
|
|
||||||
|
// Wait for the session data to be flushed before continuing the test
|
||||||
|
await new Promise(resolve =>
|
||||||
|
SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
|
||||||
|
);
|
||||||
|
|
||||||
|
is(
|
||||||
|
popupEvent.target.children.length,
|
||||||
|
iter > 3 ? 3 : iter,
|
||||||
|
"Correct number of history items"
|
||||||
|
);
|
||||||
|
|
||||||
|
let node = popupEvent.target.lastElementChild;
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
is(node.getAttribute("uri"), "http://example.com/", "'1' item uri");
|
||||||
|
is(node.getAttribute("index"), "0", "'1' item index");
|
||||||
|
is(
|
||||||
|
node.getAttribute("historyindex"),
|
||||||
|
iter == 3 ? "-2" : "-1",
|
||||||
|
"'1' item historyindex"
|
||||||
|
);
|
||||||
|
|
||||||
|
node = node.previousElementSibling;
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
is(node.getAttribute("uri"), "http://example.com/2.html", "'2' item uri");
|
||||||
|
is(node.getAttribute("index"), "1", "'2' item index");
|
||||||
|
is(
|
||||||
|
node.getAttribute("historyindex"),
|
||||||
|
iter == 3 ? "-1" : "0",
|
||||||
|
"'2' item historyindex"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (iter >= 3) {
|
||||||
|
node = node.previousElementSibling;
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
is(node.getAttribute("uri"), "http://example.com/3.html", "'3' item uri");
|
||||||
|
is(node.getAttribute("index"), "2", "'3' item index");
|
||||||
|
is(
|
||||||
|
node.getAttribute("historyindex"),
|
||||||
|
iter == 4 ? "1" : "0",
|
||||||
|
"'3' item historyindex"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the popup, but on the last iteration, click on one of the history items
|
||||||
|
// to ensure it opens in a new tab.
|
||||||
|
let popupHiddenPromise = BrowserTestUtils.waitForEvent(
|
||||||
|
popupEvent.target,
|
||||||
|
"popuphidden"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (iter < 4) {
|
||||||
|
popupEvent.target.hidePopup();
|
||||||
|
} else {
|
||||||
|
let newTabPromise = BrowserTestUtils.waitForNewTab(
|
||||||
|
gBrowser,
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
url => url == "http://example.com/"
|
||||||
|
);
|
||||||
|
|
||||||
|
popupEvent.target.activateItem(popupEvent.target.children[2], {
|
||||||
|
button: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let newtab = await newTabPromise;
|
||||||
|
gBrowser.removeTab(newtab);
|
||||||
|
}
|
||||||
|
|
||||||
|
await popupHiddenPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
gBrowser.removeTab(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the history popup appears after navigating around in a preferences page.
|
||||||
|
add_task(async function test_preferences_page() {
|
||||||
|
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||||
|
gBrowser,
|
||||||
|
"about:preferences"
|
||||||
|
);
|
||||||
|
|
||||||
|
openPreferences("search");
|
||||||
|
let popupEvent = await openHistoryMenu(true);
|
||||||
|
|
||||||
|
// Wait for the session data to be flushed before continuing the test
|
||||||
|
await new Promise(resolve =>
|
||||||
|
SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)
|
||||||
|
);
|
||||||
|
|
||||||
|
is(popupEvent.target.children.length, 2, "Correct number of history items");
|
||||||
|
|
||||||
|
let popupHiddenPromise = BrowserTestUtils.waitForEvent(
|
||||||
|
popupEvent.target,
|
||||||
|
"popuphidden"
|
||||||
|
);
|
||||||
|
popupEvent.target.hidePopup();
|
||||||
|
await popupHiddenPromise;
|
||||||
|
|
||||||
|
gBrowser.removeTab(tab);
|
||||||
|
});
|
||||||
10
src/zen/tests/mochitests/caps/browser.toml
Normal file
10
src/zen/tests/mochitests/caps/browser.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
|
||||||
|
["browser_principalSerialization_csp.js"]
|
||||||
|
|
||||||
|
["browser_principalSerialization_json.js"]
|
||||||
|
skip-if = [
|
||||||
|
"debug", # deliberately bypass assertions when deserializing. Bug 965637 removed the CSP from Principals, but the remaining bits in such Principals should deserialize correctly.
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_principalSerialization_version1.js"]
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Within Bug 965637 we move the CSP away from the Principal. Serialized Principals however
|
||||||
|
* might still have CSPs serialized within them. This tests ensures that we do not
|
||||||
|
* encounter a memory corruption when deserializing. It's fine that the deserialized
|
||||||
|
* CSP is null, but the Principal itself should deserialize correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
add_task(async function test_deserialize_principal_with_csp() {
|
||||||
|
/*
|
||||||
|
This test should be resilient to changes in principal serialization, if these are failing then it's likely the code will break session storage.
|
||||||
|
To recreate this for another version, copy the function into the browser console, browse some pages and printHistory.
|
||||||
|
|
||||||
|
Generated with:
|
||||||
|
function printHistory() {
|
||||||
|
let tests = [];
|
||||||
|
let entries = SessionStore.getSessionHistory(gBrowser.selectedTab).entries.map((entry) => { return entry.triggeringPrincipal_base64 });
|
||||||
|
entries.push(E10SUtils.serializePrincipal(gBrowser.selectedTab.linkedBrowser._contentPrincipal));
|
||||||
|
for (let entry of entries) {
|
||||||
|
console.log(entry);
|
||||||
|
let testData = {};
|
||||||
|
testData.input = entry;
|
||||||
|
let principal = E10SUtils.deserializePrincipal(testData.input);
|
||||||
|
testData.output = {};
|
||||||
|
if (principal.URI === null) {
|
||||||
|
testData.output.URI = false;
|
||||||
|
} else {
|
||||||
|
testData.output.URISpec = principal.URI.spec;
|
||||||
|
}
|
||||||
|
testData.output.originAttributes = principal.originAttributes;
|
||||||
|
testData.output.cspJSON = principal.cspJSON;
|
||||||
|
|
||||||
|
tests.push(testData);
|
||||||
|
}
|
||||||
|
return tests;
|
||||||
|
}
|
||||||
|
printHistory(); // Copy this into: serializedPrincipalsFromFirefox
|
||||||
|
*/
|
||||||
|
|
||||||
|
let serializedPrincipalsFromFirefox = [
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
"ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAHmh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLwAAAAAAAAAFAAAACAAAAA8AAAAA/////wAAAAD/////AAAACAAAAA8AAAAXAAAABwAAABcAAAAHAAAAFwAAAAcAAAAeAAAAAAAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AQAAAAAAAAAAAAAAAQnZ7Rrl1EAEv+Anzrkj2ayzxMCuvV5MrYfgjSENuz+fAd6UctCANBHTk5kAEEug/UCSBzpUbXhPMJE6uHGBMgjGAAAAAv////8AAAG7AQAAAB5odHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8AAAAAAAAABQAAAAgAAAAPAAAAAP////8AAAAA/////wAAAAgAAAAPAAAAFwAAAAcAAAAXAAAABwAAABcAAAAHAAAAHgAAAAAAAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wEAAAAAAAAAAAABAAAFtgBzAGMAcgBpAHAAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdABhAGcAbQBhAG4AYQBnAGUAcgAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AcwAuAHkAdABpAG0AZwAuAGMAbwBtADsAIABpAG0AZwAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABkAGEAdABhADoAIABoAHQAdABwAHMAOgAvAC8AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AZABlACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBkAGsAIABoAHQAdABwAHMAOgAvAC8AYwByAGUAYQB0AGkAdgBlAGMAbwBtAG0AbwBuAHMALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwBhAGQALgBkAG8AdQBiAGwAZQBjAGwAaQBjAGsALgBuAGUAdAA7ACAAZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AOwAgAGYAcgBhAG0AZQAtAHMAcgBjACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAtAG4AbwBjAG8AbwBrAGkAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAcgBhAGMAawBlAHIAdABlAHMAdAAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AcwB1AHIAdgBlAHkAZwBpAHoAbQBvAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAuAGMAbgAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA7ACAAcwB0AHkAbABlAC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnADsAIABjAG8AbgBuAGUAYwB0AC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALwAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALgBjAG4ALwA7ACAAYwBoAGkAbABkAC0AcwByAGMAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC0AbgBvAGMAbwBvAGsAaQBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdAByAGEAYwBrAGUAcgB0AGUAcwB0AC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBzAHUAcgB2AGUAeQBnAGkAegBtAG8ALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtAAA=",
|
||||||
|
output: {
|
||||||
|
// Within Bug 965637 we removed CSP from Principals. Already serialized Principals however should still deserialize correctly (just without the CSP).
|
||||||
|
// "cspJSON": "{\"csp-policies\":[{\"child-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"connect-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://accounts.firefox.com/\",\"https://accounts.firefox.com.cn/\"],\"default-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\"],\"frame-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"img-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"data:\",\"https://mozilla.org\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://adservice.google.com\",\"https://adservice.google.de\",\"https://adservice.google.dk\",\"https://creativecommons.org\",\"https://ad.doubleclick.net\"],\"report-only\":false,\"script-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\",\"'unsafe-eval'\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://tagmanager.google.com\",\"https://www.youtube.com\",\"https://s.ytimg.com\"],\"style-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\"]}]}",
|
||||||
|
URISpec: "https://www.mozilla.org/en-US/",
|
||||||
|
originAttributes: {
|
||||||
|
firstPartyDomain: "",
|
||||||
|
inIsolatedMozBrowser: false,
|
||||||
|
privateBrowsingId: 0,
|
||||||
|
userContextId: 0,
|
||||||
|
geckoViewSessionContextId: "",
|
||||||
|
partitionKey: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
"ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAL2h0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTL2ZpcmVmb3gvYWNjb3VudHMvAAAAAAAAAAUAAAAIAAAADwAAAAj/////AAAACP////8AAAAIAAAADwAAABcAAAAYAAAAFwAAABgAAAAXAAAAGAAAAC8AAAAAAAAAL/////8AAAAA/////wAAABf/////AAAAF/////8BAAAAAAAAAAAAAAABCdntGuXUQAS/4CfOuSPZrLPEwK69Xkyth+CNIQ27P58B3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAL2h0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTL2ZpcmVmb3gvYWNjb3VudHMvAAAAAAAAAAUAAAAIAAAADwAAAAj/////AAAACP////8AAAAIAAAADwAAABcAAAAYAAAAFwAAABgAAAAXAAAAGAAAAC8AAAAAAAAAL/////8AAAAA/////wAAABf/////AAAAF/////8BAAAAAAAAAAAAAQAABbYAcwBjAHIAaQBwAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAJwB1AG4AcwBhAGYAZQAtAGkAbgBsAGkAbgBlACcAIAAnAHUAbgBzAGEAZgBlAC0AZQB2AGEAbAAnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBnAG8AbwBnAGwAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHMALgB5AHQAaQBtAGcALgBjAG8AbQA7ACAAaQBtAGcALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAZABhAHQAYQA6ACAAaAB0AHQAcABzADoALwAvAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBkAHMAZQByAHYAaQBjAGUALgBnAG8AbwBnAGwAZQAuAGQAZQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AZABrACAAaAB0AHQAcABzADoALwAvAGMAcgBlAGEAdABpAHYAZQBjAG8AbQBtAG8AbgBzAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AYQBkAC4AZABvAHUAYgBsAGUAYwBsAGkAYwBrAC4AbgBlAHQAOwAgAGQAZQBmAGEAdQBsAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtADsAIABmAHIAYQBtAGUALQBzAHIAYwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALQBuAG8AYwBvAG8AawBpAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB0AHIAYQBjAGsAZQByAHQAZQBzAHQALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHMAdQByAHYAZQB5AGcAaQB6AG0AbwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALgBjAG4AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AOwAgAHMAdAB5AGwAZQAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwA7ACAAYwBvAG4AbgBlAGMAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC8AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuAC8AOwAgAGMAaABpAGwAZAAtAHMAcgBjACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAtAG4AbwBjAG8AbwBrAGkAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAcgBhAGMAawBlAHIAdABlAHMAdAAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AcwB1AHIAdgBlAHkAZwBpAHoAbQBvAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAuAGMAbgAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQAA",
|
||||||
|
output: {
|
||||||
|
URISpec: "https://www.mozilla.org/en-US/firefox/accounts/",
|
||||||
|
originAttributes: {
|
||||||
|
firstPartyDomain: "",
|
||||||
|
inIsolatedMozBrowser: false,
|
||||||
|
privateBrowsingId: 0,
|
||||||
|
userContextId: 0,
|
||||||
|
geckoViewSessionContextId: "",
|
||||||
|
partitionKey: "",
|
||||||
|
},
|
||||||
|
// Within Bug 965637 we removed CSP from Principals. Already serialized Principals however should still deserialize correctly (just without the CSP).
|
||||||
|
// "cspJSON": "{\"csp-policies\":[{\"child-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"connect-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://accounts.firefox.com/\",\"https://accounts.firefox.com.cn/\"],\"default-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\"],\"frame-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"img-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"data:\",\"https://mozilla.org\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://adservice.google.com\",\"https://adservice.google.de\",\"https://adservice.google.dk\",\"https://creativecommons.org\",\"https://ad.doubleclick.net\"],\"report-only\":false,\"script-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\",\"'unsafe-eval'\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://tagmanager.google.com\",\"https://www.youtube.com\",\"https://s.ytimg.com\"],\"style-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\"]}]}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let test of serializedPrincipalsFromFirefox) {
|
||||||
|
let principal = E10SUtils.deserializePrincipal(test.input);
|
||||||
|
|
||||||
|
for (let key in principal.originAttributes) {
|
||||||
|
is(
|
||||||
|
principal.originAttributes[key],
|
||||||
|
test.output.originAttributes[key],
|
||||||
|
`Ensure value of ${key} is ${test.output.originAttributes[key]}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("URI" in test.output && test.output.URI === false) {
|
||||||
|
is(
|
||||||
|
principal.isContentPrincipal,
|
||||||
|
false,
|
||||||
|
"Should have not have a URI for system"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
is(
|
||||||
|
principal.spec,
|
||||||
|
test.output.URISpec,
|
||||||
|
`Should have spec ${test.output.URISpec}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
This test file exists to ensure whenever changes to principal serialization happens,
|
||||||
|
we guarantee that the data can be restored and generated into a new principal.
|
||||||
|
|
||||||
|
The tests are written to be brittle so we encode all versions of the changes into the tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
add_task(async function test_nullPrincipal() {
|
||||||
|
const nullId = "0";
|
||||||
|
// fields
|
||||||
|
const uri = 0;
|
||||||
|
const suffix = 1;
|
||||||
|
|
||||||
|
const nullReplaceRegex =
|
||||||
|
/moz-nullprincipal:{[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}}/;
|
||||||
|
const NULL_REPLACE = "NULL_PRINCIPAL_URL";
|
||||||
|
|
||||||
|
/*
|
||||||
|
This test should NOT be resilient to changes in versioning,
|
||||||
|
however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code.
|
||||||
|
*/
|
||||||
|
let tests = [
|
||||||
|
{
|
||||||
|
input: { OA: {} },
|
||||||
|
expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: { OA: {} },
|
||||||
|
expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: { OA: { userContextId: 0 } },
|
||||||
|
expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: { OA: { userContextId: 2 } },
|
||||||
|
expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}","${suffix}":"^userContextId=2"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: { OA: { privateBrowsingId: 1 } },
|
||||||
|
expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}","${suffix}":"^privateBrowsingId=1"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: { OA: { privateBrowsingId: 0 } },
|
||||||
|
expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let test of tests) {
|
||||||
|
let p = Services.scriptSecurityManager.createNullPrincipal(test.input.OA);
|
||||||
|
let sp = E10SUtils.serializePrincipal(p);
|
||||||
|
// Not sure why cppjson is adding a \n here
|
||||||
|
let spr = sp.replace(nullReplaceRegex, NULL_REPLACE);
|
||||||
|
is(
|
||||||
|
test.expected,
|
||||||
|
spr,
|
||||||
|
"Expected serialized object for " + JSON.stringify(test.input)
|
||||||
|
);
|
||||||
|
let dp = E10SUtils.deserializePrincipal(sp);
|
||||||
|
|
||||||
|
// Check all the origin attributes
|
||||||
|
for (let key in test.input.OA) {
|
||||||
|
is(
|
||||||
|
dp.originAttributes[key],
|
||||||
|
test.input.OA[key],
|
||||||
|
"Ensure value of " + key + " is " + test.input.OA[key]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_contentPrincipal() {
|
||||||
|
const contentId = "1";
|
||||||
|
// fields
|
||||||
|
const content = 0;
|
||||||
|
// const domain = 1;
|
||||||
|
const suffix = 2;
|
||||||
|
// const csp = 3;
|
||||||
|
|
||||||
|
/*
|
||||||
|
This test should NOT be resilient to changes in versioning,
|
||||||
|
however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code.
|
||||||
|
*/
|
||||||
|
let tests = [
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
input: { uri: "http://example.com/", OA: {} },
|
||||||
|
expected: `{"${contentId}":{"${content}":"http://example.com/"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
input: { uri: "http://mozilla1.com/", OA: {} },
|
||||||
|
expected: `{"${contentId}":{"${content}":"http://mozilla1.com/"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
input: { uri: "http://mozilla2.com/", OA: { userContextId: 0 } },
|
||||||
|
expected: `{"${contentId}":{"${content}":"http://mozilla2.com/"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
input: { uri: "http://mozilla3.com/", OA: { userContextId: 2 } },
|
||||||
|
expected: `{"${contentId}":{"${content}":"http://mozilla3.com/","${suffix}":"^userContextId=2"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
input: { uri: "http://mozilla4.com/", OA: { privateBrowsingId: 1 } },
|
||||||
|
expected: `{"${contentId}":{"${content}":"http://mozilla4.com/","${suffix}":"^privateBrowsingId=1"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
input: { uri: "http://mozilla5.com/", OA: { privateBrowsingId: 0 } },
|
||||||
|
expected: `{"${contentId}":{"${content}":"http://mozilla5.com/"}}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let test of tests) {
|
||||||
|
let uri = Services.io.newURI(test.input.uri);
|
||||||
|
let p = Services.scriptSecurityManager.createContentPrincipal(
|
||||||
|
uri,
|
||||||
|
test.input.OA
|
||||||
|
);
|
||||||
|
let sp = E10SUtils.serializePrincipal(p);
|
||||||
|
is(test.expected, sp, "Expected serialized object for " + test.input.uri);
|
||||||
|
let dp = E10SUtils.deserializePrincipal(sp);
|
||||||
|
is(dp.URI.spec, test.input.uri, "Ensure spec is the same");
|
||||||
|
|
||||||
|
// Check all the origin attributes
|
||||||
|
for (let key in test.input.OA) {
|
||||||
|
is(
|
||||||
|
dp.originAttributes[key],
|
||||||
|
test.input.OA[key],
|
||||||
|
"Ensure value of " + key + " is " + test.input.OA[key]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_systemPrincipal() {
|
||||||
|
const systemId = "3";
|
||||||
|
/*
|
||||||
|
This test should NOT be resilient to changes in versioning,
|
||||||
|
however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code.
|
||||||
|
*/
|
||||||
|
const expected = `{"${systemId}":{}}`;
|
||||||
|
|
||||||
|
let p = Services.scriptSecurityManager.getSystemPrincipal();
|
||||||
|
let sp = E10SUtils.serializePrincipal(p);
|
||||||
|
is(expected, sp, "Expected serialized object for system principal");
|
||||||
|
let dp = E10SUtils.deserializePrincipal(sp);
|
||||||
|
is(
|
||||||
|
dp,
|
||||||
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
"Deserialized the system principal"
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
This test file exists to ensure whenever changes to principal serialization happens,
|
||||||
|
we guarantee that the data can be restored and generated into a new principal.
|
||||||
|
|
||||||
|
The tests are written to be brittle so we encode all versions of the changes into the tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
add_task(function test_nullPrincipal() {
|
||||||
|
/*
|
||||||
|
As Null principals are designed to be non deterministic we just need to ensure that
|
||||||
|
a previous serialized version matches what it was generated as.
|
||||||
|
|
||||||
|
This test should be resilient to changes in versioning, however it should also be duplicated for a new serialization change.
|
||||||
|
*/
|
||||||
|
// Principal created with: E10SUtils.serializePrincipal(Services.scriptSecurityManager.createNullPrincipal({ }));
|
||||||
|
let p = E10SUtils.deserializePrincipal(
|
||||||
|
"vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezU2Y2FjNTQwLTg2NGQtNDdlNy04ZTI1LTE2MTRlYWI1MTU1ZX0AAAAA"
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
"moz-nullprincipal:{56cac540-864d-47e7-8e25-1614eab5155e}",
|
||||||
|
p.URI.spec,
|
||||||
|
"Deserialized principal doesn't have the correct URI"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Principal created with: E10SUtils.serializePrincipal(Services.scriptSecurityManager.createNullPrincipal({ userContextId: 2 }));
|
||||||
|
let p2 = E10SUtils.deserializePrincipal(
|
||||||
|
"vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezA1ZjllN2JhLWIwODMtNDJhMi1iNDdkLTZiODRmNmYwYTM3OX0AAAAQXnVzZXJDb250ZXh0SWQ9Mg=="
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
"moz-nullprincipal:{05f9e7ba-b083-42a2-b47d-6b84f6f0a379}",
|
||||||
|
p2.URI.spec,
|
||||||
|
"Deserialized principal doesn't have the correct URI"
|
||||||
|
);
|
||||||
|
is(p2.originAttributes.userContextId, 2, "Expected a userContextId of 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_realHistoryCheck() {
|
||||||
|
/*
|
||||||
|
This test should be resilient to changes in principal serialization, if these are failing then it's likely the code will break session storage.
|
||||||
|
To recreate this for another version, copy the function into the browser console, browse some pages and printHistory.
|
||||||
|
|
||||||
|
Generated with:
|
||||||
|
function printHistory() {
|
||||||
|
let tests = [];
|
||||||
|
let entries = SessionStore.getSessionHistory(gBrowser.selectedTab).entries.map((entry) => { return entry.triggeringPrincipal_base64 });
|
||||||
|
entries.push(E10SUtils.serializePrincipal(gBrowser.selectedTab.linkedBrowser._contentPrincipal));
|
||||||
|
for (let entry of entries) {
|
||||||
|
console.log(entry);
|
||||||
|
let testData = {};
|
||||||
|
testData.input = entry;
|
||||||
|
let principal = E10SUtils.deserializePrincipal(testData.input);
|
||||||
|
testData.output = {};
|
||||||
|
if (principal.URI === null) {
|
||||||
|
testData.output.URI = false;
|
||||||
|
} else {
|
||||||
|
testData.output.URISpec = principal.URI.spec;
|
||||||
|
}
|
||||||
|
testData.output.originAttributes = principal.originAttributes;
|
||||||
|
|
||||||
|
tests.push(testData);
|
||||||
|
}
|
||||||
|
return tests;
|
||||||
|
}
|
||||||
|
printHistory(); // Copy this into: serializedPrincipalsFromFirefox
|
||||||
|
*/
|
||||||
|
|
||||||
|
let serializedPrincipalsFromFirefox = [
|
||||||
|
{
|
||||||
|
input: "SmIS26zLEdO3ZQBgsLbOywAAAAAAAAAAwAAAAAAAAEY=",
|
||||||
|
output: {
|
||||||
|
URI: false,
|
||||||
|
originAttributes: {
|
||||||
|
firstPartyDomain: "",
|
||||||
|
inIsolatedMozBrowser: false,
|
||||||
|
privateBrowsingId: 0,
|
||||||
|
userContextId: 0,
|
||||||
|
geckoViewSessionContextId: "",
|
||||||
|
partitionKey: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
"ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAe2h0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTLz91dG1fc291cmNlPXd3dy5tb3ppbGxhLm9yZyZ1dG1fbWVkaXVtPXJlZmVycmFsJnV0bV9jYW1wYWlnbj1uYXYmdXRtX2NvbnRlbnQ9ZGV2ZWxvcGVycwAAAAAAAAAFAAAACAAAABUAAAAA/////wAAAAD/////AAAACAAAABUAAAAdAAAAXgAAAB0AAAAHAAAAHQAAAAcAAAAkAAAAAAAAAAD/////AAAAAP////8AAAAlAAAAVgAAAAD/////AQAAAAAAAAAAAAAAAA==",
|
||||||
|
output: {
|
||||||
|
URISpec:
|
||||||
|
"https://developer.mozilla.org/en-US/?utm_source=www.mozilla.org&utm_medium=referral&utm_campaign=nav&utm_content=developers",
|
||||||
|
originAttributes: {
|
||||||
|
firstPartyDomain: "",
|
||||||
|
inIsolatedMozBrowser: false,
|
||||||
|
privateBrowsingId: 0,
|
||||||
|
userContextId: 0,
|
||||||
|
geckoViewSessionContextId: "",
|
||||||
|
partitionKey: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "SmIS26zLEdO3ZQBgsLbOywAAAAAAAAAAwAAAAAAAAEY=",
|
||||||
|
output: {
|
||||||
|
URI: false,
|
||||||
|
originAttributes: {
|
||||||
|
firstPartyDomain: "",
|
||||||
|
inIsolatedMozBrowser: false,
|
||||||
|
privateBrowsingId: 0,
|
||||||
|
userContextId: 0,
|
||||||
|
geckoViewSessionContextId: "",
|
||||||
|
partitionKey: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input:
|
||||||
|
"vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezA0NWNhMThkLTQzNmMtNDc0NC1iYmI2LWIxYTE1MzY2ZGY3OX0AAAAA",
|
||||||
|
output: {
|
||||||
|
URISpec: "moz-nullprincipal:{045ca18d-436c-4744-bbb6-b1a15366df79}",
|
||||||
|
originAttributes: {
|
||||||
|
firstPartyDomain: "",
|
||||||
|
inIsolatedMozBrowser: false,
|
||||||
|
privateBrowsingId: 0,
|
||||||
|
userContextId: 0,
|
||||||
|
geckoViewSessionContextId: "",
|
||||||
|
partitionKey: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let test of serializedPrincipalsFromFirefox) {
|
||||||
|
let principal = E10SUtils.deserializePrincipal(test.input);
|
||||||
|
|
||||||
|
for (let key in principal.originAttributes) {
|
||||||
|
is(
|
||||||
|
principal.originAttributes[key],
|
||||||
|
test.output.originAttributes[key],
|
||||||
|
`Ensure value of ${key} is ${test.output.originAttributes[key]}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("URI" in test.output && test.output.URI === false) {
|
||||||
|
is(
|
||||||
|
principal.isContentPrincipal,
|
||||||
|
false,
|
||||||
|
"Should have not have a URI for system"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
is(
|
||||||
|
principal.spec,
|
||||||
|
test.output.URISpec,
|
||||||
|
`Should have spec ${test.output.URISpec}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
11
src/zen/tests/mochitests/gesture/browser.toml
Normal file
11
src/zen/tests/mochitests/gesture/browser.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
prefs = [
|
||||||
|
"zen.window-sync.enabled=false",
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_gesture_navigation.js"]
|
||||||
|
|
||||||
|
["browser_gesture_scroll.js"]
|
||||||
|
skip-if = [
|
||||||
|
"os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 1988390
|
||||||
|
]
|
||||||
231
src/zen/tests/mochitests/gesture/browser_gesture_navigation.js
Normal file
231
src/zen/tests/mochitests/gesture/browser_gesture_navigation.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
add_setup(async () => {
|
||||||
|
// Disable window occlusion. See bug 1733955 / bug 1779559.
|
||||||
|
if (navigator.platform.indexOf("Win") == 0) {
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["widget.windows.window_occlusion_tracking.enabled", false]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async () => {
|
||||||
|
// Open a new browser window to make sure there is no navigation history.
|
||||||
|
const newBrowser = await BrowserTestUtils.openNewBrowserWindow({});
|
||||||
|
|
||||||
|
let event = {
|
||||||
|
direction: SimpleGestureEvent.DIRECTION_LEFT,
|
||||||
|
};
|
||||||
|
ok(!newBrowser.gGestureSupport._shouldDoSwipeGesture(event));
|
||||||
|
|
||||||
|
event = {
|
||||||
|
direction: SimpleGestureEvent.DIRECTION_RIGHT,
|
||||||
|
};
|
||||||
|
ok(!newBrowser.gGestureSupport._shouldDoSwipeGesture(event));
|
||||||
|
|
||||||
|
await BrowserTestUtils.closeWindow(newBrowser);
|
||||||
|
});
|
||||||
|
|
||||||
|
function createSimpleGestureEvent(type, direction) {
|
||||||
|
let event = document.createEvent("SimpleGestureEvent");
|
||||||
|
event.initSimpleGestureEvent(
|
||||||
|
type,
|
||||||
|
false /* canBubble */,
|
||||||
|
false /* cancelableArg */,
|
||||||
|
window,
|
||||||
|
0 /* detail */,
|
||||||
|
0 /* screenX */,
|
||||||
|
0 /* screenY */,
|
||||||
|
0 /* clientX */,
|
||||||
|
0 /* clientY */,
|
||||||
|
false /* ctrlKey */,
|
||||||
|
false /* altKey */,
|
||||||
|
false /* shiftKey */,
|
||||||
|
false /* metaKey */,
|
||||||
|
0 /* button */,
|
||||||
|
null /* relatedTarget */,
|
||||||
|
0 /* allowedDirections */,
|
||||||
|
direction,
|
||||||
|
1 /* delta */
|
||||||
|
);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async () => {
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["ui.swipeAnimationEnabled", false]],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open a new browser window and load two pages so that the browser can go
|
||||||
|
// back but can't go forward.
|
||||||
|
const newWindow = await BrowserTestUtils.openNewBrowserWindow({});
|
||||||
|
|
||||||
|
// gHistroySwipeAnimation gets initialized in a requestIdleCallback so we need
|
||||||
|
// to wait for the initialization.
|
||||||
|
await TestUtils.waitForCondition(() => {
|
||||||
|
return (
|
||||||
|
// There's no explicit notification for the initialization, so we wait
|
||||||
|
// until `isLTR` matches the browser locale state.
|
||||||
|
newWindow.gHistorySwipeAnimation.isLTR != Services.locale.isAppLocaleRTL
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
BrowserTestUtils.startLoadingURIString(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
"about:mozilla"
|
||||||
|
);
|
||||||
|
await BrowserTestUtils.browserLoaded(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
false,
|
||||||
|
"about:mozilla"
|
||||||
|
);
|
||||||
|
BrowserTestUtils.startLoadingURIString(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
"about:about"
|
||||||
|
);
|
||||||
|
await BrowserTestUtils.browserLoaded(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
false,
|
||||||
|
"about:about"
|
||||||
|
);
|
||||||
|
|
||||||
|
let event = createSimpleGestureEvent(
|
||||||
|
"MozSwipeGestureMayStart",
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT
|
||||||
|
);
|
||||||
|
newWindow.gGestureSupport._shouldDoSwipeGesture(event);
|
||||||
|
|
||||||
|
// Assuming we are on LTR environment.
|
||||||
|
is(
|
||||||
|
event.allowedDirections,
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT,
|
||||||
|
"Allows only swiping to left, i.e. backward"
|
||||||
|
);
|
||||||
|
|
||||||
|
event = createSimpleGestureEvent(
|
||||||
|
"MozSwipeGestureMayStart",
|
||||||
|
SimpleGestureEvent.DIRECTION_RIGHT
|
||||||
|
);
|
||||||
|
newWindow.gGestureSupport._shouldDoSwipeGesture(event);
|
||||||
|
is(
|
||||||
|
event.allowedDirections,
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT,
|
||||||
|
"Allows only swiping to left, i.e. backward"
|
||||||
|
);
|
||||||
|
|
||||||
|
await BrowserTestUtils.closeWindow(newWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async () => {
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["ui.swipeAnimationEnabled", true]],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open a new browser window and load two pages so that the browser can go
|
||||||
|
// back but can't go forward.
|
||||||
|
const newWindow = await BrowserTestUtils.openNewBrowserWindow({});
|
||||||
|
|
||||||
|
if (!newWindow.gHistorySwipeAnimation._isSupported()) {
|
||||||
|
await BrowserTestUtils.closeWindow(newWindow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSwipeSequence(sendEnd) {
|
||||||
|
let event = createSimpleGestureEvent(
|
||||||
|
"MozSwipeGestureMayStart",
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT
|
||||||
|
);
|
||||||
|
newWindow.gGestureSupport.handleEvent(event);
|
||||||
|
|
||||||
|
event = createSimpleGestureEvent(
|
||||||
|
"MozSwipeGestureStart",
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT
|
||||||
|
);
|
||||||
|
newWindow.gGestureSupport.handleEvent(event);
|
||||||
|
|
||||||
|
event = createSimpleGestureEvent(
|
||||||
|
"MozSwipeGestureUpdate",
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT
|
||||||
|
);
|
||||||
|
newWindow.gGestureSupport.handleEvent(event);
|
||||||
|
|
||||||
|
event = createSimpleGestureEvent(
|
||||||
|
"MozSwipeGestureUpdate",
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT
|
||||||
|
);
|
||||||
|
newWindow.gGestureSupport.handleEvent(event);
|
||||||
|
|
||||||
|
if (sendEnd) {
|
||||||
|
sendSwipeEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function sendSwipeEnd() {
|
||||||
|
let event = createSimpleGestureEvent(
|
||||||
|
"MozSwipeGestureEnd",
|
||||||
|
SimpleGestureEvent.DIRECTION_LEFT
|
||||||
|
);
|
||||||
|
newWindow.gGestureSupport.handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gHistroySwipeAnimation gets initialized in a requestIdleCallback so we need
|
||||||
|
// to wait for the initialization.
|
||||||
|
await TestUtils.waitForCondition(() => {
|
||||||
|
return (
|
||||||
|
// There's no explicit notification for the initialization, so we wait
|
||||||
|
// until `isLTR` matches the browser locale state.
|
||||||
|
newWindow.gHistorySwipeAnimation.isLTR != Services.locale.isAppLocaleRTL
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
BrowserTestUtils.startLoadingURIString(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
"about:mozilla"
|
||||||
|
);
|
||||||
|
await BrowserTestUtils.browserLoaded(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
false,
|
||||||
|
"about:mozilla"
|
||||||
|
);
|
||||||
|
BrowserTestUtils.startLoadingURIString(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
"about:about"
|
||||||
|
);
|
||||||
|
await BrowserTestUtils.browserLoaded(
|
||||||
|
newWindow.gBrowser.selectedBrowser,
|
||||||
|
false,
|
||||||
|
"about:about"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start a swipe that's not enough to navigate
|
||||||
|
sendSwipeSequence(/* sendEnd = */ true);
|
||||||
|
|
||||||
|
// Wait two frames
|
||||||
|
await new Promise(r =>
|
||||||
|
window.requestAnimationFrame(() => window.requestAnimationFrame(r))
|
||||||
|
);
|
||||||
|
|
||||||
|
// The transition to fully stopped shouldn't have had enough time yet to
|
||||||
|
// become fully stopped.
|
||||||
|
ok(
|
||||||
|
newWindow.gHistorySwipeAnimation._isStoppingAnimation,
|
||||||
|
"should be stopping anim"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start another swipe.
|
||||||
|
sendSwipeSequence(/* sendEnd = */ false);
|
||||||
|
|
||||||
|
// Wait two frames
|
||||||
|
await new Promise(r =>
|
||||||
|
window.requestAnimationFrame(() => window.requestAnimationFrame(r))
|
||||||
|
);
|
||||||
|
|
||||||
|
// We should have started a new swipe, ie we shouldn't be stopping.
|
||||||
|
ok(
|
||||||
|
!newWindow.gHistorySwipeAnimation._isStoppingAnimation,
|
||||||
|
"should not be stopping anim"
|
||||||
|
);
|
||||||
|
|
||||||
|
sendSwipeEnd();
|
||||||
|
|
||||||
|
await BrowserTestUtils.closeWindow(newWindow);
|
||||||
|
});
|
||||||
43
src/zen/tests/mochitests/gesture/browser_gesture_scroll.js
Normal file
43
src/zen/tests/mochitests/gesture/browser_gesture_scroll.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const URL = `data:text/html,
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<div style="height: 200vh"></div>
|
||||||
|
Some content
|
||||||
|
<div style="height: 200vh"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function getScrollPos(browser) {
|
||||||
|
return SpecialPowers.spawn(browser, [], () => {
|
||||||
|
return {
|
||||||
|
scrollY: Math.round(content.scrollY),
|
||||||
|
scrollMaxY: Math.round(content.scrollMaxY),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bug 1964901
|
||||||
|
add_task(async function test_scroll_command() {
|
||||||
|
await BrowserTestUtils.withNewTab(URL, async function (browser) {
|
||||||
|
{
|
||||||
|
let { scrollY, scrollMaxY } = await getScrollPos(browser);
|
||||||
|
is(scrollY, 0, "Should be scrolled to the top");
|
||||||
|
isnot(scrollMaxY, 0, "Should be scrollable");
|
||||||
|
}
|
||||||
|
let scrollEvent = SpecialPowers.spawn(browser, [], async () => {
|
||||||
|
await new Promise(r => {
|
||||||
|
content.addEventListener("scrollend", r, { once: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
goDoCommand("cmd_scrollBottom");
|
||||||
|
await scrollEvent;
|
||||||
|
{
|
||||||
|
let { scrollY, scrollMaxY } = await getScrollPos(browser);
|
||||||
|
is(scrollY, scrollMaxY, "Should be scrolled to the bottom");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/zen/tests/mochitests/linkHandling/browser.toml
Normal file
12
src/zen/tests/mochitests/linkHandling/browser.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
|
||||||
|
["browser_contentAreaClick_subframe_javascript.js"]
|
||||||
|
support-files = [
|
||||||
|
"file_contentAreaClick_subframe_javascript.html"
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_javascript_links.js"]
|
||||||
|
disabled="Disabled by import_external_tests.py"
|
||||||
|
support-files = [
|
||||||
|
"file_javascript_links_subframe.html"
|
||||||
|
]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
const gExampleComRoot = getRootDirectory(gTestPath).replace(
|
||||||
|
"chrome://mochitests/content/",
|
||||||
|
"https://example.com/"
|
||||||
|
);
|
||||||
|
const IFRAME_FILE = "file_contentAreaClick_subframe_javascript.html";
|
||||||
|
|
||||||
|
add_task(async function () {
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["browser.link.alternative_click.block_javascript", false]],
|
||||||
|
});
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
`data:text/html,<iframe src="${gExampleComRoot + IFRAME_FILE}"></iframe>`,
|
||||||
|
async browser => {
|
||||||
|
let newTabPromise = BrowserTestUtils.waitForNewTab(
|
||||||
|
gBrowser,
|
||||||
|
"about:blank"
|
||||||
|
);
|
||||||
|
let javascriptRanPromise = TestUtils.topicObserved(
|
||||||
|
"contentAreaClick-javascriptRan"
|
||||||
|
);
|
||||||
|
|
||||||
|
// ctrl/cmd-click the link in the subframe. This should cause it to be
|
||||||
|
// loaded in a new tab.
|
||||||
|
info("Clicking link");
|
||||||
|
let expectedRemoteType =
|
||||||
|
browser.browsingContext.children[0].currentRemoteType;
|
||||||
|
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||||
|
"a",
|
||||||
|
{ ctrlKey: true, metaKey: true },
|
||||||
|
browser.browsingContext.children[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
info("Waiting for new tab");
|
||||||
|
let newTab = await newTabPromise;
|
||||||
|
|
||||||
|
info("Waiting to be notified that the javascript ran");
|
||||||
|
await javascriptRanPromise;
|
||||||
|
is(
|
||||||
|
newTab.linkedBrowser.remoteType,
|
||||||
|
expectedRemoteType,
|
||||||
|
"new tab was loaded in expected process"
|
||||||
|
);
|
||||||
|
|
||||||
|
info("Removing the tab");
|
||||||
|
BrowserTestUtils.removeTab(newTab);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const TEST_PATH = getRootDirectory(gTestPath).replace(
|
||||||
|
"chrome://mochitests/content/",
|
||||||
|
"https://example.com/"
|
||||||
|
);
|
||||||
|
const IFRAME_PATH = TEST_PATH + "file_javascript_links_subframe.html";
|
||||||
|
|
||||||
|
add_setup(async function () {
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [
|
||||||
|
["browser.link.alternative_click.block_javascript", true],
|
||||||
|
["browser.tabs.opentabfor.middleclick", true],
|
||||||
|
["middlemouse.paste", false],
|
||||||
|
["middlemouse.contentLoadURL", false],
|
||||||
|
["general.autoScroll", false],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function () {
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
`data:text/html,<a href="javascript:alert(1);">click me`,
|
||||||
|
async browser => {
|
||||||
|
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||||
|
"a",
|
||||||
|
{ button: 0, ctrlKey: true, metaKey: true },
|
||||||
|
browser
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
gBrowser.tabs.length,
|
||||||
|
2,
|
||||||
|
"Accel+click on javascript: link shouldn't open a new tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||||
|
"a",
|
||||||
|
{ button: 1 },
|
||||||
|
browser
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
gBrowser.tabs.length,
|
||||||
|
2,
|
||||||
|
"Middle click on javascript: link shouldn't open a new tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||||
|
"a",
|
||||||
|
{ button: 0, shiftKey: true },
|
||||||
|
browser
|
||||||
|
);
|
||||||
|
// This is fragile and might miss the new window, but the test will fail
|
||||||
|
// anyway when finishing with an extra window left behind.
|
||||||
|
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
is(
|
||||||
|
BrowserWindowTracker.windowCount,
|
||||||
|
1,
|
||||||
|
"Shift+click on javascript: link shouldn't open a new window"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function iframe_link() {
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
`data:text/html,<iframe src="${IFRAME_PATH}"></iframe>`,
|
||||||
|
async browser => {
|
||||||
|
// ctrl/cmd-click the link in the subframe.
|
||||||
|
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||||
|
"a",
|
||||||
|
{ ctrlKey: true, metaKey: true },
|
||||||
|
browser.browsingContext.children[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
is(
|
||||||
|
gBrowser.tabs.length,
|
||||||
|
2,
|
||||||
|
"Click on javascript: link in iframe shouldn't open a new tab"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<a href="javascript:SpecialPowers.notifyObserversInParentProcess(null, 'contentAreaClick-javascriptRan');">link</a>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<a href="javascript:alert(1)">click me
|
||||||
@@ -8,11 +8,21 @@
|
|||||||
|
|
||||||
|
|
||||||
BROWSER_CHROME_MANIFESTS += [
|
BROWSER_CHROME_MANIFESTS += [
|
||||||
|
"alerts/browser.toml",
|
||||||
|
"backforward/browser.toml",
|
||||||
|
"caps/browser.toml",
|
||||||
|
"gesture/browser.toml",
|
||||||
|
"linkHandling/browser.toml",
|
||||||
"readermode/browser.toml",
|
"readermode/browser.toml",
|
||||||
"safebrowsing/browser.toml",
|
"safebrowsing/browser.toml",
|
||||||
|
"sessionstore/browser.toml",
|
||||||
"shell/browser.toml",
|
"shell/browser.toml",
|
||||||
"tabMediaIndicator/browser.toml",
|
"tabMediaIndicator/browser.toml",
|
||||||
"tooltiptext/browser.toml",
|
"tooltiptext/browser.toml",
|
||||||
|
"zoom/browser.toml",
|
||||||
]
|
]
|
||||||
XPCSHELL_TESTS_MANIFESTS += [
|
XPCSHELL_TESTS_MANIFESTS += [
|
||||||
|
"remote-settings/xpcshell.toml",
|
||||||
|
"services-crypto/xpcshell.toml",
|
||||||
|
"sync/xpcshell.toml",
|
||||||
]
|
]
|
||||||
86
src/zen/tests/mochitests/remote-settings/head_settings.js
Normal file
86
src/zen/tests/mochitests/remote-settings/head_settings.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/* import-globals-from ../../../common/tests/unit/head_global.js */
|
||||||
|
/* import-globals-from ../../../common/tests/unit/head_helpers.js */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(this, {
|
||||||
|
AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
|
||||||
|
Database: "resource://services-settings/Database.sys.mjs",
|
||||||
|
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
||||||
|
RemoteSettingsClient:
|
||||||
|
"resource://services-settings/RemoteSettingsClient.sys.mjs",
|
||||||
|
RemoteSettingsWorker:
|
||||||
|
"resource://services-settings/RemoteSettingsWorker.sys.mjs",
|
||||||
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||||
|
SharedUtils: "resource://services-settings/SharedUtils.sys.mjs",
|
||||||
|
SyncHistory: "resource://services-settings/SyncHistory.sys.mjs",
|
||||||
|
TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
|
||||||
|
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
||||||
|
UptakeTelemetry: "resource://services-settings/UptakeTelemetry.sys.mjs",
|
||||||
|
Utils: "resource://services-settings/Utils.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
const IS_ANDROID = AppConstants.platform == "android";
|
||||||
|
|
||||||
|
function arrayEqual(a, b) {
|
||||||
|
return JSON.stringify(a) == JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_setup(function () {
|
||||||
|
Services.fog.initializeFOG();
|
||||||
|
});
|
||||||
|
|
||||||
|
function enableUptakeMetric() {
|
||||||
|
Services.fog.applyServerKnobsConfig(
|
||||||
|
JSON.stringify({
|
||||||
|
metrics_enabled: {
|
||||||
|
"uptake.remotecontent.result.uptake_remotesettings": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertTelemetryEvents(expectedEvents) {
|
||||||
|
if (IS_ANDROID) {
|
||||||
|
/*
|
||||||
|
Since `Services.fog.applyServerKnobsConfig()` has no effect on Android
|
||||||
|
https://searchfox.org/firefox-main/rev/e28b34ab33dbf49364999070168cbb7e11e8e5bd/toolkit/components/glean/xpcom/FOG.cpp#379-390
|
||||||
|
we skip assertions on events on this platform.
|
||||||
|
Alternatively we could have disabled the whole tests suites or introduced `if IS_ANDROID` on each
|
||||||
|
test involving telemetry, but this seems the most reasonable way.
|
||||||
|
See Bug 2028765 and Bug 2042499
|
||||||
|
*/
|
||||||
|
Assert.ok(
|
||||||
|
true,
|
||||||
|
"Glean Uptake Telemetry assertions are ALWAYS true on Android"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const events =
|
||||||
|
Glean.uptakeRemotecontentResult.uptakeRemotesettings.testGetValue() ?? [];
|
||||||
|
const receivedValues = events.map(e => e.extra.value);
|
||||||
|
Assert.equal(
|
||||||
|
events.length,
|
||||||
|
expectedEvents.length,
|
||||||
|
`number of uptake events (${receivedValues})`
|
||||||
|
);
|
||||||
|
for (let i = 0; i < expectedEvents.length; i++) {
|
||||||
|
for (const [key, expected] of Object.entries(expectedEvents[i])) {
|
||||||
|
if (typeof expected === "function") {
|
||||||
|
Assert.ok(
|
||||||
|
expected(events[i].extra[key]),
|
||||||
|
`event[${i}].extra.${key} passes validator`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Assert.equal(
|
||||||
|
events[i].extra[key],
|
||||||
|
expected,
|
||||||
|
`event[${i}].extra.${key}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,896 @@
|
|||||||
|
const { Downloader } = ChromeUtils.importESModule(
|
||||||
|
"resource://services-settings/Attachments.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const RECORD = {
|
||||||
|
id: "1f3a0802-648d-11ea-bd79-876a8b69c377",
|
||||||
|
attachment: {
|
||||||
|
hash: "f41ed47d0f43325c9f089d03415c972ce1d3f1ecab6e4d6260665baf3db3ccee",
|
||||||
|
size: 1597,
|
||||||
|
filename: "test_file.pem",
|
||||||
|
location:
|
||||||
|
"main-workspace/some-collection/65650a0f-7c22-4c10-9744-2d67e301f5f4.pem",
|
||||||
|
mimetype: "application/x-pem-file",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const RECORD_OF_DUMP = {
|
||||||
|
id: "filename-of-dump.txt",
|
||||||
|
attachment: {
|
||||||
|
filename: "filename-of-dump.txt",
|
||||||
|
hash: "4c46ef7e4f1951d210fe54c21e07c09bab265fd122580083ed1d6121547a8c6b",
|
||||||
|
size: 25,
|
||||||
|
},
|
||||||
|
last_modified: 1234567,
|
||||||
|
some_key: "some metadata",
|
||||||
|
};
|
||||||
|
|
||||||
|
let downloader;
|
||||||
|
let server;
|
||||||
|
|
||||||
|
add_setup(() => {
|
||||||
|
server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
registerCleanupFunction(() => server.stop(() => {}));
|
||||||
|
|
||||||
|
server.registerDirectory(
|
||||||
|
"/cdn/main-workspace/some-collection/",
|
||||||
|
do_get_file("test_attachments_downloader")
|
||||||
|
);
|
||||||
|
server.registerDirectory(
|
||||||
|
"/cdn/bundles/",
|
||||||
|
do_get_file("test_attachments_downloader")
|
||||||
|
);
|
||||||
|
|
||||||
|
// For this test, we are using a server other than production. Force
|
||||||
|
// LOAD_DUMPS to true so that we can still load attachments from dumps.
|
||||||
|
delete Utils.LOAD_DUMPS;
|
||||||
|
Utils.LOAD_DUMPS = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
|
||||||
|
downloader = new Downloader("main", "some-collection");
|
||||||
|
downloader.cache = {};
|
||||||
|
const memCacheImpl = {
|
||||||
|
get: async id => {
|
||||||
|
return downloader.cache[id];
|
||||||
|
},
|
||||||
|
set: async (id, obj) => {
|
||||||
|
downloader.cache[id] = obj;
|
||||||
|
},
|
||||||
|
setMultiple: async idsObjs => {
|
||||||
|
idsObjs.forEach(([id, obj]) => (downloader.cache[id] = obj));
|
||||||
|
},
|
||||||
|
delete: async id => {
|
||||||
|
delete downloader.cache[id];
|
||||||
|
},
|
||||||
|
deleteMultiple: async ids => {
|
||||||
|
ids.forEach(id => delete downloader.cache[id]);
|
||||||
|
},
|
||||||
|
hasData: async () => {
|
||||||
|
return !!Object.keys(downloader.cache).length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// The download() method requires a cacheImpl, but the Downloader
|
||||||
|
// class does not have one. Define a dummy no-op one.
|
||||||
|
Object.defineProperty(downloader, "cacheImpl", {
|
||||||
|
value: memCacheImpl,
|
||||||
|
// Writable to allow specific tests to override cacheImpl.
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
await downloader.deleteDownloaded(RECORD);
|
||||||
|
|
||||||
|
server.registerPathHandler("/v1/", (request, response) => {
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
capabilities: {
|
||||||
|
attachments: {
|
||||||
|
base_url: `http://localhost:${server.identity.primaryPort}/cdn/`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
// For tests that use a real client and DB cache, clear the local DB too.
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
await client.db.clear();
|
||||||
|
await client.db.pruneAttachments([]);
|
||||||
|
}
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_download_throws_server_info_error_if_invalid_response() {
|
||||||
|
server.registerPathHandler("/v1/", (request, response) => {
|
||||||
|
response.write("{bad json content");
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await downloader.download(RECORD);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.ok(error instanceof Downloader.ServerInfoError);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_download_is_retried_3_times_if_download_fails() {
|
||||||
|
const record = {
|
||||||
|
id: "abc",
|
||||||
|
attachment: {
|
||||||
|
...RECORD.attachment,
|
||||||
|
location: "404-error.pem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let called = 0;
|
||||||
|
const _fetchAttachment = downloader._fetchAttachment;
|
||||||
|
downloader._fetchAttachment = async url => {
|
||||||
|
called++;
|
||||||
|
return _fetchAttachment(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await downloader.download(record);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.equal(called, 4); // 1 + 3 retries
|
||||||
|
Assert.ok(error instanceof Downloader.DownloadError);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_download_as_bytes() {
|
||||||
|
const bytes = await downloader.downloadAsBytes(RECORD);
|
||||||
|
|
||||||
|
// See *.pem file in tests data.
|
||||||
|
Assert.greater(
|
||||||
|
bytes.byteLength,
|
||||||
|
1500,
|
||||||
|
`Wrong bytes size: ${bytes.byteLength}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_download_is_retried_3_times_if_content_fails() {
|
||||||
|
const record = {
|
||||||
|
id: "abc",
|
||||||
|
attachment: {
|
||||||
|
...RECORD.attachment,
|
||||||
|
hash: "always-wrong",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let called = 0;
|
||||||
|
downloader._fetchAttachment = async () => {
|
||||||
|
called++;
|
||||||
|
return new ArrayBuffer();
|
||||||
|
};
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await downloader.download(record);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.equal(called, 4); // 1 + 3 retries
|
||||||
|
Assert.ok(error instanceof Downloader.BadContentError);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_delete_all() {
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
await client.db.create(RECORD);
|
||||||
|
await downloader.download(RECORD);
|
||||||
|
|
||||||
|
await client.attachments.deleteAll();
|
||||||
|
|
||||||
|
Assert.ok(!(await client.attachments.cacheImpl.get(RECORD.id)));
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_downloader_reports_download_errors() {
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
attachment: {
|
||||||
|
...RECORD.attachment,
|
||||||
|
location: "404-error.pem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.attachments.download(record, { retry: 0 });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
assertTelemetryEvents([
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.DOWNLOAD_START,
|
||||||
|
source: client.identifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.DOWNLOAD_ERROR,
|
||||||
|
source: client.identifier,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_downloader_reports_offline_error() {
|
||||||
|
const backupOffline = Services.io.offline;
|
||||||
|
Services.io.offline = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
const record = {
|
||||||
|
attachment: {
|
||||||
|
...RECORD.attachment,
|
||||||
|
location: "will-try-and-fail.pem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await client.attachments.download(record, { retry: 0 });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
assertTelemetryEvents([
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.DOWNLOAD_START,
|
||||||
|
source: client.identifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR,
|
||||||
|
source: client.identifier,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
Services.io.offline = backupOffline;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
// Common code for test_download_cache_hit and test_download_cache_corruption.
|
||||||
|
async function doTestDownloadCacheImpl({
|
||||||
|
simulateCorruption,
|
||||||
|
expectedReads = 1,
|
||||||
|
expectedWrites = 1,
|
||||||
|
downloadOptions = {},
|
||||||
|
}) {
|
||||||
|
let readCount = 0;
|
||||||
|
let writeCount = 0;
|
||||||
|
const cacheImpl = {
|
||||||
|
async get(attachmentId) {
|
||||||
|
Assert.equal(attachmentId, RECORD.id, "expected attachmentId");
|
||||||
|
++readCount;
|
||||||
|
if (simulateCorruption) {
|
||||||
|
throw new Error("Simulation of corrupted cache (read)");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async set(attachmentId, attachment) {
|
||||||
|
Assert.equal(attachmentId, RECORD.id, "expected attachmentId");
|
||||||
|
Assert.deepEqual(attachment.record, RECORD, "expected record");
|
||||||
|
++writeCount;
|
||||||
|
if (simulateCorruption) {
|
||||||
|
throw new Error("Simulation of corrupted cache (write)");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async delete() {},
|
||||||
|
};
|
||||||
|
Object.defineProperty(downloader, "cacheImpl", { value: cacheImpl });
|
||||||
|
|
||||||
|
let downloadResult = await downloader.download(RECORD, downloadOptions);
|
||||||
|
Assert.equal(downloadResult._source, "remote_match", "expected source");
|
||||||
|
Assert.equal(downloadResult.buffer.byteLength, 1597, "expected result");
|
||||||
|
Assert.equal(readCount, expectedReads, "expected cache read attempts");
|
||||||
|
Assert.equal(writeCount, expectedWrites, "expected cache write attempts");
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_download_cache_hit() {
|
||||||
|
await doTestDownloadCacheImpl({ simulateCorruption: false });
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
// Verify that the downloader works despite a broken cache implementation.
|
||||||
|
add_task(async function test_download_cache_corruption() {
|
||||||
|
await doTestDownloadCacheImpl({ simulateCorruption: true });
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_download_with_cache_enabled() {
|
||||||
|
await doTestDownloadCacheImpl({
|
||||||
|
simulateCorruption: false,
|
||||||
|
downloadOptions: {
|
||||||
|
cacheResult: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_download_with_cache_disabled() {
|
||||||
|
await doTestDownloadCacheImpl({
|
||||||
|
simulateCorruption: false,
|
||||||
|
expectedWrites: 0,
|
||||||
|
downloadOptions: {
|
||||||
|
cacheResult: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_download_cached() {
|
||||||
|
const client = RemoteSettings("main", "some-collection");
|
||||||
|
const attachmentId = "dummy filename";
|
||||||
|
const badRecord = {
|
||||||
|
attachment: {
|
||||||
|
...RECORD.attachment,
|
||||||
|
hash: "non-matching hash",
|
||||||
|
location: "non-existing-location-should-fail.bin",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
async function downloadWithCache(record, options) {
|
||||||
|
options = { ...options, useCache: true };
|
||||||
|
return client.attachments.download(record, options);
|
||||||
|
}
|
||||||
|
function checkInfo(downloadResult, expectedSource, msg) {
|
||||||
|
Assert.deepEqual(
|
||||||
|
downloadResult.record,
|
||||||
|
RECORD,
|
||||||
|
`${msg} : expected identical record`
|
||||||
|
);
|
||||||
|
// Simple check: assume that content is identical if the size matches.
|
||||||
|
Assert.equal(
|
||||||
|
downloadResult.buffer.byteLength,
|
||||||
|
RECORD.attachment.size,
|
||||||
|
`${msg} : expected buffer`
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
downloadResult._source,
|
||||||
|
expectedSource,
|
||||||
|
`${msg} : expected source of the result`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
downloadWithCache(null, { attachmentId }),
|
||||||
|
/DownloadError: Could not download dummy filename/,
|
||||||
|
"Download without record or cache should fail."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Populate cache.
|
||||||
|
const info1 = await downloadWithCache(RECORD, { attachmentId });
|
||||||
|
checkInfo(info1, "remote_match", "first time download");
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
downloadWithCache(null, { attachmentId }),
|
||||||
|
/DownloadError: Could not download dummy filename/,
|
||||||
|
"Download without record still fails even if there is a cache."
|
||||||
|
);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
downloadWithCache(badRecord, { attachmentId }),
|
||||||
|
/DownloadError: Could not download .*non-existing-location-should-fail.bin/,
|
||||||
|
"Download with non-matching record still fails even if there is a cache."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download from cache.
|
||||||
|
const info2 = await downloadWithCache(RECORD, { attachmentId });
|
||||||
|
checkInfo(info2, "cache_match", "download matching record from cache");
|
||||||
|
|
||||||
|
const info3 = await downloadWithCache(RECORD, {
|
||||||
|
attachmentId,
|
||||||
|
fallbackToCache: true,
|
||||||
|
});
|
||||||
|
checkInfo(info3, "cache_match", "fallbackToCache accepts matching record");
|
||||||
|
|
||||||
|
const info4 = await downloadWithCache(null, {
|
||||||
|
attachmentId,
|
||||||
|
fallbackToCache: true,
|
||||||
|
});
|
||||||
|
checkInfo(info4, "cache_fallback", "fallbackToCache accepts null record");
|
||||||
|
|
||||||
|
const info5 = await downloadWithCache(badRecord, {
|
||||||
|
attachmentId,
|
||||||
|
fallbackToCache: true,
|
||||||
|
});
|
||||||
|
checkInfo(info5, "cache_fallback", "fallbackToCache ignores bad record");
|
||||||
|
|
||||||
|
// Bye bye cache.
|
||||||
|
await client.attachments.deleteDownloaded({ id: attachmentId });
|
||||||
|
await Assert.rejects(
|
||||||
|
downloadWithCache(null, { attachmentId, fallbackToCache: true }),
|
||||||
|
/DownloadError: Could not download dummy filename/,
|
||||||
|
"Download without cache should fail again."
|
||||||
|
);
|
||||||
|
await Assert.rejects(
|
||||||
|
downloadWithCache(badRecord, { attachmentId, fallbackToCache: true }),
|
||||||
|
/DownloadError: Could not download .*non-existing-location-should-fail.bin/,
|
||||||
|
"Download should fail to fall back to a download of a non-existing record"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_download_from_dump() {
|
||||||
|
const client = RemoteSettings("dump-collection", {
|
||||||
|
bucketName: "dump-bucket",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Temporarily replace the resource:-URL with another resource:-URL.
|
||||||
|
const orig_RESOURCE_BASE_URL = Downloader._RESOURCE_BASE_URL;
|
||||||
|
Downloader._RESOURCE_BASE_URL = "resource://rs-downloader-test";
|
||||||
|
const resProto = Services.io
|
||||||
|
.getProtocolHandler("resource")
|
||||||
|
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||||
|
resProto.setSubstitution(
|
||||||
|
"rs-downloader-test",
|
||||||
|
Services.io.newFileURI(do_get_file("test_attachments_downloader"))
|
||||||
|
);
|
||||||
|
|
||||||
|
function checkInfo(result, expectedSource, expectedRecord = RECORD_OF_DUMP) {
|
||||||
|
Assert.equal(
|
||||||
|
new TextDecoder().decode(new Uint8Array(result.buffer)),
|
||||||
|
"This would be a RS dump.\n",
|
||||||
|
"expected content from dump"
|
||||||
|
);
|
||||||
|
Assert.deepEqual(result.record, expectedRecord, "expected record for dump");
|
||||||
|
Assert.equal(result._source, expectedSource, "expected source of dump");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If record matches, should happen before network request.
|
||||||
|
const dump1 = await client.attachments.download(RECORD_OF_DUMP, {
|
||||||
|
// Note: attachmentId not set, so should fall back to record.id.
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump1, "dump_match");
|
||||||
|
|
||||||
|
// If no record given, should try network first, but then fall back to dump.
|
||||||
|
const dump2 = await client.attachments.download(null, {
|
||||||
|
attachmentId: RECORD_OF_DUMP.id,
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump2, "dump_fallback");
|
||||||
|
|
||||||
|
// Fill the cache with the same data as the dump for the next part.
|
||||||
|
await client.db.saveAttachment(RECORD_OF_DUMP.id, {
|
||||||
|
record: RECORD_OF_DUMP,
|
||||||
|
blob: new Blob([dump1.buffer]),
|
||||||
|
});
|
||||||
|
// The dump should take precedence over the cache.
|
||||||
|
const dump3 = await client.attachments.download(RECORD_OF_DUMP, {
|
||||||
|
fallbackToCache: true,
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump3, "dump_match");
|
||||||
|
|
||||||
|
// When the record is not given, the dump takes precedence over the cache
|
||||||
|
// as a fallback (when the cache and dump are identical).
|
||||||
|
const dump4 = await client.attachments.download(null, {
|
||||||
|
attachmentId: RECORD_OF_DUMP.id,
|
||||||
|
fallbackToCache: true,
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump4, "dump_fallback");
|
||||||
|
|
||||||
|
// Store a record in the cache that is newer than the dump.
|
||||||
|
const RECORD_NEWER_THAN_DUMP = {
|
||||||
|
...RECORD_OF_DUMP,
|
||||||
|
last_modified: RECORD_OF_DUMP.last_modified + 1,
|
||||||
|
};
|
||||||
|
await client.db.saveAttachment(RECORD_OF_DUMP.id, {
|
||||||
|
record: RECORD_NEWER_THAN_DUMP,
|
||||||
|
blob: new Blob([dump1.buffer]),
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the record is not given, use the cache if it has a more recent record.
|
||||||
|
const dump5 = await client.attachments.download(null, {
|
||||||
|
attachmentId: RECORD_OF_DUMP.id,
|
||||||
|
fallbackToCache: true,
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump5, "cache_fallback", RECORD_NEWER_THAN_DUMP);
|
||||||
|
|
||||||
|
// When a record is given, use whichever that has the matching last_modified.
|
||||||
|
const dump6 = await client.attachments.download(RECORD_OF_DUMP, {
|
||||||
|
fallbackToCache: true,
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump6, "dump_match");
|
||||||
|
const dump7 = await client.attachments.download(RECORD_NEWER_THAN_DUMP, {
|
||||||
|
fallbackToCache: true,
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump7, "cache_match", RECORD_NEWER_THAN_DUMP);
|
||||||
|
|
||||||
|
await client.attachments.deleteDownloaded(RECORD_OF_DUMP);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
client.attachments.download(null, {
|
||||||
|
attachmentId: "filename-without-meta.txt",
|
||||||
|
fallbackToDump: true,
|
||||||
|
}),
|
||||||
|
/DownloadError: Could not download filename-without-meta.txt/,
|
||||||
|
"Cannot download dump that lacks a .meta.json file"
|
||||||
|
);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
client.attachments.download(null, {
|
||||||
|
attachmentId: "filename-without-content.txt",
|
||||||
|
fallbackToDump: true,
|
||||||
|
}),
|
||||||
|
/Could not download resource:\/\/rs-downloader-test\/settings\/dump-bucket\/dump-collection\/filename-without-content\.txt(?!\.meta\.json)/,
|
||||||
|
"Cannot download dump that is missing, despite the existing .meta.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore, just in case.
|
||||||
|
Downloader._RESOURCE_BASE_URL = orig_RESOURCE_BASE_URL;
|
||||||
|
resProto.setSubstitution("rs-downloader-test", null);
|
||||||
|
});
|
||||||
|
// Not really needed because the last test doesn't modify the main collection,
|
||||||
|
// but added for consistency with other tests tasks around here.
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_download_from_dump_fails_when_load_dumps_is_false() {
|
||||||
|
const client = RemoteSettings("dump-collection", {
|
||||||
|
bucketName: "dump-bucket",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Temporarily replace the resource:-URL with another resource:-URL.
|
||||||
|
const orig_RESOURCE_BASE_URL = Downloader._RESOURCE_BASE_URL;
|
||||||
|
Downloader._RESOURCE_BASE_URL = "resource://rs-downloader-test";
|
||||||
|
const resProto = Services.io
|
||||||
|
.getProtocolHandler("resource")
|
||||||
|
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||||
|
resProto.setSubstitution(
|
||||||
|
"rs-downloader-test",
|
||||||
|
Services.io.newFileURI(do_get_file("test_attachments_downloader"))
|
||||||
|
);
|
||||||
|
|
||||||
|
function checkInfo(
|
||||||
|
result,
|
||||||
|
expectedSource,
|
||||||
|
expectedRecord = RECORD_OF_DUMP
|
||||||
|
) {
|
||||||
|
Assert.equal(
|
||||||
|
new TextDecoder().decode(new Uint8Array(result.buffer)),
|
||||||
|
"This would be a RS dump.\n",
|
||||||
|
"expected content from dump"
|
||||||
|
);
|
||||||
|
Assert.deepEqual(
|
||||||
|
result.record,
|
||||||
|
expectedRecord,
|
||||||
|
"expected record for dump"
|
||||||
|
);
|
||||||
|
Assert.equal(result._source, expectedSource, "expected source of dump");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the dump so that we can use it to fill the cache.
|
||||||
|
const dump1 = await client.attachments.download(RECORD_OF_DUMP, {
|
||||||
|
// Note: attachmentId not set, so should fall back to record.id.
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump1, "dump_match");
|
||||||
|
|
||||||
|
// Fill the cache with the same data as the dump for the next part.
|
||||||
|
await client.db.saveAttachment(RECORD_OF_DUMP.id, {
|
||||||
|
record: RECORD_OF_DUMP,
|
||||||
|
blob: new Blob([dump1.buffer]),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now turn off loading dumps, and check we no longer load from the dump,
|
||||||
|
// but use the cache instead.
|
||||||
|
Utils.LOAD_DUMPS = false;
|
||||||
|
|
||||||
|
const dump2 = await client.attachments.download(RECORD_OF_DUMP, {
|
||||||
|
// Note: attachmentId not set, so should fall back to record.id.
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump2, "cache_match");
|
||||||
|
|
||||||
|
// When the record is not given, the dump would take precedence over the
|
||||||
|
// cache but we have disabled dumps, so we should load from the cache.
|
||||||
|
const dump4 = await client.attachments.download(null, {
|
||||||
|
attachmentId: RECORD_OF_DUMP.id,
|
||||||
|
fallbackToCache: true,
|
||||||
|
fallbackToDump: true,
|
||||||
|
});
|
||||||
|
checkInfo(dump4, "cache_fallback");
|
||||||
|
|
||||||
|
// Restore, just in case.
|
||||||
|
Utils.LOAD_DUMPS = true;
|
||||||
|
Downloader._RESOURCE_BASE_URL = orig_RESOURCE_BASE_URL;
|
||||||
|
resProto.setSubstitution("rs-downloader-test", null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
add_task(async function test_attachment_get() {
|
||||||
|
// Since get() is largely a wrapper around the same code as download(),
|
||||||
|
// we only test a couple of parts to check it functions as expected, and
|
||||||
|
// rely on the download() testing for the rest.
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
downloader.get(RECORD),
|
||||||
|
/NotFoundError: Could not find /,
|
||||||
|
"get() fails when there is no local cache nor dump"
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = RemoteSettings("dump-collection", {
|
||||||
|
bucketName: "dump-bucket",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Temporarily replace the resource:-URL with another resource:-URL.
|
||||||
|
const orig_RESOURCE_BASE_URL = Downloader._RESOURCE_BASE_URL;
|
||||||
|
Downloader._RESOURCE_BASE_URL = "resource://rs-downloader-test";
|
||||||
|
const resProto = Services.io
|
||||||
|
.getProtocolHandler("resource")
|
||||||
|
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||||
|
resProto.setSubstitution(
|
||||||
|
"rs-downloader-test",
|
||||||
|
Services.io.newFileURI(do_get_file("test_attachments_downloader"))
|
||||||
|
);
|
||||||
|
|
||||||
|
function checkInfo(result, expectedSource, expectedRecord = RECORD_OF_DUMP) {
|
||||||
|
Assert.equal(
|
||||||
|
new TextDecoder().decode(new Uint8Array(result.buffer)),
|
||||||
|
"This would be a RS dump.\n",
|
||||||
|
"expected content from dump"
|
||||||
|
);
|
||||||
|
Assert.deepEqual(result.record, expectedRecord, "expected record for dump");
|
||||||
|
Assert.equal(result._source, expectedSource, "expected source of dump");
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a record is given, use whichever that has the matching last_modified.
|
||||||
|
const dump = await client.attachments.get(RECORD_OF_DUMP);
|
||||||
|
checkInfo(dump, "dump_match");
|
||||||
|
|
||||||
|
await client.attachments.deleteDownloaded(RECORD_OF_DUMP);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
client.attachments.get(null, {
|
||||||
|
attachmentId: "filename-without-meta.txt",
|
||||||
|
fallbackToDump: true,
|
||||||
|
}),
|
||||||
|
/NotFoundError: Could not find filename-without-meta.txt in cache or dump/,
|
||||||
|
"Cannot download dump that lacks a .meta.json file"
|
||||||
|
);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
client.attachments.get(null, {
|
||||||
|
attachmentId: "filename-without-content.txt",
|
||||||
|
fallbackToDump: true,
|
||||||
|
}),
|
||||||
|
/Could not download resource:\/\/rs-downloader-test\/settings\/dump-bucket\/dump-collection\/filename-without-content\.txt(?!\.meta\.json)/,
|
||||||
|
"Cannot download dump that is missing, despite the existing .meta.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore, just in case.
|
||||||
|
Downloader._RESOURCE_BASE_URL = orig_RESOURCE_BASE_URL;
|
||||||
|
resProto.setSubstitution("rs-downloader-test", null);
|
||||||
|
});
|
||||||
|
// Not really needed because the last test doesn't modify the main collection,
|
||||||
|
// but added for consistency with other tests tasks around here.
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_obsolete_attachments_are_pruned() {
|
||||||
|
const RECORD2 = {
|
||||||
|
...RECORD,
|
||||||
|
id: "another-id",
|
||||||
|
};
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
// Store records and related attachments directly in the cache.
|
||||||
|
await client.db.importChanges({}, 42, [RECORD, RECORD2], { clear: true });
|
||||||
|
await client.db.saveAttachment(RECORD.id, {
|
||||||
|
record: RECORD,
|
||||||
|
blob: new Blob(["123"]),
|
||||||
|
});
|
||||||
|
await client.db.saveAttachment("custom-id", {
|
||||||
|
record: RECORD2,
|
||||||
|
blob: new Blob(["456"]),
|
||||||
|
});
|
||||||
|
// Store an extraneous cached attachment.
|
||||||
|
await client.db.saveAttachment("bar", {
|
||||||
|
record: { id: "bar" },
|
||||||
|
blob: new Blob(["789"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordAttachment = await client.attachments.cacheImpl.get(RECORD.id);
|
||||||
|
Assert.equal(
|
||||||
|
await recordAttachment.blob.text(),
|
||||||
|
"123",
|
||||||
|
"Record has a cached attachment"
|
||||||
|
);
|
||||||
|
const record2Attachment = await client.attachments.cacheImpl.get("custom-id");
|
||||||
|
Assert.equal(
|
||||||
|
await record2Attachment.blob.text(),
|
||||||
|
"456",
|
||||||
|
"Record 2 has a cached attachment"
|
||||||
|
);
|
||||||
|
const { blob: cachedExtra } = await client.attachments.cacheImpl.get("bar");
|
||||||
|
Assert.equal(await cachedExtra.text(), "789", "There is an extra attachment");
|
||||||
|
|
||||||
|
await client.attachments.prune([]);
|
||||||
|
|
||||||
|
Assert.ok(
|
||||||
|
await client.attachments.cacheImpl.get(RECORD.id),
|
||||||
|
"Record attachment was kept"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
await client.attachments.cacheImpl.get("custom-id"),
|
||||||
|
"Record 2 attachment was kept"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!(await client.attachments.cacheImpl.get("bar")),
|
||||||
|
"Extra was deleted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_obsolete_attachments_listed_as_excluded_are_not_pruned() {
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
// Store records and related attachments directly in the cache.
|
||||||
|
await client.db.importChanges({}, 42, [], { clear: true });
|
||||||
|
await client.db.saveAttachment(RECORD.id, {
|
||||||
|
record: RECORD,
|
||||||
|
blob: new Blob(["123"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordAttachment = await client.attachments.cacheImpl.get(RECORD.id);
|
||||||
|
Assert.equal(
|
||||||
|
await recordAttachment.blob.text(),
|
||||||
|
"123",
|
||||||
|
"Record has a cached attachment"
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.attachments.prune([RECORD.id]);
|
||||||
|
|
||||||
|
Assert.ok(
|
||||||
|
await client.attachments.cacheImpl.get(RECORD.id),
|
||||||
|
"Record attachment was kept"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_cacheAll_happy_path() {
|
||||||
|
// verify bundle is downloaded succesfully
|
||||||
|
const allSuccess = await downloader.cacheAll();
|
||||||
|
Assert.ok(
|
||||||
|
allSuccess,
|
||||||
|
"Attachments cacheAll succesfully downloaded a bundle and saved all attachments"
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify accuracy of attachments downloaded
|
||||||
|
Assert.equal(
|
||||||
|
downloader.cache["1"].record.title,
|
||||||
|
"test1",
|
||||||
|
"Test record 1 meta content appears accurate."
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
await downloader.cache["1"].blob.text(),
|
||||||
|
"test1\n",
|
||||||
|
"Test file 1 content is accurate."
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
downloader.cache["2"].record.title,
|
||||||
|
"test2",
|
||||||
|
"Test record 2 meta content appears accurate."
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
await downloader.cache["2"].blob.text(),
|
||||||
|
"test2\n",
|
||||||
|
"Test file 2 content is accurate."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_cacheAll_using_real_db() {
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
|
||||||
|
const allSuccess = await client.attachments.cacheAll();
|
||||||
|
|
||||||
|
Assert.ok(
|
||||||
|
allSuccess,
|
||||||
|
"Attachments cacheAll succesfully downloaded a bundle and saved all attachments"
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
(await client.attachments.cacheImpl.get("2")).record.title,
|
||||||
|
"test2",
|
||||||
|
"Test record 2 meta content appears accurate."
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
await (await client.attachments.cacheImpl.get("2")).blob.text(),
|
||||||
|
"test2\n",
|
||||||
|
"Test file 2 content is accurate."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_cacheAll_skips_with_existing_data() {
|
||||||
|
downloader.cache = {
|
||||||
|
1: "1",
|
||||||
|
};
|
||||||
|
const allSuccess = await downloader.cacheAll();
|
||||||
|
Assert.equal(
|
||||||
|
allSuccess,
|
||||||
|
null,
|
||||||
|
"Attachments cacheAll skips downloads if data already exists"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_cacheAll_does_not_skip_if_force_is_true() {
|
||||||
|
downloader.cache = {
|
||||||
|
1: "1",
|
||||||
|
};
|
||||||
|
const allSuccess = await downloader.cacheAll(true);
|
||||||
|
Assert.equal(
|
||||||
|
allSuccess,
|
||||||
|
true,
|
||||||
|
"Attachments cacheAll does not skip downloads if force is true"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_cacheAll_failed_request() {
|
||||||
|
downloader.bucketName = "fake-bucket";
|
||||||
|
downloader.collectionName = "fake-collection";
|
||||||
|
const allSuccess = await downloader.cacheAll();
|
||||||
|
Assert.equal(
|
||||||
|
allSuccess,
|
||||||
|
false,
|
||||||
|
"Attachments cacheAll request failed to download a bundle and returned false"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_cacheAll_failed_unzip() {
|
||||||
|
downloader.bucketName = "error-bucket";
|
||||||
|
downloader.collectionName = "bad-zip";
|
||||||
|
const allSuccess = await downloader.cacheAll();
|
||||||
|
Assert.equal(
|
||||||
|
allSuccess,
|
||||||
|
false,
|
||||||
|
"Attachments cacheAll request failed to extract a bundle and returned false"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_cacheAll_failed_save() {
|
||||||
|
const client = RemoteSettings("some-collection");
|
||||||
|
|
||||||
|
const backup = client.db.saveAttachments;
|
||||||
|
client.db.saveAttachments = () => {
|
||||||
|
throw new Error("boom");
|
||||||
|
};
|
||||||
|
|
||||||
|
const allSuccess = await client.attachments.cacheAll();
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
allSuccess,
|
||||||
|
false,
|
||||||
|
"Attachments cacheAll failed to save entries in DB and returned false"
|
||||||
|
);
|
||||||
|
client.db.saveAttachments = backup;
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEbjCCA1agAwIBAgIQBg3WwdBnkBtUdfz/wp4xNzANBgkqhkiG9w0BAQsFADBa
|
||||||
|
MQswCQYDVQQGEwJJRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJl
|
||||||
|
clRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTE1
|
||||||
|
MTAxNDEyMDAwMFoXDTIwMTAxNDEyMDAwMFowbzELMAkGA1UEBhMCVVMxCzAJBgNV
|
||||||
|
BAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRkwFwYDVQQKExBDbG91ZEZs
|
||||||
|
YXJlLCBJbmMuMSAwHgYDVQQDExdDbG91ZEZsYXJlIEluYyBSU0EgQ0EtMTCCASIw
|
||||||
|
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJGiNOIE4s0M4wdhDeV9aMfAYY9l
|
||||||
|
yG9cfGQqt7a5UgrRA81bi4istCyhzfzRWUW+NAmf6X2HEnA3xLI1M+pH/xEbk9pw
|
||||||
|
jc8/1CPy9jUjBwb89zt5PWh2I1KxZVg/Bnx2yYdVcKTUMKt0GLDXfZXN+RYZHJQo
|
||||||
|
lDlzjH5xV0IpDMv/FsMEZWcfx1JorBf08bRnRVkl9RY00y2ujVr+492ze+zYQ9s7
|
||||||
|
HcidpR+7ret3jzLSvojsaA5+fOaCG0ctVJcLfnkQ5lWR95ByBdO1NapfqZ1+kmCL
|
||||||
|
3baVSeUpYQriBwznxfLuGs8POo4QdviYVtSPBWjOEfb+o1c6Mbo8p4noFzUCAwEA
|
||||||
|
AaOCARkwggEVMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDQG
|
||||||
|
CCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
|
||||||
|
Y29tMDoGA1UdHwQzMDEwL6AtoCuGKWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9P
|
||||||
|
bW5pcm9vdDIwMjUuY3JsMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
|
||||||
|
FhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMB0GA1UdDgQWBBSRBYrfTCLG
|
||||||
|
bYuUTBZFfu5vAvu3wDAfBgNVHSMEGDAWgBTlnVkwgkdYzKz6CFQ2hns6tQRN8DAN
|
||||||
|
BgkqhkiG9w0BAQsFAAOCAQEAVJle3ar9NSnTrLAhgfkcpClIY6/kabDIEa8cOnu1
|
||||||
|
SOXf4vbtZakSmmIbFbmYDUGIU5XwwVdF/FKNzNBRf9G4EL/S0NXytBKj4A34UGQA
|
||||||
|
InaV+DgVLzCifN9cAHi8EFEAfbglUvPvLPFXF0bwffElYm7QBSiHYSZmfOKLCyiv
|
||||||
|
3zlQsf7ozNBAxfbmnRMRSUBcIhRwnaFoFgDs7yU6R1Yk4pO7eMgWpdPGhymDTIvv
|
||||||
|
RnauKStzKsAli9i5hQ4nTDITUpMAmeJoXodgwRkC3Civw32UR2rxObIyxPpbfODb
|
||||||
|
sZKNGO9K5Sjj6turB1zwbd2wI8MhtUCY9tGmSYhe7G6Bkw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
not a zip
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This would be a RS dump.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "filename-of-dump.txt",
|
||||||
|
"attachment": {
|
||||||
|
"filename": "filename-of-dump.txt",
|
||||||
|
"hash": "4c46ef7e4f1951d210fe54c21e07c09bab265fd122580083ed1d6121547a8c6b",
|
||||||
|
"size": 25
|
||||||
|
},
|
||||||
|
"last_modified": 1234567,
|
||||||
|
"some_key": "some metadata"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"fyi": "This .meta.json file describes an attachment, but that attachment is missing.",
|
||||||
|
"attachment": {
|
||||||
|
"filename": "filename-without-content.txt",
|
||||||
|
"hash": "...",
|
||||||
|
"size": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
The filename-without-meta.txt.meta.json file is missing.
|
||||||
1798
src/zen/tests/mochitests/remote-settings/test_remote_settings.js
Normal file
1798
src/zen/tests/mochitests/remote-settings/test_remote_settings.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
async function getLocalDumpLastModified(bucket, collection) {
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await fetch(
|
||||||
|
`resource://app/defaults/settings/${bucket}/${collection}.json`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const { timestamp } = await res.json();
|
||||||
|
Assert.greaterOrEqual(
|
||||||
|
timestamp,
|
||||||
|
0,
|
||||||
|
`${bucket}/${collection} dump has timestamp`
|
||||||
|
);
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function lastModified_of_non_existing_dump() {
|
||||||
|
ok(!Utils._dumpStats, "_dumpStats not initialized");
|
||||||
|
equal(
|
||||||
|
await Utils.getLocalDumpLastModified("did not", "exist"),
|
||||||
|
-1,
|
||||||
|
"A non-existent dump has value -1"
|
||||||
|
);
|
||||||
|
ok(Utils._dumpStats, "_dumpStats was initialized");
|
||||||
|
|
||||||
|
ok("did not/exist" in Utils._dumpStats, "cached non-existing dump result");
|
||||||
|
delete Utils._dumpStats["did not/exist"];
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function lastModified_summary_is_correct() {
|
||||||
|
ok(!!Object.keys(Utils._dumpStats).length, "Contains summary of dumps");
|
||||||
|
|
||||||
|
let checked = 0;
|
||||||
|
for (let [identifier, lastModified] of Object.entries(Utils._dumpStats)) {
|
||||||
|
let [bucket, collection] = identifier.split("/");
|
||||||
|
let actual = await getLocalDumpLastModified(bucket, collection);
|
||||||
|
if (actual < 0) {
|
||||||
|
info(`${identifier} has no dump, skip.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info(`Checking correctness of ${identifier}`);
|
||||||
|
equal(
|
||||||
|
await Utils.getLocalDumpLastModified(bucket, collection),
|
||||||
|
lastModified,
|
||||||
|
`Expected last_modified value for ${identifier}`
|
||||||
|
);
|
||||||
|
equal(lastModified, actual, `last_modified should match collection`);
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
Assert.greater(checked, 0, "At least one dump was packaged and checked.");
|
||||||
|
});
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
let client;
|
||||||
|
|
||||||
|
async function createRecords(records) {
|
||||||
|
await client.db.importChanges(
|
||||||
|
{},
|
||||||
|
42,
|
||||||
|
records.map((record, i) => ({
|
||||||
|
id: `record-${i}`,
|
||||||
|
...record,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_setup(() => {
|
||||||
|
client = RemoteSettings("some-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_returns_all_without_target() {
|
||||||
|
await createRecords([
|
||||||
|
{
|
||||||
|
passwordSelector: "#pass-signin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter_expression: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter_expression: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_filters_can_be_disabled() {
|
||||||
|
const c = RemoteSettings("no-jexl", { filterCreator: null });
|
||||||
|
await c.db.importChanges({}, 42, [
|
||||||
|
{
|
||||||
|
id: "abc",
|
||||||
|
filter_expression: "1 == 2",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await c.get();
|
||||||
|
equal(list.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_returns_entries_where_jexl_is_true() {
|
||||||
|
await createRecords([
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: "[42]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: "1 == 2 || 1 == 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: 'env.appinfo.ID == "xpcshell@tests.mozilla.org"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: false,
|
||||||
|
filter_expression: "env.version == undefined",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: "env.unknown == undefined",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: false,
|
||||||
|
filter_expression: "1 == 2",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 5);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ignores_entries_where_jexl_is_invalid() {
|
||||||
|
await createRecords([
|
||||||
|
{
|
||||||
|
filter_expression: "true === true", // JavaScript Error: "Invalid expression token: ="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter_expression: "Objects.keys({}) == []", // Token ( (openParen) unexpected in expression
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_date_filters() {
|
||||||
|
await createRecords([
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: '"1982-05-08"|date < "2016-03-22"|date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: false,
|
||||||
|
filter_expression: '"2000-01-01"|date < "1970-01-01"|date',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 1);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_preferences_filters() {
|
||||||
|
await createRecords([
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: '"services.settings.last_etag"|preferenceValue == 42',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression:
|
||||||
|
'"services.settings.poll_interval"|preferenceExists == true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression:
|
||||||
|
'"services.settings.poll_interval"|preferenceIsUserSet == false',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression:
|
||||||
|
'"services.settings.last_etag"|preferenceIsUserSet == true',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set a pref for the user.
|
||||||
|
Services.prefs.setIntPref("services.settings.last_etag", 42);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 4);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_intersect_operator() {
|
||||||
|
await createRecords([
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: '{foo: 1, bar: 2}|keys intersect ["foo"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: '(["a", "b"] intersect ["a", 1, 4]) == "a"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: false,
|
||||||
|
filter_expression: '(["a", "b"] intersect [3, 1, 4]) == "c"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: `
|
||||||
|
[1, 2, 3]
|
||||||
|
intersect
|
||||||
|
[3, 4, 5]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 3);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_support_of_samples() {
|
||||||
|
await createRecords([
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: '"always-true"|stableSample(1)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: false,
|
||||||
|
filter_expression: '"always-false"|stableSample(0)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: '"turns-to-true-0"|stableSample(0.5)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: false,
|
||||||
|
filter_expression: '"turns-to-false-1"|stableSample(0.5)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: true,
|
||||||
|
filter_expression: '"turns-to-true-0"|bucketSample(0, 50, 100)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
willMatch: false,
|
||||||
|
filter_expression: '"turns-to-false-1"|bucketSample(0, 50, 100)',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const list = await client.get();
|
||||||
|
equal(list.length, 3);
|
||||||
|
ok(list.every(e => e.willMatch));
|
||||||
|
});
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// A collection with a dump that's packaged on all builds where this test runs,
|
||||||
|
// including on Android at mobile/android/installer/package-manifest.in
|
||||||
|
const TEST_BUCKET = "main";
|
||||||
|
const TEST_COLLECTION = "password-recipes";
|
||||||
|
|
||||||
|
let client;
|
||||||
|
let DUMP_RECORDS;
|
||||||
|
let DUMP_LAST_MODIFIED;
|
||||||
|
|
||||||
|
add_setup(async () => {
|
||||||
|
// "services.settings.server" pref is not set.
|
||||||
|
// Test defaults to an unreachable server,
|
||||||
|
// and will only load from the dump if any.
|
||||||
|
|
||||||
|
client = new RemoteSettingsClient(TEST_COLLECTION, {
|
||||||
|
bucketName: TEST_BUCKET,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dump = await SharedUtils.loadJSONDump(TEST_BUCKET, TEST_COLLECTION);
|
||||||
|
DUMP_RECORDS = dump.data;
|
||||||
|
DUMP_LAST_MODIFIED = dump.timestamp;
|
||||||
|
|
||||||
|
// Dumps are fetched via the following, which sorts the records, newest first.
|
||||||
|
// https://searchfox.org/mozilla-central/rev/5b3444ad300e244b5af4214212e22bd9e4b7088a/taskcluster/docker/periodic-updates/scripts/periodic_file_updates.sh#304
|
||||||
|
equal(
|
||||||
|
DUMP_LAST_MODIFIED,
|
||||||
|
DUMP_RECORDS[0].last_modified,
|
||||||
|
"records in dump ought to be sorted by last_modified"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function importData(records) {
|
||||||
|
await RemoteSettingsWorker._execute("_test_only_import", [
|
||||||
|
TEST_BUCKET,
|
||||||
|
TEST_COLLECTION,
|
||||||
|
records,
|
||||||
|
records[0]?.last_modified || 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
await client.db.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_load_from_dump_when_offline() {
|
||||||
|
// Baseline: verify that the collection is empty at first,
|
||||||
|
// but non-empty after loading from the dump.
|
||||||
|
const beforeTimestamp = await client.db.getLastModified();
|
||||||
|
equal(beforeTimestamp, null, "collection empty when offline");
|
||||||
|
|
||||||
|
// should import from dump since collection was not initialized.
|
||||||
|
const after = await client.get();
|
||||||
|
equal(after.length, DUMP_RECORDS.length, "collection loaded from dump");
|
||||||
|
equal(await client.getLastModified(), DUMP_LAST_MODIFIED, "dump's timestamp");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_optional_skip_dump_after_empty_import() {
|
||||||
|
// clear_state should have wiped the database.
|
||||||
|
const beforeTimestamp = await client.db.getLastModified();
|
||||||
|
equal(beforeTimestamp, null, "collection empty after clearing");
|
||||||
|
|
||||||
|
// Verify that the dump is not imported again by client.get()
|
||||||
|
// when the database is initialized with an empty dump
|
||||||
|
// with `loadDumpIfNewer` disabled.
|
||||||
|
await importData([]); // <-- Empty set of records.
|
||||||
|
|
||||||
|
const after = await client.get({ loadDumpIfNewer: false });
|
||||||
|
equal(after.length, 0, "collection still empty due to import");
|
||||||
|
equal(await client.getLastModified(), 0, "Empty dump has no timestamp");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_optional_skip_dump_after_non_empty_import() {
|
||||||
|
await importData([{ last_modified: 1234, id: "dummy" }]);
|
||||||
|
|
||||||
|
const after = await client.get({ loadDumpIfNewer: false });
|
||||||
|
equal(after.length, 1, "Imported dummy data");
|
||||||
|
equal(await client.getLastModified(), 1234, "Expected timestamp of import");
|
||||||
|
|
||||||
|
await importData([]);
|
||||||
|
const after2 = await client.get({ loadDumpIfNewer: false });
|
||||||
|
equal(after2.length, 0, "Previous data wiped on duplicate import");
|
||||||
|
equal(await client.getLastModified(), 0, "Timestamp of empty collection");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_load_dump_after_empty_import() {
|
||||||
|
await importData([]); // <-- Empty set of records, i.e. last_modified = 0.
|
||||||
|
|
||||||
|
const after = await client.get();
|
||||||
|
equal(after.length, DUMP_RECORDS.length, "Imported dump");
|
||||||
|
equal(await client.getLastModified(), DUMP_LAST_MODIFIED, "dump's timestamp");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_load_dump_after_non_empty_import() {
|
||||||
|
// Dump is updated regularly, verify that the dump matches our expectations
|
||||||
|
// before running the test.
|
||||||
|
Assert.greater(
|
||||||
|
DUMP_LAST_MODIFIED,
|
||||||
|
1234,
|
||||||
|
"Assuming dump to be newer than dummy 1234"
|
||||||
|
);
|
||||||
|
|
||||||
|
await importData([{ last_modified: 1234, id: "dummy" }]);
|
||||||
|
|
||||||
|
const after = await client.get();
|
||||||
|
equal(after.length, DUMP_RECORDS.length, "Imported dump");
|
||||||
|
equal(await client.getLastModified(), DUMP_LAST_MODIFIED, "dump's timestamp");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_load_dump_after_import_from_broken_distro() {
|
||||||
|
// Dump is updated regularly, verify that the dump matches our expectations
|
||||||
|
// before running the test.
|
||||||
|
Assert.greater(
|
||||||
|
DUMP_LAST_MODIFIED,
|
||||||
|
1234,
|
||||||
|
"Assuming dump to be newer than dummy 1234"
|
||||||
|
);
|
||||||
|
|
||||||
|
// No last_modified time.
|
||||||
|
await importData([{ id: "dummy" }]);
|
||||||
|
|
||||||
|
const after = await client.get();
|
||||||
|
equal(after.length, DUMP_RECORDS.length, "Imported dump");
|
||||||
|
equal(await client.getLastModified(), DUMP_LAST_MODIFIED, "dump's timestamp");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_skip_dump_if_same_last_modified() {
|
||||||
|
await importData([{ last_modified: DUMP_LAST_MODIFIED, id: "dummy" }]);
|
||||||
|
|
||||||
|
const after = await client.get();
|
||||||
|
equal(after.length, 1, "Not importing dump when time matches");
|
||||||
|
equal(await client.getLastModified(), DUMP_LAST_MODIFIED, "Same timestamp");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let client;
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
await client.db.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setJSONResponse(response, changeset) {
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setHeader("Date", new Date().toUTCString());
|
||||||
|
response.write(JSON.stringify(changeset));
|
||||||
|
}
|
||||||
|
|
||||||
|
add_setup(() => {
|
||||||
|
Services.prefs.setStringPref("services.settings.loglevel", "debug");
|
||||||
|
|
||||||
|
// Set up an HTTP Server
|
||||||
|
server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
PREF_SETTINGS_SERVER,
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
|
||||||
|
client = RemoteSettings("some-cid");
|
||||||
|
|
||||||
|
const bodies = {
|
||||||
|
// First successful sync at timestamp=333
|
||||||
|
"/v1/buckets/main/collections/some-cid/changeset?_expected=333": {
|
||||||
|
timestamp: 333,
|
||||||
|
metadata: {
|
||||||
|
signatures: [{ signature: "abc", x5u: "data:text/plain;base64,pem" }],
|
||||||
|
},
|
||||||
|
changes: [
|
||||||
|
{ id: "rid2", last_modified: 22 },
|
||||||
|
{ id: "rid1", last_modified: 11 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Client fetches with _expected=222&_since=333
|
||||||
|
"/v1/buckets/main/collections/some-cid/changeset?_expected=222&_since=333":
|
||||||
|
{
|
||||||
|
timestamp: 222,
|
||||||
|
metadata: {
|
||||||
|
signatures: [{ signature: "ghi", x5u: "data:text/plain;base64,pem" }],
|
||||||
|
},
|
||||||
|
changes: [{ id: "rid1", last_modified: 11 }],
|
||||||
|
},
|
||||||
|
// Client refetches full collection with _expected=222 after signature fails.
|
||||||
|
"/v1/buckets/main/collections/some-cid/changeset?_expected=222": {
|
||||||
|
timestamp: 222,
|
||||||
|
metadata: {
|
||||||
|
signatures: [{ signature: "ghi", x5u: "data:text/plain;base64,pem" }],
|
||||||
|
},
|
||||||
|
changes: [{ id: "rid1", last_modified: 11 }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const handler = (request, response) => {
|
||||||
|
const body = bodies[`${request.path}?${request.queryString}`];
|
||||||
|
if (!body) {
|
||||||
|
const err = new Error(
|
||||||
|
`Unexpected request ${request.path}?${request.queryString}`
|
||||||
|
);
|
||||||
|
console.error(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
setJSONResponse(response, body);
|
||||||
|
};
|
||||||
|
Object.keys(bodies).map(path =>
|
||||||
|
server.registerPathHandler(path.split("?")[0], handler)
|
||||||
|
);
|
||||||
|
|
||||||
|
// In this test suite, the only valid data is the one at timestamp=222 or timestamp=333, but not a mix of the two.
|
||||||
|
let backup = client._verifier;
|
||||||
|
client._verifier = {
|
||||||
|
asyncVerifyContentSignature: serialized => {
|
||||||
|
return (
|
||||||
|
serialized ==
|
||||||
|
'{"data":[{"id":"rid2","last_modified":22}],"last_modified":"444"}' ||
|
||||||
|
serialized ==
|
||||||
|
'{"data":[{"id":"rid1","last_modified":11},{"id":"rid2","last_modified":22}],"last_modified":"333"}' ||
|
||||||
|
serialized ==
|
||||||
|
'{"data":[{"id":"rid1","last_modified":11}],"last_modified":"222"}'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
client._verifier = backup;
|
||||||
|
server.stop(() => {});
|
||||||
|
Services.prefs.clearUserPref(PREF_SETTINGS_SERVER);
|
||||||
|
|
||||||
|
Services.prefs.clearUserPref("services.settings.loglevel");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_ignores_if_local_signature_is_valid() {
|
||||||
|
await client.maybeSync(333);
|
||||||
|
Assert.deepEqual(
|
||||||
|
[
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
(await client.get()).map(({ id }) => id),
|
||||||
|
],
|
||||||
|
[333, ["rid1", "rid2"]]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.maybeSync(222);
|
||||||
|
|
||||||
|
// The client should detect that timestamp is older,
|
||||||
|
// but it ignores it because local data is good.
|
||||||
|
Assert.deepEqual(
|
||||||
|
[
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
(await client.get()).map(({ id }) => id),
|
||||||
|
],
|
||||||
|
[333, ["rid1", "rid2"]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_uses_old_data_if_local_signature_is_invalid() {
|
||||||
|
// Import some data in the local DB. Signature will be bad.
|
||||||
|
await client.db.importChanges({}, 333, [{ id: "myid", last_modified: 1234 }]);
|
||||||
|
Assert.deepEqual(
|
||||||
|
[
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
(await client.get()).map(({ id }) => id),
|
||||||
|
],
|
||||||
|
[333, ["myid"]]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.maybeSync(222);
|
||||||
|
|
||||||
|
// The client should detect that timestamp is older,
|
||||||
|
// but will replace local data with this old data.
|
||||||
|
Assert.deepEqual(
|
||||||
|
[
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
(await client.get()).map(({ id }) => id),
|
||||||
|
],
|
||||||
|
[222, ["rid1"]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
|||||||
|
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||||
|
const CHANGES_PATH = "/v1" + Utils.CHANGES_PATH;
|
||||||
|
const BROKEN_SYNC_THRESHOLD = 10; // See default pref value
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let client;
|
||||||
|
let maybeSyncBackup;
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
// Disable logging output.
|
||||||
|
Services.prefs.setStringPref("services.settings.loglevel", "critical");
|
||||||
|
// Pull data from the test server.
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
PREF_SETTINGS_SERVER,
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear sync history.
|
||||||
|
await new SyncHistory("").clear();
|
||||||
|
|
||||||
|
// Simulate a response whose ETag gets incremented on each call
|
||||||
|
// (in order to generate several history entries, indexed by timestamp).
|
||||||
|
let timestamp = 1337;
|
||||||
|
server.registerPathHandler(CHANGES_PATH, (request, response) => {
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setHeader("Date", new Date(1000000).toUTCString());
|
||||||
|
response.setHeader("ETag", `"${timestamp}"`);
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
last_modified: ++timestamp,
|
||||||
|
bucket: "main",
|
||||||
|
collection: "desktop-manager",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore original maybeSync() method between each test.
|
||||||
|
client.maybeSync = maybeSyncBackup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_test() {
|
||||||
|
// Set up an HTTP Server
|
||||||
|
server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
|
||||||
|
client = RemoteSettings("desktop-manager");
|
||||||
|
maybeSyncBackup = client.maybeSync;
|
||||||
|
|
||||||
|
run_next_test();
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
server.stop(() => {});
|
||||||
|
// Restore original maybeSync() method when test suite is done.
|
||||||
|
client.maybeSync = maybeSyncBackup;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_db_is_destroyed_when_sync_is_broken() {
|
||||||
|
// Simulate a successful sync.
|
||||||
|
client.maybeSync = async () => {
|
||||||
|
// Store some data in local DB.
|
||||||
|
await client.db.importChanges({}, 1515, []);
|
||||||
|
};
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
|
||||||
|
// Register a client with a failing sync method.
|
||||||
|
client.maybeSync = () => {
|
||||||
|
throw new RemoteSettingsClient.InvalidSignatureError(
|
||||||
|
"main/desktop-manager"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now obtain several failures in a row.
|
||||||
|
for (var i = 0; i < BROKEN_SYNC_THRESHOLD; i++) {
|
||||||
|
try {
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronization is in broken state.
|
||||||
|
Assert.equal(
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
1515,
|
||||||
|
"Local DB was not destroyed yet"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Synchronize again. Broken state will be detected.
|
||||||
|
try {
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// DB was destroyed.
|
||||||
|
Assert.equal(
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
null,
|
||||||
|
"Local DB was destroyed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_db_is_not_destroyed_when_state_is_server_error() {
|
||||||
|
// Since we don't mock the server endpoints to obtain the changeset of this
|
||||||
|
// collection, the call to `maybeSync()` will fail with network errors.
|
||||||
|
|
||||||
|
// Store some data in local DB.
|
||||||
|
await client.db.importChanges({}, 1515, []);
|
||||||
|
|
||||||
|
// Now obtain several failures in a row.
|
||||||
|
let lastError;
|
||||||
|
for (var i = 0; i < BROKEN_SYNC_THRESHOLD + 1; i++) {
|
||||||
|
try {
|
||||||
|
await RemoteSettings.pollChanges({ trigger: "timer" });
|
||||||
|
} catch (e) {
|
||||||
|
lastError = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.ok(
|
||||||
|
/Cannot parse server content/.test(lastError.message),
|
||||||
|
"Error is about server"
|
||||||
|
);
|
||||||
|
// DB was not destroyed.
|
||||||
|
Assert.equal(
|
||||||
|
await client.db.getLastModified(),
|
||||||
|
1515,
|
||||||
|
"Local DB was not destroyed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
var nextUniqId = 0;
|
||||||
|
function getNewUtils() {
|
||||||
|
const { Utils } = ChromeUtils.importESModule(
|
||||||
|
`resource://services-settings/Utils.sys.mjs?_${++nextUniqId}`
|
||||||
|
);
|
||||||
|
return Utils;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A collection with a dump that's packaged on all builds where this test runs,
|
||||||
|
// including on Android at mobile/android/installer/package-manifest.in
|
||||||
|
const TEST_BUCKET = "main";
|
||||||
|
const TEST_COLLECTION = "password-recipes";
|
||||||
|
|
||||||
|
async function importData(records) {
|
||||||
|
await RemoteSettingsWorker._execute("_test_only_import", [
|
||||||
|
TEST_BUCKET,
|
||||||
|
TEST_COLLECTION,
|
||||||
|
records,
|
||||||
|
records[0]?.last_modified || 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
Services.env.set("MOZ_REMOTE_SETTINGS_DEVTOOLS", "0");
|
||||||
|
Services.prefs.clearUserPref("services.settings.server");
|
||||||
|
Services.prefs.clearUserPref("services.settings.preview_enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
add_setup(async function () {
|
||||||
|
// Set this env vars in order to test the code path where the
|
||||||
|
// server URL can only be overridden from Dev Tools.
|
||||||
|
// See `isRunningTests` in `services/settings/Utils.sys.mjs`.
|
||||||
|
const before = Services.env.get("MOZ_DISABLE_NONLOCAL_CONNECTIONS");
|
||||||
|
Services.env.set("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "0");
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
clear_state();
|
||||||
|
Services.env.set("MOZ_DISABLE_NONLOCAL_CONNECTIONS", before);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
{
|
||||||
|
skip_if: () => !AppConstants.RELEASE_OR_BETA,
|
||||||
|
},
|
||||||
|
async function test_server_url_cannot_be_toggled_in_release() {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
"http://localhost:8888/v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
Utils.SERVER_URL,
|
||||||
|
AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
|
||||||
|
"Server url pref was not read in release"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
{
|
||||||
|
skip_if: () => AppConstants.RELEASE_OR_BETA,
|
||||||
|
},
|
||||||
|
async function test_server_url_cannot_be_toggled_in_dev_nightly() {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
"http://localhost:8888/v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.notEqual(
|
||||||
|
Utils.SERVER_URL,
|
||||||
|
AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
|
||||||
|
"Server url pref was read in nightly/dev"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
{
|
||||||
|
skip_if: () => !AppConstants.RELEASE_OR_BETA,
|
||||||
|
},
|
||||||
|
async function test_preview_mode_cannot_be_toggled_in_release() {
|
||||||
|
Services.prefs.setBoolPref("services.settings.preview_enabled", true);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.ok(!Utils.PREVIEW_MODE, "Preview mode pref was not read in release");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
{
|
||||||
|
skip_if: () => AppConstants.RELEASE_OR_BETA,
|
||||||
|
},
|
||||||
|
async function test_preview_mode_cannot_be_toggled_in_dev_nightly() {
|
||||||
|
Services.prefs.setBoolPref("services.settings.preview_enabled", true);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.ok(Utils.PREVIEW_MODE, "Preview mode pref is read in dev/nightly");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
{
|
||||||
|
skip_if: () => !AppConstants.RELEASE_OR_BETA,
|
||||||
|
},
|
||||||
|
async function test_load_dumps_will_always_be_loaded_in_release() {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
"http://localhost:8888/v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
Utils.SERVER_URL,
|
||||||
|
AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
|
||||||
|
"Server url pref was not read"
|
||||||
|
);
|
||||||
|
Assert.ok(Utils.LOAD_DUMPS, "Dumps will always be loaded");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
{
|
||||||
|
skip_if: () => AppConstants.RELEASE_OR_BETA,
|
||||||
|
},
|
||||||
|
async function test_load_dumps_can_be_disabled_in_dev_nightly() {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
"http://localhost:8888/v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.notEqual(
|
||||||
|
Utils.SERVER_URL,
|
||||||
|
AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
|
||||||
|
"Server url pref was read"
|
||||||
|
);
|
||||||
|
Assert.ok(!Utils.LOAD_DUMPS, "Dumps are not loaded if server is not prod");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_server_url_can_be_changed_in_all_versions_if_running_for_devtools() {
|
||||||
|
Services.env.set("MOZ_REMOTE_SETTINGS_DEVTOOLS", "1");
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
"http://localhost:8888/v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.notEqual(
|
||||||
|
Utils.SERVER_URL,
|
||||||
|
AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
|
||||||
|
"Server url pref was read"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_preview_mode_can_be_changed_in_all_versions_if_running_for_devtools() {
|
||||||
|
Services.env.set("MOZ_REMOTE_SETTINGS_DEVTOOLS", "1");
|
||||||
|
Services.prefs.setBoolPref("services.settings.preview_enabled", true);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.ok(Utils.PREVIEW_MODE, "Preview mode pref was read");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_dumps_are_not_loaded_if_server_is_not_prod_if_running_for_devtools() {
|
||||||
|
Services.env.set("MOZ_REMOTE_SETTINGS_DEVTOOLS", "1");
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
"http://localhost:8888/v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
Assert.ok(!Utils.LOAD_DUMPS, "Dumps won't be loaded");
|
||||||
|
|
||||||
|
// The section below ensures that the LOAD_DUMPS flag properly takes effect.
|
||||||
|
// The client is set up here rather than add_setup to avoid triggering the
|
||||||
|
// lazy getters that are behind the global Utils.LOAD_DUMPS. If they are
|
||||||
|
// triggered too early, then they will potentially cache different values
|
||||||
|
// for the server urls and environment variables and this test then won't be
|
||||||
|
// testing what we expect it to.
|
||||||
|
|
||||||
|
let client = new RemoteSettingsClient(TEST_COLLECTION);
|
||||||
|
|
||||||
|
const dump = await SharedUtils.loadJSONDump(TEST_BUCKET, TEST_COLLECTION);
|
||||||
|
let DUMP_LAST_MODIFIED = dump.timestamp;
|
||||||
|
|
||||||
|
// Dump is updated regularly, verify that the dump matches our expectations
|
||||||
|
// before running the test.
|
||||||
|
Assert.greater(
|
||||||
|
DUMP_LAST_MODIFIED,
|
||||||
|
1234,
|
||||||
|
"Assuming dump to be newer than dummy 1234"
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.db.clear();
|
||||||
|
await importData([{ last_modified: 1234, id: "dummy" }]);
|
||||||
|
|
||||||
|
const after = await client.get();
|
||||||
|
Assert.deepEqual(
|
||||||
|
after,
|
||||||
|
[{ last_modified: 1234, id: "dummy" }],
|
||||||
|
"Should have kept the original import"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
await client.getLastModified(),
|
||||||
|
1234,
|
||||||
|
"Should have kept the import's timestamp"
|
||||||
|
);
|
||||||
|
await client.db.clear();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_dumps_are_loaded_if_server_is_prod_if_running_for_devtools() {
|
||||||
|
Services.env.set("MOZ_REMOTE_SETTINGS_DEVTOOLS", "1");
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
AppConstants.REMOTE_SETTINGS_SERVER_URLS[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const Utils = getNewUtils();
|
||||||
|
|
||||||
|
Assert.ok(Utils.LOAD_DUMPS, "dumps are loaded if prod");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
@@ -0,0 +1,952 @@
|
|||||||
|
/* import-globals-from ../../../common/tests/unit/head_helpers.js */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||||
|
const SIGNER_NAME = "onecrl.content-signature.mozilla.org";
|
||||||
|
|
||||||
|
const CERT_DIR = "test_remote_settings_signatures/";
|
||||||
|
const CHAIN_FILES = ["collection_signing_ee.pem", "collection_signing_int.pem"];
|
||||||
|
|
||||||
|
function getFileData(file) {
|
||||||
|
const stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
|
||||||
|
Ci.nsIFileInputStream
|
||||||
|
);
|
||||||
|
stream.init(file, -1, 0, 0);
|
||||||
|
const data = NetUtil.readInputStreamToString(stream, stream.available());
|
||||||
|
stream.close();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCertChain() {
|
||||||
|
const chain = [];
|
||||||
|
for (let file of CHAIN_FILES) {
|
||||||
|
chain.push(getFileData(do_get_file(CERT_DIR + file)));
|
||||||
|
}
|
||||||
|
return chain.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let client;
|
||||||
|
|
||||||
|
add_setup(() => {
|
||||||
|
// Signature verification is enabled by default. We use a custom signer
|
||||||
|
// because these tests were originally written for OneCRL.
|
||||||
|
client = RemoteSettings("signed", { signerName: SIGNER_NAME });
|
||||||
|
|
||||||
|
Services.prefs.setStringPref("services.settings.loglevel", "debug");
|
||||||
|
|
||||||
|
// Set up an HTTP Server
|
||||||
|
server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
Services.prefs.clearUserPref("services.settings.loglevel");
|
||||||
|
Services.prefs.clearUserPref(PREF_SETTINGS_SERVER);
|
||||||
|
server.stop(() => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_check_signatures() {
|
||||||
|
// First, perform a signature verification with known data and signature
|
||||||
|
// to ensure things are working correctly
|
||||||
|
let verifier = Cc[
|
||||||
|
"@mozilla.org/security/contentsignatureverifier;1"
|
||||||
|
].createInstance(Ci.nsIContentSignatureVerifier);
|
||||||
|
|
||||||
|
const emptyData = "[]";
|
||||||
|
const emptySignature =
|
||||||
|
"p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
|
||||||
|
|
||||||
|
ok(
|
||||||
|
await verifier.asyncVerifyContentSignature(
|
||||||
|
emptyData,
|
||||||
|
emptySignature,
|
||||||
|
getCertChain(),
|
||||||
|
SIGNER_NAME,
|
||||||
|
Ci.nsIX509CertDB.AppXPCShellRoot
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionData =
|
||||||
|
'[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]';
|
||||||
|
const collectionSignature =
|
||||||
|
"p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
|
||||||
|
|
||||||
|
ok(
|
||||||
|
await verifier.asyncVerifyContentSignature(
|
||||||
|
collectionData,
|
||||||
|
collectionSignature,
|
||||||
|
getCertChain(),
|
||||||
|
SIGNER_NAME,
|
||||||
|
Ci.nsIX509CertDB.AppXPCShellRoot
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_bad_signature_does_not_lead_to_empty_list() {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
PREF_SETTINGS_SERVER,
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
const x5u = `http://localhost:${server.identity.primaryPort}/x5u.pem`;
|
||||||
|
|
||||||
|
const networkCalls = [];
|
||||||
|
|
||||||
|
server.registerPathHandler(
|
||||||
|
"/v1/buckets/monitor/collections/changes/changeset",
|
||||||
|
(request, response) => {
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
bucket: "main",
|
||||||
|
collection: "no-dump-no-local-data",
|
||||||
|
last_modified: 42,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
server.registerPathHandler(
|
||||||
|
"/v1/buckets/monitor/collections/changes/changeset",
|
||||||
|
(request, response) => {
|
||||||
|
networkCalls.push(request);
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
bucket: "main",
|
||||||
|
collection: "no-dump-no-local-data",
|
||||||
|
last_modified: 42,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
server.registerPathHandler(
|
||||||
|
"/v1/buckets/main/collections/no-dump-no-local-data/changeset",
|
||||||
|
(request, response) => {
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: 42,
|
||||||
|
changes: [],
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
signature: "bad-signature",
|
||||||
|
x5u,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
server.registerPathHandler("/x5u.pem", (request, response) => {
|
||||||
|
response.write(getCertChain()); // At least cert will be valid.
|
||||||
|
response.setHeader("Content-Type", "text/plain; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientEmpty = RemoteSettings("no-dump-no-local-data");
|
||||||
|
clientEmpty.verifySignature = true; // default
|
||||||
|
|
||||||
|
// Check that client.get() will initiate a sync,
|
||||||
|
// and that it will throw since the signature is bad,
|
||||||
|
// and not return an empty list (`emptyListFallback: false`)
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await clientEmpty.get({
|
||||||
|
emptyListFallback: false,
|
||||||
|
syncIfEmpty: true, // default value
|
||||||
|
});
|
||||||
|
} catch (exc) {
|
||||||
|
error = exc;
|
||||||
|
}
|
||||||
|
equal(error.name, "InvalidSignatureError");
|
||||||
|
|
||||||
|
// Even running client.sync() will throw and won't leave
|
||||||
|
// anything in the database.
|
||||||
|
error = null;
|
||||||
|
try {
|
||||||
|
await clientEmpty.sync();
|
||||||
|
} catch (exc) {
|
||||||
|
error = exc;
|
||||||
|
}
|
||||||
|
equal(error.name, "InvalidSignatureError");
|
||||||
|
equal(await clientEmpty.db.getLastModified(), null);
|
||||||
|
|
||||||
|
// Call .get() again will initiate another sync.
|
||||||
|
networkCalls.length = 0;
|
||||||
|
try {
|
||||||
|
await clientEmpty.get({
|
||||||
|
emptyListFallback: false,
|
||||||
|
syncIfEmpty: true, // default value
|
||||||
|
});
|
||||||
|
} catch (exc) {
|
||||||
|
error = exc;
|
||||||
|
}
|
||||||
|
Assert.greater(networkCalls.length, 0, "Network calls were made");
|
||||||
|
equal(error.name, "InvalidSignatureError");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_check_synchronization_with_signatures() {
|
||||||
|
const port = server.identity.primaryPort;
|
||||||
|
|
||||||
|
const x5u = `http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem`;
|
||||||
|
|
||||||
|
// Telemetry reports.
|
||||||
|
const TELEMETRY_SOURCE = client.identifier;
|
||||||
|
|
||||||
|
function registerHandlers(responses) {
|
||||||
|
function handleResponse(serverTimeMillis, request, response) {
|
||||||
|
const key = `${request.method}:${request.path}?${request.queryString}`;
|
||||||
|
const available = responses[key];
|
||||||
|
const sampled = available.length > 1 ? available.shift() : available[0];
|
||||||
|
if (!sampled) {
|
||||||
|
do_throw(
|
||||||
|
`unexpected ${request.method} request for ${request.path}?${request.queryString}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setStatusLine(
|
||||||
|
null,
|
||||||
|
sampled.status.status,
|
||||||
|
sampled.status.statusText
|
||||||
|
);
|
||||||
|
// send the headers
|
||||||
|
for (let headerLine of sampled.sampleHeaders) {
|
||||||
|
let headerElements = headerLine.split(":");
|
||||||
|
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the server date
|
||||||
|
response.setHeader("Date", new Date(serverTimeMillis).toUTCString());
|
||||||
|
|
||||||
|
response.write(sampled.responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key of Object.keys(responses)) {
|
||||||
|
const keyParts = key.split(":");
|
||||||
|
const valueParts = keyParts[1].split("?");
|
||||||
|
const path = valueParts[0];
|
||||||
|
|
||||||
|
server.registerPathHandler(path, handleResponse.bind(null, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up prefs so the kinto updater talks to the test server
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
PREF_SETTINGS_SERVER,
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
|
||||||
|
// These are records we'll use in the test collections
|
||||||
|
const RECORD1 = {
|
||||||
|
details: {
|
||||||
|
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
||||||
|
created: "2016-01-18T14:43:37Z",
|
||||||
|
name: "GlobalSign certs",
|
||||||
|
who: ".",
|
||||||
|
why: ".",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
|
||||||
|
issuerName:
|
||||||
|
"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==",
|
||||||
|
last_modified: 2000,
|
||||||
|
serialNumber: "BAAAAAABA/A35EU=",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RECORD2 = {
|
||||||
|
details: {
|
||||||
|
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
||||||
|
created: "2016-01-18T14:48:11Z",
|
||||||
|
name: "GlobalSign certs",
|
||||||
|
who: ".",
|
||||||
|
why: ".",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
id: "e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc",
|
||||||
|
issuerName:
|
||||||
|
"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
|
||||||
|
last_modified: 3000,
|
||||||
|
serialNumber: "BAAAAAABI54PryQ=",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RECORD3 = {
|
||||||
|
details: {
|
||||||
|
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
||||||
|
created: "2016-01-18T14:48:11Z",
|
||||||
|
name: "GlobalSign certs",
|
||||||
|
who: ".",
|
||||||
|
why: ".",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
id: "c7c49b69-a4ab-418e-92a9-e1961459aa7f",
|
||||||
|
issuerName:
|
||||||
|
"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
|
||||||
|
last_modified: 4000,
|
||||||
|
serialNumber: "BAAAAAABI54PryQ=",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RECORD1_DELETION = {
|
||||||
|
deleted: true,
|
||||||
|
enabled: true,
|
||||||
|
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
|
||||||
|
last_modified: 3500,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check that a signature on an empty collection is OK
|
||||||
|
// We need to set up paths on the HTTP server to return specific data from
|
||||||
|
// specific paths for each test. Here we prepare data for each response.
|
||||||
|
|
||||||
|
// A cert chain response (this the cert chain that contains the signing
|
||||||
|
// cert, the root and any intermediates in between). This is used in each
|
||||||
|
// sync.
|
||||||
|
const RESPONSE_CERT_CHAIN = {
|
||||||
|
comment: "RESPONSE_CERT_CHAIN",
|
||||||
|
sampleHeaders: ["Content-Type: text/plain; charset=UTF-8"],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: getCertChain(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// A server settings response. This is used in each sync.
|
||||||
|
const RESPONSE_SERVER_SETTINGS = {
|
||||||
|
comment: "RESPONSE_SERVER_SETTINGS",
|
||||||
|
sampleHeaders: [
|
||||||
|
"Access-Control-Allow-Origin: *",
|
||||||
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
"Server: waitress",
|
||||||
|
],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
settings: {
|
||||||
|
batch_max_requests: 25,
|
||||||
|
},
|
||||||
|
url: `http://localhost:${port}/v1/`,
|
||||||
|
documentation: "https://kinto.readthedocs.org/",
|
||||||
|
version: "1.5.1",
|
||||||
|
commit: "cbc6f58",
|
||||||
|
hello: "kinto",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is the initial, empty state of the collection. This is only used
|
||||||
|
// for the first sync.
|
||||||
|
const RESPONSE_EMPTY_INITIAL = {
|
||||||
|
comment: "RESPONSE_EMPTY_INITIAL",
|
||||||
|
sampleHeaders: [
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
'ETag: "1000"',
|
||||||
|
],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 1000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature:
|
||||||
|
"vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Here, we map request method and path to the available responses
|
||||||
|
const emptyCollectionResponses = {
|
||||||
|
"GET:/test_remote_settings_signatures/test_cert_chain.pem?": [
|
||||||
|
RESPONSE_CERT_CHAIN,
|
||||||
|
],
|
||||||
|
"GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=1000": [
|
||||||
|
RESPONSE_EMPTY_INITIAL,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1.
|
||||||
|
// - collection: undefined -> []
|
||||||
|
// - timestamp: undefined -> 1000
|
||||||
|
//
|
||||||
|
|
||||||
|
// .. and use this map to register handlers for each path
|
||||||
|
registerHandlers(emptyCollectionResponses);
|
||||||
|
|
||||||
|
// Clear events snapshot.
|
||||||
|
Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_ALL_CHANNELS, true);
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
|
||||||
|
// With all of this set up, we attempt a sync. This will resolve if all is
|
||||||
|
// well and throw if something goes wrong.
|
||||||
|
await client.maybeSync(1000);
|
||||||
|
|
||||||
|
equal((await client.get()).length, 0);
|
||||||
|
|
||||||
|
assertTelemetryEvents([
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.SYNC_START,
|
||||||
|
source: TELEMETRY_SOURCE,
|
||||||
|
trigger: "manual",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.SUCCESS,
|
||||||
|
source: TELEMETRY_SOURCE,
|
||||||
|
trigger: "manual",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2.
|
||||||
|
// - collection: [] -> [RECORD2, RECORD1]
|
||||||
|
// - timestamp: 1000 -> 3000
|
||||||
|
//
|
||||||
|
// Check that some additions (2 records) to the collection have a valid
|
||||||
|
// signature.
|
||||||
|
|
||||||
|
// This response adds two entries (RECORD1 and RECORD2) to the collection
|
||||||
|
const RESPONSE_TWO_ADDED = {
|
||||||
|
comment: "RESPONSE_TWO_ADDED",
|
||||||
|
sampleHeaders: [
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
'ETag: "3000"',
|
||||||
|
],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 3000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature:
|
||||||
|
"dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [RECORD2, RECORD1],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const twoItemsResponses = {
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=1000":
|
||||||
|
[RESPONSE_TWO_ADDED],
|
||||||
|
};
|
||||||
|
registerHandlers(twoItemsResponses);
|
||||||
|
await client.maybeSync(3000);
|
||||||
|
|
||||||
|
equal((await client.get()).length, 2);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3.
|
||||||
|
// - collection: [RECORD2, RECORD1] -> [RECORD2, RECORD3]
|
||||||
|
// - timestamp: 3000 -> 4000
|
||||||
|
//
|
||||||
|
// Check the collection with one addition and one removal has a valid
|
||||||
|
// signature
|
||||||
|
const THREE_ITEMS_SIG =
|
||||||
|
"MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw";
|
||||||
|
|
||||||
|
// Remove RECORD1, add RECORD3
|
||||||
|
const RESPONSE_ONE_ADDED_ONE_REMOVED = {
|
||||||
|
comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
|
||||||
|
sampleHeaders: [
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
'ETag: "4000"',
|
||||||
|
],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 4000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature: THREE_ITEMS_SIG,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [RECORD3, RECORD1_DELETION],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const oneAddedOneRemovedResponses = {
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=3000":
|
||||||
|
[RESPONSE_ONE_ADDED_ONE_REMOVED],
|
||||||
|
};
|
||||||
|
registerHandlers(oneAddedOneRemovedResponses);
|
||||||
|
await client.maybeSync(4000);
|
||||||
|
|
||||||
|
equal((await client.get()).length, 2);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 4.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
||||||
|
// - timestamp: 4000 -> 4100
|
||||||
|
//
|
||||||
|
// Check the signature is still valid with no operation (no changes)
|
||||||
|
|
||||||
|
// Leave the collection unchanged
|
||||||
|
const RESPONSE_EMPTY_NO_UPDATE = {
|
||||||
|
comment: "RESPONSE_EMPTY_NO_UPDATE ",
|
||||||
|
sampleHeaders: [
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
'ETag: "4000"',
|
||||||
|
],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 4000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature: THREE_ITEMS_SIG,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const noOpResponses = {
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=4000":
|
||||||
|
[RESPONSE_EMPTY_NO_UPDATE],
|
||||||
|
};
|
||||||
|
registerHandlers(noOpResponses);
|
||||||
|
await client.maybeSync(4100);
|
||||||
|
|
||||||
|
equal((await client.get()).length, 2);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 5.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
||||||
|
// - timestamp: 4000 -> 5000
|
||||||
|
//
|
||||||
|
// Check the collection is reset when the signature is invalid.
|
||||||
|
// Client will:
|
||||||
|
// - Fetch metadata (with bad signature)
|
||||||
|
// - Perform the sync (fetch empty changes)
|
||||||
|
// - Refetch the metadata and the whole collection
|
||||||
|
// - Validate signature successfully, but with no changes to emit.
|
||||||
|
|
||||||
|
const RESPONSE_COMPLETE_INITIAL = {
|
||||||
|
comment: "RESPONSE_COMPLETE_INITIAL ",
|
||||||
|
sampleHeaders: [
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
'ETag: "4000"',
|
||||||
|
],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 4000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature: THREE_ITEMS_SIG,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [RECORD2, RECORD3],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const RESPONSE_EMPTY_NO_UPDATE_BAD_SIG = {
|
||||||
|
...RESPONSE_EMPTY_NO_UPDATE,
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 4000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature: "aW52YWxpZCBzaWduYXR1cmUK",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const badSigGoodSigResponses = {
|
||||||
|
// The first collection state is the three item collection (since
|
||||||
|
// there was sync with no updates before) - but, since the signature is wrong,
|
||||||
|
// another request will be made...
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000":
|
||||||
|
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
|
||||||
|
// Subsequent signature returned is a valid one for the three item
|
||||||
|
// collection.
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
|
||||||
|
RESPONSE_COMPLETE_INITIAL,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
registerHandlers(badSigGoodSigResponses);
|
||||||
|
Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_ALL_CHANNELS, true);
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
|
||||||
|
let syncEventSent = false;
|
||||||
|
client.on("sync", () => {
|
||||||
|
syncEventSent = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.maybeSync(5000);
|
||||||
|
|
||||||
|
equal((await client.get()).length, 2);
|
||||||
|
|
||||||
|
// since we only fixed the signature, and no data was changed, the sync event
|
||||||
|
// was not sent.
|
||||||
|
equal(syncEventSent, false);
|
||||||
|
|
||||||
|
// ensure that the failure count is incremented for a succesful sync with an
|
||||||
|
// (initial) bad signature - only SERVICES_SETTINGS_SYNC_SIG_FAIL should
|
||||||
|
// increment.
|
||||||
|
assertTelemetryEvents([
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.SYNC_START,
|
||||||
|
source: TELEMETRY_SOURCE,
|
||||||
|
trigger: "manual",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.SIGNATURE_ERROR,
|
||||||
|
source: TELEMETRY_SOURCE,
|
||||||
|
trigger: "manual",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 6.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
||||||
|
// - timestamp: 4000 -> 5000
|
||||||
|
//
|
||||||
|
// Check the collection is reset when the signature is invalid.
|
||||||
|
// Client will:
|
||||||
|
// - Fetch metadata (with bad signature)
|
||||||
|
// - Perform the sync (fetch empty changes)
|
||||||
|
// - Refetch the whole collection and metadata
|
||||||
|
// - Sync will be no-op since local is equal to server, no changes to emit.
|
||||||
|
|
||||||
|
const badSigGoodOldResponses = {
|
||||||
|
// The first collection state is the current state (since there's no update
|
||||||
|
// - but, since the signature is wrong, another request will be made)
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000":
|
||||||
|
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
|
||||||
|
// The next request is for the full collection. This will be
|
||||||
|
// checked against the valid signature and last_modified times will be
|
||||||
|
// compared. Sync should be a no-op, even though the signature is good,
|
||||||
|
// because the local collection is newer.
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
|
||||||
|
RESPONSE_EMPTY_INITIAL,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ensure our collection hasn't been replaced with an older, empty one
|
||||||
|
equal((await client.get()).length, 2, "collection was restored");
|
||||||
|
|
||||||
|
registerHandlers(badSigGoodOldResponses);
|
||||||
|
|
||||||
|
syncEventSent = false;
|
||||||
|
client.on("sync", () => {
|
||||||
|
syncEventSent = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.maybeSync(5000);
|
||||||
|
|
||||||
|
// Local data was unchanged, since it was never than the one returned by the server,
|
||||||
|
// thus the sync event is not sent.
|
||||||
|
equal(syncEventSent, false, "event was not sent");
|
||||||
|
|
||||||
|
//
|
||||||
|
// 7.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
||||||
|
// - timestamp: 4000 -> 5000
|
||||||
|
//
|
||||||
|
// Check that a tampered local DB will be overwritten and
|
||||||
|
// sync event contain the appropriate data.
|
||||||
|
|
||||||
|
const RESPONSE_COMPLETE_BAD_SIG = {
|
||||||
|
...RESPONSE_EMPTY_NO_UPDATE,
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 5000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature: "aW52YWxpZCBzaWduYXR1cmUK",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [RECORD2, RECORD3],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const badLocalContentGoodSigResponses = {
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=3900":
|
||||||
|
[RESPONSE_COMPLETE_BAD_SIG],
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
|
||||||
|
RESPONSE_COMPLETE_INITIAL,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
registerHandlers(badLocalContentGoodSigResponses);
|
||||||
|
|
||||||
|
// we create a local state manually here, in order to test that the sync event data
|
||||||
|
// properly contains created, updated, and deleted records.
|
||||||
|
// the local DB contains same id as RECORD2 and a fake record.
|
||||||
|
// the final server collection contains RECORD2 and RECORD3
|
||||||
|
const localId = "0602b1b2-12ab-4d3a-b6fb-593244e7b035";
|
||||||
|
await client.db.importChanges(
|
||||||
|
{ signatures: [{ x5u, signature: "abc" }] },
|
||||||
|
3900,
|
||||||
|
[
|
||||||
|
{ ...RECORD2, last_modified: 1234567890, serialNumber: "abc" },
|
||||||
|
{ id: localId },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
clear: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let syncData = null;
|
||||||
|
client.on("sync", ({ data }) => {
|
||||||
|
syncData = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear events snapshot.
|
||||||
|
Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_ALL_CHANNELS, true);
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
|
||||||
|
// Events telemetry is sampled on released, use fake channel.
|
||||||
|
await client.maybeSync(5000);
|
||||||
|
|
||||||
|
// We should report a corruption_error.
|
||||||
|
assertTelemetryEvents([
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.SYNC_START,
|
||||||
|
source: client.identifier,
|
||||||
|
trigger: "manual",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.CORRUPTION_ERROR,
|
||||||
|
source: client.identifier,
|
||||||
|
trigger: "manual",
|
||||||
|
duration: d => parseInt(d) > 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// The local data was corrupted, and the Telemetry status reflects it.
|
||||||
|
// But the sync overwrote the bad data and was eventually a success.
|
||||||
|
// Since local data was replaced, we use records IDs to determine
|
||||||
|
// what was created and deleted. And bad local data will appear
|
||||||
|
// in the sync event as deleted.
|
||||||
|
equal(syncData.current.length, 2);
|
||||||
|
equal(syncData.created.length, 1);
|
||||||
|
equal(syncData.created[0].id, RECORD3.id);
|
||||||
|
equal(syncData.updated.length, 1);
|
||||||
|
equal(syncData.updated[0].old.serialNumber, "abc");
|
||||||
|
equal(syncData.updated[0].new.serialNumber, RECORD2.serialNumber);
|
||||||
|
equal(syncData.deleted.length, 1);
|
||||||
|
equal(syncData.deleted[0].id, localId);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 8.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3] (unchanged because of error)
|
||||||
|
// - timestamp: 4000 -> 6000
|
||||||
|
//
|
||||||
|
// Check that a failing signature throws after retry, and that sync changes
|
||||||
|
// are not applied.
|
||||||
|
|
||||||
|
const RESPONSE_ONLY_RECORD4_BAD_SIG = {
|
||||||
|
comment: "Create RECORD4",
|
||||||
|
sampleHeaders: [
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
'ETag: "6000"',
|
||||||
|
],
|
||||||
|
status: { status: 200, statusText: "OK" },
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 6000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature: "aaaaaaaaaaaaaaaaaaaaaaaa", // sig verifier wants proper length or will crash.
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
id: "f765df30-b2f1-42f6-9803-7bd5a07b5098",
|
||||||
|
last_modified: 6000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000 = {
|
||||||
|
...RESPONSE_EMPTY_NO_UPDATE,
|
||||||
|
responseBody: JSON.stringify({
|
||||||
|
timestamp: 6000,
|
||||||
|
metadata: {
|
||||||
|
signatures: [
|
||||||
|
{
|
||||||
|
x5u,
|
||||||
|
signature: "aW52YWxpZCBzaWduYXR1cmUK",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
changes: [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const allBadSigResponses = {
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=4000":
|
||||||
|
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000],
|
||||||
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=6000": [
|
||||||
|
RESPONSE_ONLY_RECORD4_BAD_SIG,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset telemetry capture.
|
||||||
|
Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_ALL_CHANNELS, true);
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
|
||||||
|
registerHandlers(allBadSigResponses);
|
||||||
|
await Assert.rejects(
|
||||||
|
client.maybeSync(6000),
|
||||||
|
RemoteSettingsClient.InvalidSignatureError,
|
||||||
|
"Sync failed as expected (bad signature after retry)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure that the failure is reflected in the accumulated telemetry:
|
||||||
|
assertTelemetryEvents([
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.SYNC_START,
|
||||||
|
source: TELEMETRY_SOURCE,
|
||||||
|
trigger: "manual",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR,
|
||||||
|
source: TELEMETRY_SOURCE,
|
||||||
|
trigger: "manual",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// When signature fails after retry, the local data present before sync
|
||||||
|
// should be maintained (if its signature is valid).
|
||||||
|
ok(
|
||||||
|
arrayEqual(
|
||||||
|
(await client.get()).map(r => r.id),
|
||||||
|
[RECORD3.id, RECORD2.id]
|
||||||
|
),
|
||||||
|
"Local records were not changed"
|
||||||
|
);
|
||||||
|
// And local data should still be valid.
|
||||||
|
await client.get({ verifySignature: true }); // Not raising.
|
||||||
|
|
||||||
|
//
|
||||||
|
// 9.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [] (cleared)
|
||||||
|
// - timestamp: 4000 -> 6000
|
||||||
|
//
|
||||||
|
// Check that local data is cleared during sync if signature is not valid.
|
||||||
|
|
||||||
|
await client.db.create({
|
||||||
|
id: "c6b19c67-2e0e-4a82-b7f7-1777b05f3e81",
|
||||||
|
last_modified: 42,
|
||||||
|
tampered: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
client.maybeSync(6000),
|
||||||
|
RemoteSettingsClient.InvalidSignatureError,
|
||||||
|
"Sync failed as expected (bad signature after retry)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since local data was tampered, it was cleared.
|
||||||
|
equal((await client.get()).length, 0, "Local database is now empty.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// 10.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [] (cleared)
|
||||||
|
// - timestamp: 4000 -> 6000
|
||||||
|
//
|
||||||
|
// Check that local data is cleared during sync if signature is not valid.
|
||||||
|
|
||||||
|
await client.db.create({
|
||||||
|
id: "c6b19c67-2e0e-4a82-b7f7-1777b05f3e81",
|
||||||
|
last_modified: 42,
|
||||||
|
tampered: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
client.maybeSync(6000),
|
||||||
|
RemoteSettingsClient.InvalidSignatureError,
|
||||||
|
"Sync failed as expected (bad signature after retry)"
|
||||||
|
);
|
||||||
|
// Since local data was tampered, it was cleared.
|
||||||
|
equal((await client.get()).length, 0, "Local database is now empty.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// 11.
|
||||||
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
||||||
|
// - timestamp: 4000 -> 6000
|
||||||
|
//
|
||||||
|
// Check that local data is restored if signature was valid before sync.
|
||||||
|
const sigCalls = [];
|
||||||
|
let i = 0;
|
||||||
|
client._verifier = {
|
||||||
|
async asyncVerifyContentSignature(serialized) {
|
||||||
|
sigCalls.push(serialized);
|
||||||
|
console.log(`verify call ${i}`);
|
||||||
|
return [
|
||||||
|
false, // After importing changes.
|
||||||
|
true, // When checking previous local data.
|
||||||
|
false, // Still fail after retry.
|
||||||
|
true, // When checking previous local data again.
|
||||||
|
][i++];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Create an extra record. It will have a valid signature locally
|
||||||
|
// thanks to the verifier mock.
|
||||||
|
await client.db.importChanges(
|
||||||
|
{
|
||||||
|
signatures: [{ x5u, signature: "aa" }],
|
||||||
|
},
|
||||||
|
4000,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "extraId",
|
||||||
|
last_modified: 42,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
equal((await client.get()).length, 1);
|
||||||
|
|
||||||
|
// Now sync, but importing changes will have failing signature,
|
||||||
|
// and so will retry (see `sigResults`).
|
||||||
|
await Assert.rejects(
|
||||||
|
client.maybeSync(6000),
|
||||||
|
RemoteSettingsClient.InvalidSignatureError,
|
||||||
|
"Sync failed as expected (bad signature after retry)"
|
||||||
|
);
|
||||||
|
equal(i, 4, "sync has retried as expected");
|
||||||
|
|
||||||
|
// Make sure that we retried on a blank DB. The extra record should
|
||||||
|
// have been deleted when we validated the signature the second time.
|
||||||
|
// Since local data was tampered, it was cleared.
|
||||||
|
ok(/extraId/.test(sigCalls[0]), "extra record when importing changes");
|
||||||
|
ok(/extraId/.test(sigCalls[1]), "extra record when checking local");
|
||||||
|
ok(!/extraId/.test(sigCalls[2]), "db was flushed before retry");
|
||||||
|
ok(/extraId/.test(sigCalls[3]), "when checking local after retry");
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICdTCCAV2gAwIBAgIUdVZo8xeazUY3Fzm6cy61HaSjFJUwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwIzEhMB8GA1UEAwwYY29sbGVjdGlvbi1zaWduZXItaW50LUNBMCIYDzIwMjQx
|
||||||
|
MTI3MDAwMDAwWhgPMjAyNzAyMDUwMDAwMDBaMCYxJDAiBgNVBAMMG2NvbGxlY3Rp
|
||||||
|
b24tc2lnbmVyLWVlLWludC1DQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABKFockM2
|
||||||
|
K1x7GInzeRVGFaHHP7SN7oY+AikV22COJS3ktxMtqM6Y6DFTTmqcDAsJyNY5regy
|
||||||
|
BuW6gTRzoR+jMOBdqMluQ4P+J4c9qXEDviiIz/AC8Fr3Gh/dzIN0qm6pzqNIMEYw
|
||||||
|
EwYDVR0lBAwwCgYIKwYBBQUHAwMwLwYDVR0RBCgwJoIkb25lY3JsLmNvbnRlbnQt
|
||||||
|
c2lnbmF0dXJlLm1vemlsbGEub3JnMA0GCSqGSIb3DQEBCwUAA4IBAQBVHMPKgXKX
|
||||||
|
EVVTDRw8lF0MQBHUPA2+4f9JDsaq5AexGpFqfDzFobrzZnzKQyaODeoalhASraWs
|
||||||
|
GvhXrXFeSkSLqNpw657JXuIvVOULJjr2h9uA9N837KgFosvLLIIoKs1zeXG0LSYy
|
||||||
|
NSJBSme+n8ETV2d8RJ/w575rhnheDz6GcVH0hduvQR0g7dg8ZMtdfi16sjKaJRup
|
||||||
|
coH4N8t4YhLqM4PyrEATZyTEW5jOCKCb91FWgPu0ovfQz+t3jjS7TPpUa3IYJS8V
|
||||||
|
eFUHXUAXFFgD0xCl7P/V+kopWSijZHT8mwnPfn2z9EmV5ijioIsw2MF+FACkPWa1
|
||||||
|
o2NzujCdDAEl
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
issuer:collection-signer-int-CA
|
||||||
|
subject:collection-signer-ee-int-CA
|
||||||
|
subjectKey:secp384r1
|
||||||
|
extension:extKeyUsage:codeSigning
|
||||||
|
extension:subjectAlternativeName:onecrl.content-signature.mozilla.org
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAzCCAeugAwIBAgIUf3MHKqXo3U9dG7tkPLy8t7D+1o4wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwKTEnMCUGA1UEAwweeHBjc2hlbGwgc2lnbmVkIGFwcHMgdGVzdCByb290MCIY
|
||||||
|
DzIwMjQxMTI3MDAwMDAwWhgPMjAyNzAyMDUwMDAwMDBaMCMxITAfBgNVBAMMGGNv
|
||||||
|
bGxlY3Rpb24tc2lnbmVyLWludC1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBALqIUahEjhbWQf1utogGNhA9PBPZ6uQ1SrTs9WhXbCR7wcclqODYH72x
|
||||||
|
nAabbhqG8mvir1p1a2pkcQh6pVqnRYf3HNUknAJ+zUP8HmnQOCApk6sgw0nk27lM
|
||||||
|
wmtsDu0Vgg/xfq1pGrHTAjqLKkHup3DgDw2N/WYLK7AkkqR9uYhheZCxV5A90jvF
|
||||||
|
4LhIH6g304hD7ycW2FW3ZlqqfgKQLzp7EIAGJMwcbJetlmFbt+KWEsB1MaMMkd20
|
||||||
|
yvf8rR0l0wnvuRcOp2jhs3svIm9p47SKlWEd7ibWJZ2rkQhONsscJAQsvxaLL+Xx
|
||||||
|
j5kXMbiz/kkj+nJRxDHVA6zaGAo17Y0CAwEAAaMlMCMwDAYDVR0TBAUwAwEB/zAT
|
||||||
|
BgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAjJAZX8SZOh1H
|
||||||
|
xLqKHgt2EiFloIW3k3aAnTFQPAEUp+IBXjJdA8cFzEDSHo0dTr8jBhR8V1gwy8+Z
|
||||||
|
1i6loA0rgJUdDhUP24SqbfsdF6kcltlBfjR4lBfbZq0BClMiNDL2ZOQS4neF15fN
|
||||||
|
WXUv2UE9ooKNUjgIzXHbU/ykbwTKB7Bmsh+r5CYs9LMNF5P/QDAlDNwvZglxiyyq
|
||||||
|
61fi06lnBXff0zObSdV/8Wm0PQyHEBfNY3L94wuIFxGsX0YngO7CCtaIFIeymzob
|
||||||
|
8SvH4W0vfeqYzDFM4PfgFbDs4KnAzJS+WwWq2HRDsHMZTDZVVXVA61LXYhFEOam1
|
||||||
|
ASCDwhnClA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
issuer:xpcshell signed apps test root
|
||||||
|
subject:collection-signer-int-CA
|
||||||
|
extension:basicConstraints:cA,
|
||||||
|
extension:extKeyUsage:codeSigning
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
const { Database } = ChromeUtils.importESModule(
|
||||||
|
"resource://services-settings/Database.sys.mjs"
|
||||||
|
);
|
||||||
|
const { RemoteSettings } = ChromeUtils.importESModule(
|
||||||
|
"resource://services-settings/remote-settings.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let startupClients;
|
||||||
|
|
||||||
|
add_setup(async () => {
|
||||||
|
// Disable signature verification of collections listed in test startup.json.mzlz4
|
||||||
|
// since their metadata will eventually expire.
|
||||||
|
startupClients = [
|
||||||
|
"message-groups",
|
||||||
|
"nimbus-desktop-experiments",
|
||||||
|
"search-categorization",
|
||||||
|
"tracking-protection-lists",
|
||||||
|
"query-stripping",
|
||||||
|
"cfr",
|
||||||
|
].map(id => {
|
||||||
|
const c = RemoteSettings(id);
|
||||||
|
c.verifySignature = false;
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup an HTTP server to serve the 'startup.json.mzlz4' bundle.
|
||||||
|
server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
server.registerDirectory(
|
||||||
|
"/cdn/bundles/",
|
||||||
|
do_get_file("test_remote_settings_startup_bundle")
|
||||||
|
);
|
||||||
|
server.registerPathHandler("/v1/", (request, response) => {
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
capabilities: {
|
||||||
|
attachments: {
|
||||||
|
base_url: `http://localhost:${server.identity.primaryPort}/cdn/`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
server.stop(() => {});
|
||||||
|
Services.prefs.clearUserPref("services.settings.loglevel");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
await Database.destroy();
|
||||||
|
RemoteSettings._ongoingExtractBundlePromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_bundle_is_pulled_when_get_needs_sync() {
|
||||||
|
const client = startupClients[0];
|
||||||
|
|
||||||
|
Assert.ok(
|
||||||
|
!(await Utils.hasLocalDump(client.bucketName, client.collectionName)),
|
||||||
|
"Client has no packaged dump"
|
||||||
|
);
|
||||||
|
|
||||||
|
const records = await client.get();
|
||||||
|
|
||||||
|
Assert.equal(records.length, 6, "Records were read from startup bundle");
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_signature_of_extracted_data_from_bundle_is_verified() {
|
||||||
|
const c = RemoteSettings("tracking-protection-lists"); // part of startup.json.mzlz4
|
||||||
|
c.verifySignature = true;
|
||||||
|
|
||||||
|
let called = null;
|
||||||
|
c.validateCollectionSignature = (records, timestamp, metadata) => {
|
||||||
|
called = { records, timestamp, metadata };
|
||||||
|
};
|
||||||
|
|
||||||
|
await c.get();
|
||||||
|
|
||||||
|
Assert.ok(!!called.records.length);
|
||||||
|
Assert.greaterOrEqual(called.timestamp, 1694684362860);
|
||||||
|
Assert.ok(called.metadata.flags.includes("startup"));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_bundle_is_not_importent_when_signature_fails() {
|
||||||
|
const c = RemoteSettings("tracking-protection-lists"); // part of startup.json.mzlz4
|
||||||
|
c.verifySignature = true;
|
||||||
|
|
||||||
|
let called = false;
|
||||||
|
c.validateCollectionSignature = () => {
|
||||||
|
called = true;
|
||||||
|
throw new Error("boom!");
|
||||||
|
};
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await c.get({ emptyListFallback: false });
|
||||||
|
Assert.ok(false, ".get() should fail without network");
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
Assert.ok(called, "Signature was verified");
|
||||||
|
Assert.equal(
|
||||||
|
error.name,
|
||||||
|
"UnknownCollectionError",
|
||||||
|
".get() fails without network and with bad startup bundle"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_sync_occurs_if_collection_not_part_of_bundle() {
|
||||||
|
const c = RemoteSettings("foo");
|
||||||
|
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await c.get({ emptyListFallback: false });
|
||||||
|
Assert.ok(false, ".get() should fail when bundle disabled");
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
Assert.equal(
|
||||||
|
error.name,
|
||||||
|
"UnknownCollectionError",
|
||||||
|
".get() fails to sync data"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_several_clients_wait_for_bundle() {
|
||||||
|
// several clients calling .get() in parallel will all take content from bundle.
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
startupClients.map(c => c.get({ emptyListFallback: false }))
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.deepEqual(
|
||||||
|
[6, 70, 5, "UnknownCollectionError", 3, 11],
|
||||||
|
results.map(({ status, value, reason }) =>
|
||||||
|
status == "fulfilled" ? value.length : reason.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
Binary file not shown.
@@ -0,0 +1,65 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
async function clear_state() {
|
||||||
|
await new SyncHistory("").clear();
|
||||||
|
}
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_entries_are_stored_by_source() {
|
||||||
|
const history = new SyncHistory();
|
||||||
|
await history.store(42, "success", { pi: "3.14" });
|
||||||
|
// Check that history is isolated by source.
|
||||||
|
await new SyncHistory("main/cfr").store(88, "error");
|
||||||
|
|
||||||
|
const l = await history.list();
|
||||||
|
|
||||||
|
Assert.deepEqual(l, [
|
||||||
|
{
|
||||||
|
timestamp: 42,
|
||||||
|
status: "success",
|
||||||
|
infos: { pi: "3.14" },
|
||||||
|
datetime: new Date(42),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(
|
||||||
|
async function test_old_entries_are_removed_keep_fixed_size_per_source() {
|
||||||
|
const history = new SyncHistory("settings-sync", { size: 3 });
|
||||||
|
const anotherHistory = await new SyncHistory("main/cfr");
|
||||||
|
|
||||||
|
await history.store(42, "success");
|
||||||
|
await history.store(41, "sync_error");
|
||||||
|
await history.store(43, "up_to_date");
|
||||||
|
|
||||||
|
let l = await history.list();
|
||||||
|
Assert.equal(l.length, 3);
|
||||||
|
|
||||||
|
await history.store(44, "success");
|
||||||
|
await anotherHistory.store(44, "success");
|
||||||
|
|
||||||
|
l = await history.list();
|
||||||
|
Assert.equal(l.length, 3);
|
||||||
|
Assert.ok(!l.map(e => e.timestamp).includes(41));
|
||||||
|
|
||||||
|
l = await anotherHistory.list();
|
||||||
|
Assert.equal(l.length, 1);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
add_task(clear_state);
|
||||||
|
|
||||||
|
add_task(async function test_entries_are_sorted_by_timestamp_desc() {
|
||||||
|
const history = new SyncHistory("settings-sync");
|
||||||
|
await history.store(42, "success");
|
||||||
|
await history.store(41, "sync_error");
|
||||||
|
await history.store(44, "up_to_date");
|
||||||
|
|
||||||
|
const l = await history.list();
|
||||||
|
|
||||||
|
Assert.deepEqual(
|
||||||
|
l.map(e => e.timestamp),
|
||||||
|
[44, 42, 41]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
add_task(clear_state);
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
const BinaryOutputStream = Components.Constructor(
|
||||||
|
"@mozilla.org/binaryoutputstream;1",
|
||||||
|
"nsIBinaryOutputStream",
|
||||||
|
"setOutputStream"
|
||||||
|
);
|
||||||
|
|
||||||
|
const server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
registerCleanupFunction(() => server.stop(() => {}));
|
||||||
|
const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`;
|
||||||
|
|
||||||
|
const proxyServer = new HttpServer();
|
||||||
|
proxyServer.identity.add("http", "localhost", server.identity.primaryPort);
|
||||||
|
proxyServer.start(-1);
|
||||||
|
registerCleanupFunction(() => proxyServer.stop(() => {}));
|
||||||
|
const PROXY_PORT = proxyServer.identity.primaryPort;
|
||||||
|
|
||||||
|
// A sequence of bytes that would become garbage if it were to be read as UTF-8:
|
||||||
|
// - 0xEF 0xBB 0xBF is a byte order mark.
|
||||||
|
// - 0xC0 on its own is invalid (it's the first byte of a 2-byte encoding).
|
||||||
|
const INVALID_UTF_8_BYTES = [0xef, 0xbb, 0xbf, 0xc0];
|
||||||
|
|
||||||
|
server.registerPathHandler("/binary.dat", (request, response) => {
|
||||||
|
response.setStatusLine(null, 201, "StatusLineHere");
|
||||||
|
response.setHeader("headerName", "HeaderValue: HeaderValueEnd");
|
||||||
|
let binaryOut = new BinaryOutputStream(response.bodyOutputStream);
|
||||||
|
binaryOut.writeByteArray([0xef, 0xbb, 0xbf, 0xc0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTTPS requests are proxied with CONNECT, but our test server is HTTP,
|
||||||
|
// which means that the proxy will receive GET http://localhost:port.
|
||||||
|
var proxiedCount = 0;
|
||||||
|
proxyServer.registerPrefixHandler("/", (request, response) => {
|
||||||
|
++proxiedCount;
|
||||||
|
Assert.equal(request.path, "/binary.dat", `Proxy request ${proxiedCount}`);
|
||||||
|
// Close connection without sending any response.
|
||||||
|
response.seizePower();
|
||||||
|
response.finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_utils_fetch_binary() {
|
||||||
|
let res = await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);
|
||||||
|
|
||||||
|
Assert.equal(res.status, 201, "res.status");
|
||||||
|
Assert.equal(res.statusText, "StatusLineHere", "res.statusText");
|
||||||
|
Assert.equal(
|
||||||
|
res.headers.get("headerName"),
|
||||||
|
"HeaderValue: HeaderValueEnd",
|
||||||
|
"Utils.fetch should return the header"
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.deepEqual(
|
||||||
|
Array.from(new Uint8Array(await res.arrayBuffer())),
|
||||||
|
INVALID_UTF_8_BYTES,
|
||||||
|
"Binary response body should be returned as is"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_utils_fetch_binary_as_text() {
|
||||||
|
let res = await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);
|
||||||
|
Assert.deepEqual(
|
||||||
|
Array.from(await res.text(), c => c.charCodeAt(0)),
|
||||||
|
[65533],
|
||||||
|
"Interpreted as UTF-8, the response becomes garbage"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_utils_fetch_binary_as_json() {
|
||||||
|
let res = await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);
|
||||||
|
await Assert.rejects(
|
||||||
|
res.json(),
|
||||||
|
/SyntaxError: JSON.parse: unexpected character/,
|
||||||
|
"Binary data is invalid JSON"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_utils_fetch_has_conservative() {
|
||||||
|
let channelPromise = TestUtils.topicObserved("http-on-modify-request");
|
||||||
|
await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);
|
||||||
|
|
||||||
|
let channel = (await channelPromise)[0].QueryInterface(Ci.nsIHttpChannel);
|
||||||
|
|
||||||
|
Assert.equal(channel.URI.spec, `${SERVER_BASE_URL}/binary.dat`, "URL OK");
|
||||||
|
|
||||||
|
let internalChannel = channel.QueryInterface(Ci.nsIHttpChannelInternal);
|
||||||
|
Assert.ok(internalChannel.beConservative, "beConservative flag is set");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_utils_fetch_has_conservative() {
|
||||||
|
let channelPromise = TestUtils.topicObserved("http-on-modify-request");
|
||||||
|
await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);
|
||||||
|
|
||||||
|
let channel = (await channelPromise)[0].QueryInterface(Ci.nsIHttpChannel);
|
||||||
|
|
||||||
|
Assert.equal(channel.URI.spec, `${SERVER_BASE_URL}/binary.dat`, "URL OK");
|
||||||
|
|
||||||
|
let internalChannel = channel.QueryInterface(Ci.nsIHttpChannelInternal);
|
||||||
|
Assert.ok(internalChannel.beConservative, "beConservative flag is set");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_utils_fetch_with_bad_proxy() {
|
||||||
|
Services.prefs.setIntPref("network.proxy.type", 1);
|
||||||
|
Services.prefs.setStringPref("network.proxy.http", "127.0.0.1");
|
||||||
|
Services.prefs.setIntPref("network.proxy.http_port", PROXY_PORT);
|
||||||
|
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
|
||||||
|
|
||||||
|
// The URL that we're going to request.
|
||||||
|
const DESTINATION_URL = `${SERVER_BASE_URL}/binary.dat`;
|
||||||
|
|
||||||
|
Assert.equal(proxiedCount, 0, "Proxy not used yet");
|
||||||
|
{
|
||||||
|
info("Bad proxy, default prefs");
|
||||||
|
let res = await Utils.fetch(DESTINATION_URL);
|
||||||
|
Assert.equal(res.status, 201, "Bypassed bad proxy");
|
||||||
|
// 10 instead of 1 because of reconnect attempts after a dropped request.
|
||||||
|
Assert.equal(proxiedCount, 10, "Proxy was used by HttpChannel");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disables the failover logic from HttpChannel.
|
||||||
|
Services.prefs.setBoolPref("network.proxy.failover_direct", false);
|
||||||
|
proxiedCount = 0;
|
||||||
|
{
|
||||||
|
info("Bad proxy, disabled network.proxy.failover_direct");
|
||||||
|
let res = await Utils.fetch(DESTINATION_URL);
|
||||||
|
Assert.equal(res.status, 201, "Bypassed bad proxy");
|
||||||
|
// 10 instead of 1 because of reconnect attempts after a dropped request.
|
||||||
|
Assert.equal(proxiedCount, 10, "Proxy was used by ServiceRequest");
|
||||||
|
}
|
||||||
|
|
||||||
|
proxiedCount = 0;
|
||||||
|
{
|
||||||
|
info("Using internal option of Utils.fetch: bypassProxy=true");
|
||||||
|
let res = await Utils.fetch(DESTINATION_URL, { bypassProxy: true });
|
||||||
|
Assert.equal(res.status, 201, "Bypassed bad proxy");
|
||||||
|
Assert.equal(proxiedCount, 0, "Not using proxy when bypassProxy=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disables the failover logic from ServiceRequest/Utils.fetch
|
||||||
|
Services.prefs.setBoolPref("network.proxy.allow_bypass", false);
|
||||||
|
proxiedCount = 0;
|
||||||
|
|
||||||
|
info("Bad proxy, disabled network.proxy.allow_bypass");
|
||||||
|
await Assert.rejects(
|
||||||
|
Utils.fetch(DESTINATION_URL),
|
||||||
|
/NetworkError/,
|
||||||
|
"Bad proxy request should fail without failover"
|
||||||
|
);
|
||||||
|
// 10 instead of 1 because of reconnect attempts after a dropped request.
|
||||||
|
Assert.equal(proxiedCount, 10, "Attempted to use proxy again");
|
||||||
|
|
||||||
|
Services.prefs.clearUserPref("network.proxy.type");
|
||||||
|
Services.prefs.clearUserPref("network.proxy.http");
|
||||||
|
Services.prefs.clearUserPref("network.proxy.http_port");
|
||||||
|
Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
|
||||||
|
Services.prefs.clearUserPref("network.proxy.failover_direct");
|
||||||
|
Services.prefs.clearUserPref("network.proxy.allow_bypass");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_base_attachment_url_depends_on_server() {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
server.registerPathHandler("/v1/", (request, response) => {
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
capabilities: {
|
||||||
|
attachments: {
|
||||||
|
base_url: "http://default-url.com/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
const before = await Utils.baseAttachmentsURL();
|
||||||
|
Assert.equal(before, "http://default-url.com/");
|
||||||
|
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v2`
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
Services.prefs.getStringPref("services.settings.server"),
|
||||||
|
Utils.SERVER_URL
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerPathHandler("/v2/", (request, response) => {
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
capabilities: {
|
||||||
|
attachments: {
|
||||||
|
base_url: "http://some-cdn-url.org/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
const after = await Utils.baseAttachmentsURL();
|
||||||
|
Assert.equal(after, "http://some-cdn-url.org/");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_base_attachment_url_reads_from_prefs() {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.base_attachments_url",
|
||||||
|
`${Utils.SERVER_URL}|https://other/`
|
||||||
|
);
|
||||||
|
Assert.equal(await Utils.baseAttachmentsURL(), "https://other/");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_base_attachment_url_is_robust_to_bad_saved_data() {
|
||||||
|
server.registerPathHandler("/v1/", (request, response) => {
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
capabilities: {
|
||||||
|
attachments: {
|
||||||
|
base_url: "http://some-cdn-url.org/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const badValue of [
|
||||||
|
"",
|
||||||
|
"|",
|
||||||
|
Utils.SERVER_URL,
|
||||||
|
`${Utils.SERVER_URL}|https://other`, // Missing trailing slash.
|
||||||
|
`${Utils.SERVER_URL}|https://other/path`, // Missing trailing slash.
|
||||||
|
]) {
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.base_attachments_url",
|
||||||
|
badValue
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
await Utils.baseAttachmentsURL(),
|
||||||
|
"http://some-cdn-url.org/",
|
||||||
|
"Fallsback to server's"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_base_attachment_url_does_not_retry_by_default() {
|
||||||
|
Services.prefs.clearUserPref("services.settings.base_attachments_url");
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
let requests = 0;
|
||||||
|
server.registerPathHandler("/v1/", (request, response) => {
|
||||||
|
requests++;
|
||||||
|
response.setStatusLine(null, 503, "Service Unavailable");
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await Utils.baseAttachmentsURL({ retryWaitMsec: 10 });
|
||||||
|
Assert.ok(false, "should throw on error");
|
||||||
|
} catch (e) {}
|
||||||
|
Assert.equal(requests, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_base_attachment_url_can_retry() {
|
||||||
|
Services.prefs.clearUserPref("services.settings.base_attachments_url");
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
"services.settings.server",
|
||||||
|
`http://localhost:${server.identity.primaryPort}/v1`
|
||||||
|
);
|
||||||
|
let requests = 0;
|
||||||
|
server.registerPathHandler("/v1/", (request, response) => {
|
||||||
|
requests++;
|
||||||
|
if (requests < 4) {
|
||||||
|
response.setStatusLine(null, 503, "Service Unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.write(
|
||||||
|
JSON.stringify({
|
||||||
|
capabilities: {
|
||||||
|
attachments: {
|
||||||
|
base_url: "http://some-cdn-url.org/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
});
|
||||||
|
Assert.equal(
|
||||||
|
await Utils.baseAttachmentsURL({ retries: 3, retryWaitMsec: 10 }),
|
||||||
|
"http://some-cdn-url.org/"
|
||||||
|
);
|
||||||
|
Assert.equal(requests, 4);
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const server = new HttpServer();
|
||||||
|
server.start(-1);
|
||||||
|
registerCleanupFunction(() => server.stop(() => {}));
|
||||||
|
const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`;
|
||||||
|
|
||||||
|
const proxyServer = new HttpServer();
|
||||||
|
proxyServer.start(-1);
|
||||||
|
const PROXY_PORT = proxyServer.identity.primaryPort;
|
||||||
|
proxyServer.stop();
|
||||||
|
|
||||||
|
server.registerPathHandler("/destination", (request, response) => {
|
||||||
|
response.setStatusLine(null, 412);
|
||||||
|
});
|
||||||
|
|
||||||
|
function assertBypassTelemetryEvents(expectedEvents) {
|
||||||
|
const events = Glean.serviceRequest.bypassProxyInfo.testGetValue() ?? [];
|
||||||
|
Assert.equal(events.length, expectedEvents.length);
|
||||||
|
for (let i = 0; i < expectedEvents.length; i++) {
|
||||||
|
Assert.deepEqual(events[i].extra, {
|
||||||
|
value: expectedEvents[i].value,
|
||||||
|
...expectedEvents[i].extra,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_telemetry() {
|
||||||
|
const DESTINATION_URL = `${SERVER_BASE_URL}/destination`;
|
||||||
|
|
||||||
|
{
|
||||||
|
let res = await Utils.fetch(DESTINATION_URL);
|
||||||
|
Assert.equal(res.status, 412, "fetch without proxy succeeded");
|
||||||
|
}
|
||||||
|
assertBypassTelemetryEvents([]);
|
||||||
|
|
||||||
|
Services.prefs.setIntPref("network.proxy.type", 1);
|
||||||
|
Services.prefs.setStringPref("network.proxy.http", "127.0.0.1");
|
||||||
|
Services.prefs.setIntPref("network.proxy.http_port", PROXY_PORT);
|
||||||
|
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
|
||||||
|
|
||||||
|
{
|
||||||
|
let res = await Utils.fetch(DESTINATION_URL);
|
||||||
|
Assert.equal(res.status, 412, "fetch with broken proxy succeeded");
|
||||||
|
}
|
||||||
|
// Note: failover handled by HttpChannel, hence no failover here.
|
||||||
|
assertBypassTelemetryEvents([]);
|
||||||
|
|
||||||
|
// Disable HttpChannel failover in favor of Utils.fetch's implementation.
|
||||||
|
Services.prefs.setBoolPref("network.proxy.failover_direct", false);
|
||||||
|
{
|
||||||
|
let res = await Utils.fetch(DESTINATION_URL);
|
||||||
|
Assert.equal(res.status, 412, "fetch succeeded with bypassProxy feature");
|
||||||
|
}
|
||||||
|
assertBypassTelemetryEvents([
|
||||||
|
{
|
||||||
|
category: "service_request",
|
||||||
|
method: "bypass",
|
||||||
|
object: "proxy_info",
|
||||||
|
value: "remote-settings",
|
||||||
|
extra: {
|
||||||
|
source: "prefs",
|
||||||
|
type: "manual",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
Services.prefs.setBoolPref("network.proxy.allow_bypass", false);
|
||||||
|
await Assert.rejects(
|
||||||
|
Utils.fetch(DESTINATION_URL),
|
||||||
|
/NetworkError/,
|
||||||
|
"Request without failover fails"
|
||||||
|
);
|
||||||
|
assertBypassTelemetryEvents([]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
add_task(async function test_canonicaljson() {
|
||||||
|
const records = [
|
||||||
|
{ id: "1", title: "title 1" },
|
||||||
|
{ id: "2", title: "title 2" },
|
||||||
|
];
|
||||||
|
const timestamp = 42;
|
||||||
|
|
||||||
|
const serialized = await RemoteSettingsWorker.canonicalStringify(
|
||||||
|
records,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
serialized,
|
||||||
|
'{"data":[{"id":"1","title":"title 1"},{"id":"2","title":"title 2"}],"last_modified":"42"}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_import_json_dump_into_idb() {
|
||||||
|
if (IS_ANDROID) {
|
||||||
|
// Skip test: we don't ship remote settings dumps on Android (see package-manifest).
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = new RemoteSettingsClient("language-dictionaries");
|
||||||
|
const before = await client.db.getLastModified();
|
||||||
|
Assert.equal(before, null);
|
||||||
|
|
||||||
|
await RemoteSettingsWorker.importJSONDump("main", "language-dictionaries");
|
||||||
|
|
||||||
|
const after = await client.get({ syncIfEmpty: false });
|
||||||
|
Assert.ok(!!after.length);
|
||||||
|
let lastModifiedStamp = await client.getLastModified();
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
lastModifiedStamp,
|
||||||
|
Math.max(...after.map(record => record.last_modified)),
|
||||||
|
"Should have correct last modified timestamp"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force a DB close for shutdown so we can delete the DB later.
|
||||||
|
Database._shutdownHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_throws_error_if_worker_fails() {
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await RemoteSettingsWorker.canonicalStringify(null, 42);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
Assert.equal(error.message.endsWith("records is null"), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_throws_error_if_worker_fails_async() {
|
||||||
|
if (IS_ANDROID) {
|
||||||
|
// Skip test: we don't ship dump, so importJSONDump() is no-op.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Delete the Remote Settings database, and try to import a dump.
|
||||||
|
// This is not supported, and the error thrown asynchronously in the worker
|
||||||
|
// should be reported to the caller.
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.deleteDatabase("remote-settings");
|
||||||
|
request.onsuccess = () => resolve();
|
||||||
|
request.onblocked = () => reject(new Error("Cannot delete DB"));
|
||||||
|
request.onerror = event => reject(event.target.error);
|
||||||
|
});
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await RemoteSettingsWorker.importJSONDump("main", "language-dictionaries");
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
Assert.ok(/IndexedDB: Error accessing remote-settings/.test(error.message));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_throws_error_if_worker_crashes() {
|
||||||
|
// This simulates a crash at the worker level (not within a promise).
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await RemoteSettingsWorker._execute("unknown_method");
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
Assert.equal(error.message, "TypeError: Agent[method] is not a function");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_stops_worker_after_timeout() {
|
||||||
|
// Change the idle time.
|
||||||
|
Services.prefs.setIntPref(
|
||||||
|
"services.settings.worker_idle_max_milliseconds",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
// Run a task:
|
||||||
|
let serialized = await RemoteSettingsWorker.canonicalStringify([], 42);
|
||||||
|
Assert.equal(serialized, '{"data":[],"last_modified":"42"}', "API works.");
|
||||||
|
// Check that the worker gets stopped now the task is done:
|
||||||
|
await TestUtils.waitForCondition(() => !RemoteSettingsWorker.worker);
|
||||||
|
// Ensure the worker stays alive for 10 minutes instead:
|
||||||
|
Services.prefs.setIntPref(
|
||||||
|
"services.settings.worker_idle_max_milliseconds",
|
||||||
|
600000
|
||||||
|
);
|
||||||
|
// Run another task:
|
||||||
|
serialized = await RemoteSettingsWorker.canonicalStringify([], 42);
|
||||||
|
Assert.equal(
|
||||||
|
serialized,
|
||||||
|
'{"data":[],"last_modified":"42"}',
|
||||||
|
"API still works."
|
||||||
|
);
|
||||||
|
Assert.ok(RemoteSettingsWorker.worker, "Worker should stay alive a bit.");
|
||||||
|
|
||||||
|
// Clear the pref.
|
||||||
|
Services.prefs.clearUserPref(
|
||||||
|
"services.settings.worker_idle_max_milliseconds"
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
add_setup(() => {
|
||||||
|
Services.prefs.setStringPref("services.settings.loglevel", "debug");
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
Services.prefs.clearUserPref("services.settings.loglevel");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_shutdown_abort_after_start() {
|
||||||
|
// Start a forever transaction:
|
||||||
|
let counter = 0;
|
||||||
|
let transactionStarted;
|
||||||
|
let startedPromise = new Promise(r => {
|
||||||
|
transactionStarted = r;
|
||||||
|
});
|
||||||
|
let promise = Database._executeIDB(
|
||||||
|
"records",
|
||||||
|
store => {
|
||||||
|
// Signal we've started.
|
||||||
|
transactionStarted();
|
||||||
|
function makeRequest() {
|
||||||
|
if (++counter > 1000) {
|
||||||
|
Assert.ok(
|
||||||
|
false,
|
||||||
|
"We ran 1000 requests and didn't get aborted, what?"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dump("Making request " + counter + "\n");
|
||||||
|
const request = store
|
||||||
|
.index("cid")
|
||||||
|
.openCursor(IDBKeyRange.only("foopydoo/foo"));
|
||||||
|
request.onsuccess = () => {
|
||||||
|
makeRequest();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
makeRequest();
|
||||||
|
},
|
||||||
|
{ mode: "readonly" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for the transaction to start.
|
||||||
|
await startedPromise;
|
||||||
|
|
||||||
|
Database._shutdownHandler(); // should abort the readonly transaction.
|
||||||
|
|
||||||
|
let rejection;
|
||||||
|
await promise.catch(e => {
|
||||||
|
rejection = e;
|
||||||
|
});
|
||||||
|
ok(rejection, "Promise should have rejected.");
|
||||||
|
|
||||||
|
// Now clear the shutdown flag and rejection error:
|
||||||
|
Database._cancelShutdown();
|
||||||
|
rejection = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_shutdown_immediate_abort() {
|
||||||
|
// Now abort directly from the successful request.
|
||||||
|
let promise = Database._executeIDB(
|
||||||
|
"records",
|
||||||
|
store => {
|
||||||
|
let request = store
|
||||||
|
.index("cid")
|
||||||
|
.openCursor(IDBKeyRange.only("foopydoo/foo"));
|
||||||
|
request.onsuccess = () => {
|
||||||
|
// Abort immediately.
|
||||||
|
Database._shutdownHandler();
|
||||||
|
request = store
|
||||||
|
.index("cid")
|
||||||
|
.openCursor(IDBKeyRange.only("foopydoo/foo"));
|
||||||
|
Assert.ok(false, "IndexedDB allowed opening a cursor after aborting?!");
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ mode: "readonly" }
|
||||||
|
);
|
||||||
|
|
||||||
|
let rejection;
|
||||||
|
// Wait for the abort
|
||||||
|
await promise.catch(e => {
|
||||||
|
rejection = e;
|
||||||
|
});
|
||||||
|
ok(rejection, "Directly aborted promise should also have rejected.");
|
||||||
|
// Now clear the shutdown flag and rejection error:
|
||||||
|
Database._cancelShutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_shutdown_worker() {
|
||||||
|
let client = new RemoteSettingsClient("language-dictionaries");
|
||||||
|
const before = await client.db.getLastModified();
|
||||||
|
Assert.equal(before, null);
|
||||||
|
|
||||||
|
let records = [{}];
|
||||||
|
let importPromise = RemoteSettingsWorker._execute(
|
||||||
|
"_test_only_import",
|
||||||
|
["main", "language-dictionaries", records, 0],
|
||||||
|
{ mustComplete: true }
|
||||||
|
);
|
||||||
|
let stringifyPromise = RemoteSettingsWorker.canonicalStringify(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
// Change the idle time so we shut the worker down even though we can't
|
||||||
|
// set gShutdown from outside of the worker management code.
|
||||||
|
Services.prefs.setIntPref(
|
||||||
|
"services.settings.worker_idle_max_milliseconds",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
RemoteSettingsWorker._abortCancelableRequests();
|
||||||
|
await Assert.rejects(
|
||||||
|
stringifyPromise,
|
||||||
|
/Shutdown/,
|
||||||
|
"Should have aborted the stringify request at shutdown."
|
||||||
|
);
|
||||||
|
await Assert.rejects(
|
||||||
|
importPromise,
|
||||||
|
/shutting down/,
|
||||||
|
"Ensure imports get aborted during shutdown"
|
||||||
|
);
|
||||||
|
const after = await client.db.getLastModified();
|
||||||
|
Assert.equal(after, null);
|
||||||
|
await TestUtils.waitForCondition(() => !RemoteSettingsWorker.worker);
|
||||||
|
Assert.ok(
|
||||||
|
!RemoteSettingsWorker.worker,
|
||||||
|
"Worker should have been terminated."
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const { UptakeTelemetry } = ChromeUtils.importESModule(
|
||||||
|
"resource://services-settings/UptakeTelemetry.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
add_setup(function () {
|
||||||
|
Services.fog.initializeFOG();
|
||||||
|
});
|
||||||
|
|
||||||
|
function enableUptakeMetric() {
|
||||||
|
Services.fog.applyServerKnobsConfig(
|
||||||
|
JSON.stringify({
|
||||||
|
metrics_enabled: {
|
||||||
|
"uptake.remotecontent.result.uptake_remotesettings": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_unknown_status_is_not_reported() {
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await UptakeTelemetry.report("unknown-status", { source: "update-source" });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
null,
|
||||||
|
Glean.uptakeRemotecontentResult.uptakeRemotesettings.testGetValue()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_age_is_converted_to_string_and_reported() {
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
const status = UptakeTelemetry.STATUS.SUCCESS;
|
||||||
|
const age = 42;
|
||||||
|
|
||||||
|
await UptakeTelemetry.report(status, { source: "s", age });
|
||||||
|
|
||||||
|
const events =
|
||||||
|
Glean.uptakeRemotecontentResult.uptakeRemotesettings.testGetValue();
|
||||||
|
Assert.equal(1, events.length);
|
||||||
|
Assert.deepEqual(events[0].extra, {
|
||||||
|
value: status,
|
||||||
|
source: "s",
|
||||||
|
age: `${age}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_each_status_can_be_caught_in_snapshot() {
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
enableUptakeMetric();
|
||||||
|
const source = "some-source";
|
||||||
|
|
||||||
|
for (const status of Object.values(UptakeTelemetry.STATUS)) {
|
||||||
|
await UptakeTelemetry.report(status, { source });
|
||||||
|
}
|
||||||
|
|
||||||
|
const events =
|
||||||
|
Glean.uptakeRemotecontentResult.uptakeRemotesettings.testGetValue();
|
||||||
|
for (const status of Object.values(UptakeTelemetry.STATUS)) {
|
||||||
|
Assert.ok(
|
||||||
|
events.some(e => e.extra.value === status && e.extra.source === source),
|
||||||
|
`check events for ${status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
50
src/zen/tests/mochitests/remote-settings/xpcshell.toml
Normal file
50
src/zen/tests/mochitests/remote-settings/xpcshell.toml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
head = "../../../../services/common/tests/unit/head_global.js ../../../../services/common/tests/unit/head_helpers.js head_settings.js"
|
||||||
|
firefox-appdir = "browser"
|
||||||
|
tags = "remote-settings"
|
||||||
|
support-files = ["test_remote_settings_signatures/**"]
|
||||||
|
skip-if = [
|
||||||
|
"appname == 'thunderbird'", # Bug 1662758 - these tests don't pass if default bucket isn't "main".
|
||||||
|
]
|
||||||
|
|
||||||
|
["test_attachments_downloader.js"]
|
||||||
|
support-files = ["test_attachments_downloader/**"]
|
||||||
|
|
||||||
|
["test_remote_settings.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_dump_lastmodified.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_jexl_filters.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_offline.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_older_than_local.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_poll.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_recover_broken.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_release_prefs.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_signatures.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_startup_bundle.js"]
|
||||||
|
support-files = ["test_remote_settings_startup_bundle/**"]
|
||||||
|
|
||||||
|
["test_remote_settings_sync_history.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_utils.js"]
|
||||||
|
|
||||||
|
["test_remote_settings_utils_telemetry.js"]
|
||||||
|
skip-if = [
|
||||||
|
"os == 'android' && os_version == '14' && arch == 'x86_64'", # Bug 1739463
|
||||||
|
]
|
||||||
|
|
||||||
|
["test_remote_settings_worker.js"]
|
||||||
|
|
||||||
|
["test_shutdown_handling.js"]
|
||||||
|
|
||||||
|
["test_uptake_telemetry.js"]
|
||||||
|
skip-if = [
|
||||||
|
"os == 'android'", # Bug 2042499
|
||||||
|
]
|
||||||
68
src/zen/tests/mochitests/services-crypto/head_helpers.js
Normal file
68
src/zen/tests/mochitests/services-crypto/head_helpers.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/* import-globals-from ../../../common/tests/unit/head_helpers.js */
|
||||||
|
|
||||||
|
var { XPCOMUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// In the context of xpcshell tests, there won't be a default AppInfo
|
||||||
|
// eslint-disable-next-line mozilla/use-services
|
||||||
|
Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
|
||||||
|
} catch (ex) {
|
||||||
|
// Make sure to provide the right OS so crypto loads the right binaries
|
||||||
|
var OS = "XPCShell";
|
||||||
|
if (mozinfo.os == "win") {
|
||||||
|
OS = "WINNT";
|
||||||
|
} else if (mozinfo.os == "mac") {
|
||||||
|
OS = "Darwin";
|
||||||
|
} else {
|
||||||
|
OS = "Linux";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateAppInfo } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/AppInfo.sys.mjs"
|
||||||
|
);
|
||||||
|
updateAppInfo({
|
||||||
|
name: "XPCShell",
|
||||||
|
ID: "{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}",
|
||||||
|
version: "1",
|
||||||
|
platformVersion: "",
|
||||||
|
OS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64UrlDecode(s) {
|
||||||
|
s = s.replace(/-/g, "+");
|
||||||
|
s = s.replace(/_/g, "/");
|
||||||
|
|
||||||
|
// Replace padding if it was stripped by the sender.
|
||||||
|
// See http://tools.ietf.org/html/rfc4648#section-4
|
||||||
|
switch (s.length % 4) {
|
||||||
|
case 0:
|
||||||
|
break; // No pad chars in this case
|
||||||
|
case 2:
|
||||||
|
s += "==";
|
||||||
|
break; // Two pad chars
|
||||||
|
case 3:
|
||||||
|
s += "=";
|
||||||
|
break; // One pad char
|
||||||
|
default:
|
||||||
|
throw new Error("Illegal base64url string!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// With correct padding restored, apply the standard base64 decoder
|
||||||
|
return atob(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print some debug message to the console. All arguments will be printed,
|
||||||
|
* separated by spaces.
|
||||||
|
*
|
||||||
|
* @param [arg0, arg1, arg2, ...]
|
||||||
|
* Any number of arguments to print out
|
||||||
|
* Usage: _("Hello World") -> prints "Hello World"
|
||||||
|
* Usage: _(1, 2, 3) -> prints "1 2 3"
|
||||||
|
*/
|
||||||
|
var _ = function () {
|
||||||
|
print(Array.from(arguments).join(" "));
|
||||||
|
};
|
||||||
226
src/zen/tests/mochitests/services-crypto/test_crypto_crypt.js
Normal file
226
src/zen/tests/mochitests/services-crypto/test_crypto_crypt.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
const { WeaveCrypto } = ChromeUtils.importESModule(
|
||||||
|
"moz-src:///services/crypto/modules/WeaveCrypto.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
var cryptoSvc = new WeaveCrypto();
|
||||||
|
|
||||||
|
add_task(async function test_key_memoization() {
|
||||||
|
let cryptoGlobal = cryptoSvc._getCrypto();
|
||||||
|
let oldImport = cryptoGlobal.subtle.importKey;
|
||||||
|
if (!oldImport) {
|
||||||
|
_("Couldn't swizzle crypto.subtle.importKey; returning.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let iv = cryptoSvc.generateRandomIV();
|
||||||
|
let key = await cryptoSvc.generateRandomKey();
|
||||||
|
let c = 0;
|
||||||
|
cryptoGlobal.subtle.importKey = function (
|
||||||
|
format,
|
||||||
|
keyData,
|
||||||
|
algo,
|
||||||
|
extractable,
|
||||||
|
usages
|
||||||
|
) {
|
||||||
|
c++;
|
||||||
|
return oldImport.call(
|
||||||
|
cryptoGlobal.subtle,
|
||||||
|
format,
|
||||||
|
keyData,
|
||||||
|
algo,
|
||||||
|
extractable,
|
||||||
|
usages
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encryption should cause a single counter increment.
|
||||||
|
Assert.equal(c, 0);
|
||||||
|
let cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
|
||||||
|
Assert.equal(c, 1);
|
||||||
|
cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
|
||||||
|
Assert.equal(c, 1);
|
||||||
|
|
||||||
|
// ... as should decryption.
|
||||||
|
await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(c, 2);
|
||||||
|
|
||||||
|
// Un-swizzle.
|
||||||
|
cryptoGlobal.subtle.importKey = oldImport;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Just verify that it gets populated with the correct bytes.
|
||||||
|
add_task(async function test_makeUint8Array() {
|
||||||
|
ChromeUtils.importESModule("resource://gre/modules/ctypes.sys.mjs");
|
||||||
|
|
||||||
|
let item1 = cryptoSvc.makeUint8Array("abcdefghi", false);
|
||||||
|
Assert.ok(item1);
|
||||||
|
for (let i = 0; i < 8; ++i) {
|
||||||
|
Assert.equal(item1[i], "abcdefghi".charCodeAt(i));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_encrypt_decrypt() {
|
||||||
|
// First, do a normal run with expected usage... Generate a random key and
|
||||||
|
// iv, encrypt and decrypt a string.
|
||||||
|
var iv = cryptoSvc.generateRandomIV();
|
||||||
|
Assert.equal(iv.length, 24);
|
||||||
|
|
||||||
|
var key = await cryptoSvc.generateRandomKey();
|
||||||
|
Assert.equal(key.length, 44);
|
||||||
|
|
||||||
|
var mySecret = "bacon is a vegetable";
|
||||||
|
var cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
Assert.equal(cipherText.length, 44);
|
||||||
|
|
||||||
|
var clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(clearText.length, 20);
|
||||||
|
|
||||||
|
// Did the text survive the encryption round-trip?
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
Assert.notEqual(cipherText, mySecret); // just to be explicit
|
||||||
|
|
||||||
|
// Do some more tests with a fixed key/iv, to check for reproducable results.
|
||||||
|
key = "St1tFCor7vQEJNug/465dQ==";
|
||||||
|
iv = "oLjkfrLIOnK2bDRvW4kXYA==";
|
||||||
|
|
||||||
|
_("Testing small IV.");
|
||||||
|
mySecret = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=";
|
||||||
|
let shortiv = "YWJj";
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await cryptoSvc.encrypt(mySecret, key, shortiv);
|
||||||
|
} catch (ex) {
|
||||||
|
err = ex;
|
||||||
|
}
|
||||||
|
Assert.ok(!!err);
|
||||||
|
|
||||||
|
_("Testing long IV.");
|
||||||
|
let longiv = "gsgLRDaxWvIfKt75RjuvFWERt83FFsY2A0TW+0b2iVk=";
|
||||||
|
try {
|
||||||
|
await cryptoSvc.encrypt(mySecret, key, longiv);
|
||||||
|
} catch (ex) {
|
||||||
|
err = ex;
|
||||||
|
}
|
||||||
|
Assert.ok(!!err);
|
||||||
|
|
||||||
|
// Test small input sizes
|
||||||
|
mySecret = "";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "OGQjp6mK1a3fs9k9Ml4L3w==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
mySecret = "x";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "96iMl4vhOxFUW/lVHHzVqg==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
mySecret = "xx";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "olpPbETRYROCSqFWcH2SWg==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
mySecret = "xxx";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "rRbpHGyVSZizLX/x43Wm+Q==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
mySecret = "xxxx";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "HeC7miVGDcpxae9RmiIKAw==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
// Test non-ascii input
|
||||||
|
// ("testuser1" using similar-looking glyphs)
|
||||||
|
mySecret = String.fromCharCode(355, 277, 349, 357, 533, 537, 101, 345, 185);
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "Pj4ixByXoH3SU3JkOXaEKPgwRAWplAWFLQZkpJd5Kr4=");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
// Tests input spanning a block boundary (AES block size is 16 bytes)
|
||||||
|
mySecret = "123456789012345";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "e6c5hwphe45/3VN/M0bMUA==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
mySecret = "1234567890123456";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP1JOIQF87E2vTUvBUQnyV04=");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
mySecret = "12345678901234567";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP5GvxWJ9+GIAS6lXw+5fHTI=");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
key = "iz35tuIMq4/H+IYw2KTgow==";
|
||||||
|
iv = "TJYrvva2KxvkM8hvOIvWp3==";
|
||||||
|
mySecret = "i like pie";
|
||||||
|
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "DLGx8BWqSCLGG7i/xwvvxg==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
key = "c5hG3YG+NC61FFy8NOHQak1ZhMEWO79bwiAfar2euzI=";
|
||||||
|
iv = "gsgLRDaxWvIfKt75RjuvFW==";
|
||||||
|
mySecret = "i like pie";
|
||||||
|
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, iv);
|
||||||
|
Assert.equal(cipherText, "o+ADtdMd8ubzNWurS6jt0Q==");
|
||||||
|
Assert.equal(clearText, mySecret);
|
||||||
|
|
||||||
|
key = "St1tFCor7vQEJNug/465dQ==";
|
||||||
|
iv = "oLjkfrLIOnK2bDRvW4kXYA==";
|
||||||
|
mySecret = "does thunder read testcases?";
|
||||||
|
cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
|
||||||
|
Assert.equal(cipherText, "T6fik9Ros+DB2ablH9zZ8FWZ0xm/szSwJjIHZu7sjPs=");
|
||||||
|
|
||||||
|
var badkey = "badkeybadkeybadkeybadk==";
|
||||||
|
var badiv = "badivbadivbadivbadivbad=";
|
||||||
|
var badcipher = "crapinputcrapinputcrapinputcrapinputcrapinp=";
|
||||||
|
var failure;
|
||||||
|
|
||||||
|
try {
|
||||||
|
failure = false;
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, badkey, iv);
|
||||||
|
} catch (e) {
|
||||||
|
failure = true;
|
||||||
|
}
|
||||||
|
Assert.ok(failure);
|
||||||
|
|
||||||
|
try {
|
||||||
|
failure = false;
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, key, badiv);
|
||||||
|
} catch (e) {
|
||||||
|
failure = true;
|
||||||
|
}
|
||||||
|
Assert.ok(failure);
|
||||||
|
|
||||||
|
try {
|
||||||
|
failure = false;
|
||||||
|
clearText = await cryptoSvc.decrypt(cipherText, badkey, badiv);
|
||||||
|
} catch (e) {
|
||||||
|
failure = true;
|
||||||
|
}
|
||||||
|
Assert.ok(failure);
|
||||||
|
|
||||||
|
try {
|
||||||
|
failure = false;
|
||||||
|
clearText = await cryptoSvc.decrypt(badcipher, key, iv);
|
||||||
|
} catch (e) {
|
||||||
|
failure = true;
|
||||||
|
}
|
||||||
|
Assert.ok(failure);
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user