From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- services/automation/AutomationComponents.manifest | 2 + services/automation/ServicesAutomation.sys.mjs | 416 + services/automation/moz.build | 14 + .../AppServicesLoggerComponents.h | 38 + services/common/app_services_logger/Cargo.toml | 17 + .../common/app_services_logger/components.conf | 15 + services/common/app_services_logger/src/lib.rs | 128 + services/common/async.sys.mjs | 301 + services/common/hawkclient.sys.mjs | 336 + services/common/hawkrequest.sys.mjs | 197 + services/common/kinto-http-client.sys.mjs | 3061 ++ services/common/kinto-offline-client.js | 2643 ++ services/common/kinto-storage-adapter.sys.mjs | 553 + services/common/logmanager.sys.mjs | 447 + services/common/modules-testing/logging.sys.mjs | 56 + services/common/moz.build | 47 + services/common/observers.sys.mjs | 148 + services/common/rest.sys.mjs | 720 + services/common/servicesComponents.manifest | 2 + services/common/tests/moz.build | 9 + services/common/tests/unit/head_global.js | 35 + services/common/tests/unit/head_helpers.js | 270 + services/common/tests/unit/head_http.js | 33 + services/common/tests/unit/moz.build | 5 + services/common/tests/unit/test_async_chain.js | 40 + services/common/tests/unit/test_async_foreach.js | 88 + services/common/tests/unit/test_hawkclient.js | 515 + services/common/tests/unit/test_hawkrequest.js | 231 + services/common/tests/unit/test_kinto.js | 512 + services/common/tests/unit/test_load_modules.js | 60 + services/common/tests/unit/test_logmanager.js | 330 + services/common/tests/unit/test_observers.js | 82 + services/common/tests/unit/test_restrequest.js | 860 + services/common/tests/unit/test_storage_adapter.js | 307 + .../tests/unit/test_storage_adapter/empty.sqlite | Bin 0 -> 2048 bytes .../tests/unit/test_storage_adapter/v1.sqlite | Bin 0 -> 131072 bytes .../tests/unit/test_storage_adapter_shutdown.js | 28 + .../tests/unit/test_tokenauthenticatedrequest.js | 50 + .../common/tests/unit/test_tokenserverclient.js | 382 + .../common/tests/unit/test_uptake_telemetry.js | 121 + services/common/tests/unit/test_utils_atob.js | 9 + .../common/tests/unit/test_utils_convert_string.js | 146 + services/common/tests/unit/test_utils_dateprefs.js | 77 + .../common/tests/unit/test_utils_encodeBase32.js | 55 + .../tests/unit/test_utils_encodeBase64URL.js | 21 + .../unit/test_utils_ensureMillisecondsTimestamp.js | 40 + services/common/tests/unit/test_utils_makeURI.js | 62 + .../common/tests/unit/test_utils_namedTimer.js | 73 + services/common/tests/unit/test_utils_sets.js | 66 + services/common/tests/unit/test_utils_utf8.js | 9 + services/common/tests/unit/test_utils_uuid.js | 12 + services/common/tests/unit/xpcshell.toml | 63 + services/common/tokenserverclient.sys.mjs | 392 + services/common/uptake-telemetry.sys.mjs | 193 + services/common/utils.sys.mjs | 693 + services/crypto/cryptoComponents.manifest | 1 + services/crypto/modules/WeaveCrypto.sys.mjs | 232 + services/crypto/modules/jwcrypto.sys.mjs | 214 + services/crypto/modules/utils.sys.mjs | 539 + services/crypto/moz.build | 20 + services/crypto/tests/unit/head_helpers.js | 78 + services/crypto/tests/unit/test_crypto_crypt.js | 226 + services/crypto/tests/unit/test_crypto_random.js | 52 + services/crypto/tests/unit/test_jwcrypto.js | 51 + services/crypto/tests/unit/test_load_modules.js | 12 + services/crypto/tests/unit/test_utils_hawk.js | 346 + services/crypto/tests/unit/test_utils_httpmac.js | 73 + services/crypto/tests/unit/xpcshell.toml | 18 + services/docs/index.rst | 11 + services/docs/moz.build | 6 + services/fxaccounts/Credentials.sys.mjs | 134 + services/fxaccounts/FxAccounts.sys.mjs | 1657 + services/fxaccounts/FxAccountsClient.sys.mjs | 839 + services/fxaccounts/FxAccountsCommands.sys.mjs | 467 + services/fxaccounts/FxAccountsCommon.sys.mjs | 393 + services/fxaccounts/FxAccountsConfig.sys.mjs | 360 + services/fxaccounts/FxAccountsDevice.sys.mjs | 640 + services/fxaccounts/FxAccountsKeys.sys.mjs | 649 + services/fxaccounts/FxAccountsOAuth.sys.mjs | 224 + services/fxaccounts/FxAccountsPairing.sys.mjs | 511 + .../fxaccounts/FxAccountsPairingChannel.sys.mjs | 3693 +++ services/fxaccounts/FxAccountsProfile.sys.mjs | 193 + .../fxaccounts/FxAccountsProfileClient.sys.mjs | 273 + services/fxaccounts/FxAccountsPush.sys.mjs | 315 + services/fxaccounts/FxAccountsStorage.sys.mjs | 618 + services/fxaccounts/FxAccountsTelemetry.sys.mjs | 173 + services/fxaccounts/FxAccountsWebChannel.sys.mjs | 824 + services/fxaccounts/components.conf | 16 + services/fxaccounts/moz.build | 38 + services/fxaccounts/tests/browser/browser.toml | 6 + .../tests/browser/browser_device_connected.js | 51 + .../tests/browser/browser_verify_login.js | 33 + services/fxaccounts/tests/browser/head.js | 73 + services/fxaccounts/tests/mochitest/chrome.toml | 5 + .../tests/mochitest/file_invalidEmailCase.sjs | 81 + .../tests/mochitest/test_invalidEmailCase.html | 129 + services/fxaccounts/tests/xpcshell/head.js | 38 + .../fxaccounts/tests/xpcshell/test_accounts.js | 1642 + .../tests/xpcshell/test_accounts_config.js | 58 + .../xpcshell/test_accounts_device_registration.js | 1204 + services/fxaccounts/tests/xpcshell/test_client.js | 966 + .../fxaccounts/tests/xpcshell/test_commands.js | 708 + .../fxaccounts/tests/xpcshell/test_credentials.js | 130 + services/fxaccounts/tests/xpcshell/test_device.js | 127 + services/fxaccounts/tests/xpcshell/test_keys.js | 182 + .../tests/xpcshell/test_loginmgr_storage.js | 307 + .../fxaccounts/tests/xpcshell/test_oauth_flow.js | 274 + .../tests/xpcshell/test_oauth_token_storage.js | 180 + .../fxaccounts/tests/xpcshell/test_oauth_tokens.js | 255 + services/fxaccounts/tests/xpcshell/test_pairing.js | 384 + services/fxaccounts/tests/xpcshell/test_profile.js | 677 + .../tests/xpcshell/test_profile_client.js | 422 + .../fxaccounts/tests/xpcshell/test_push_service.js | 522 + .../tests/xpcshell/test_storage_manager.js | 586 + .../fxaccounts/tests/xpcshell/test_telemetry.js | 52 + .../fxaccounts/tests/xpcshell/test_web_channel.js | 1380 + services/fxaccounts/tests/xpcshell/xpcshell.toml | 49 + services/interfaces/moz.build | 19 + services/interfaces/mozIAppServicesLogger.idl | 11 + services/interfaces/mozIBridgedSyncEngine.idl | 160 + services/interfaces/mozIInterruptible.idl | 13 + services/interfaces/mozIServicesLogSink.idl | 26 + services/moz.build | 29 + services/settings/Attachments.sys.mjs | 527 + services/settings/Database.sys.mjs | 602 + services/settings/IDBHelpers.sys.mjs | 212 + services/settings/RemoteSettings.worker.mjs | 196 + services/settings/RemoteSettingsClient.sys.mjs | 1281 + services/settings/RemoteSettingsComponents.sys.mjs | 23 + services/settings/RemoteSettingsWorker.sys.mjs | 233 + services/settings/SharedUtils.sys.mjs | 51 + services/settings/SyncHistory.sys.mjs | 120 + services/settings/Utils.sys.mjs | 504 + services/settings/components.conf | 14 + services/settings/docs/index.rst | 552 + services/settings/docs/synchronization-flow.svg | 4 + .../dumps/blocklists/addons-bloomfilters.json | 243 + .../blocklists/addons-bloomfilters/addons-mlbf.bin | Bin 0 -> 828655 bytes .../addons-bloomfilters/addons-mlbf.bin.meta.json | 1 + services/settings/dumps/blocklists/addons.json | 21961 +++++++++++++ services/settings/dumps/blocklists/gfx.json | 1509 + services/settings/dumps/blocklists/moz.build | 24 + services/settings/dumps/blocklists/plugins.json | 3515 +++ services/settings/dumps/gen_last_modified.py | 90 + .../dumps/main/anti-tracking-url-decoration.json | 11 + .../dumps/main/cookie-banner-rules-list.json | 9234 ++++++ .../main/devtools-compatibility-browsers.json | 284 + services/settings/dumps/main/devtools-devices.json | 635 + services/settings/dumps/main/example.json | 4 + .../settings/dumps/main/hijack-blocklists.json | 59 + .../settings/dumps/main/language-dictionaries.json | 647 + services/settings/dumps/main/moz.build | 121 + services/settings/dumps/main/password-recipes.json | 145 + services/settings/dumps/main/password-rules.json | 1583 + .../settings/dumps/main/search-config-icons.json | 736 + .../001500a9-1a6c-3f5a-ba15-a5f5a075d256 | Bin 0 -> 5430 bytes .../001500a9-1a6c-3f5a-ba15-a5f5a075d256.meta.json | 1 + .../06cf7432-efd7-f244-927b-5e423005e1ea | Bin 0 -> 2053 bytes .../06cf7432-efd7-f244-927b-5e423005e1ea.meta.json | 1 + .../0d7668a8-c3f4-cfee-cbc8-536511528937 | Bin 0 -> 5430 bytes .../0d7668a8-c3f4-cfee-cbc8-536511528937.meta.json | 1 + .../0eec5640-6fde-d6fe-322a-c72c6d5bd5a2 | Bin 0 -> 790 bytes .../0eec5640-6fde-d6fe-322a-c72c6d5bd5a2.meta.json | 1 + .../101ce01d-2691-b729-7f16-9d389803384b | Bin 0 -> 884 bytes .../101ce01d-2691-b729-7f16-9d389803384b.meta.json | 1 + .../177aba42-9bed-4078-e36b-580e8794cd7f | Bin 0 -> 1406 bytes .../177aba42-9bed-4078-e36b-580e8794cd7f.meta.json | 1 + .../25de0352-aabb-d31f-15f7-bf9299fb004c | Bin 0 -> 1122 bytes .../25de0352-aabb-d31f-15f7-bf9299fb004c.meta.json | 1 + .../2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335 | Bin 0 -> 5430 bytes .../2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335.meta.json | 1 + .../2e835b0e-9709-d1bb-9725-87f59f3445ca | Bin 0 -> 1407 bytes .../2e835b0e-9709-d1bb-9725-87f59f3445ca.meta.json | 1 + .../32d26d19-aeb0-5c01-32e8-f8970be9246f | Bin 0 -> 252 bytes .../32d26d19-aeb0-5c01-32e8-f8970be9246f.meta.json | 1 + .../47da97b5-600f-c450-fd15-a52bb2169c11 | Bin 0 -> 5430 bytes .../47da97b5-600f-c450-fd15-a52bb2169c11.meta.json | 1 + .../4e271681-3e0f-91ac-9750-03f665efc171 | Bin 0 -> 2639 bytes .../4e271681-3e0f-91ac-9750-03f665efc171.meta.json | 1 + .../50f6171f-8e7a-b41b-862e-f97397038fb2 | Bin 0 -> 283 bytes .../50f6171f-8e7a-b41b-862e-f97397038fb2.meta.json | 1 + .../5ded611d-44b2-dc46-fd67-fb116888d75d | Bin 0 -> 5430 bytes .../5ded611d-44b2-dc46-fd67-fb116888d75d.meta.json | 1 + .../5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41 | Bin 0 -> 5430 bytes .../5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41.meta.json | 1 + .../6a83583a-f0ba-fd39-2fdb-fd2b6990ea3b | Bin 0 -> 5430 bytes .../6a83583a-f0ba-fd39-2fdb-fd2b6990ea3b.meta.json | 1 + .../6d10d702-7bd6-1452-90a5-3df665a38f66 | Bin 0 -> 1091 bytes .../6d10d702-7bd6-1452-90a5-3df665a38f66.meta.json | 1 + .../6f4da442-d31e-28f8-03af-797d16bbdd27 | Bin 0 -> 3638 bytes .../6f4da442-d31e-28f8-03af-797d16bbdd27.meta.json | 1 + .../70fdd651-6c50-b7bb-09ec-7e85da259173 | Bin 0 -> 1455 bytes .../70fdd651-6c50-b7bb-09ec-7e85da259173.meta.json | 1 + .../74793ce1-a918-a5eb-d3c0-2aadaff3c88c | Bin 0 -> 5430 bytes .../74793ce1-a918-a5eb-d3c0-2aadaff3c88c.meta.json | 1 + .../7bbe6c5c-fdb8-2845-a4f4-e1382e708a0e | Bin 0 -> 5430 bytes .../7bbe6c5c-fdb8-2845-a4f4-e1382e708a0e.meta.json | 1 + .../7efbed51-813c-581d-d8d3-f8758434e451 | Bin 0 -> 2584 bytes .../7efbed51-813c-581d-d8d3-f8758434e451.meta.json | 1 + .../84bb4962-e571-227a-9ef6-2ac5f2aac361 | Bin 0 -> 1743 bytes .../84bb4962-e571-227a-9ef6-2ac5f2aac361.meta.json | 1 + .../87ac4cde-f581-398b-1e32-eb4079183b36 | Bin 0 -> 1150 bytes .../87ac4cde-f581-398b-1e32-eb4079183b36.meta.json | 1 + .../8831ce10-b1e4-6eb4-4975-83c67457288e | Bin 0 -> 5430 bytes .../8831ce10-b1e4-6eb4-4975-83c67457288e.meta.json | 1 + .../890de5c4-0941-a116-473a-5d240e79497a | Bin 0 -> 530 bytes .../890de5c4-0941-a116-473a-5d240e79497a.meta.json | 1 + .../91a9672d-e945-8e1e-0996-aefdb0190716 | Bin 0 -> 318 bytes .../91a9672d-e945-8e1e-0996-aefdb0190716.meta.json | 1 + .../96327a73-c433-5eb4-a16d-b090cadfb80b | Bin 0 -> 1150 bytes .../96327a73-c433-5eb4-a16d-b090cadfb80b.meta.json | 1 + .../a06db97d-1210-ea2e-5474-0e2f7d295bfd | Bin 0 -> 2672 bytes .../a06db97d-1210-ea2e-5474-0e2f7d295bfd.meta.json | 1 + .../a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3 | Bin 0 -> 2799 bytes .../a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3.meta.json | 1 + .../a2c7d4e9-f770-51e1-0963-3c2c8401631d | Bin 0 -> 379 bytes .../a2c7d4e9-f770-51e1-0963-3c2c8401631d.meta.json | 1 + .../b64f09fd-52d1-c48e-af23-4ce918e7bf3b | Bin 0 -> 749 bytes .../b64f09fd-52d1-c48e-af23-4ce918e7bf3b.meta.json | 1 + .../b8ca5a94-8fff-27ad-6e00-96e244a32e21 | Bin 0 -> 1812 bytes .../b8ca5a94-8fff-27ad-6e00-96e244a32e21.meta.json | 1 + .../c411adc1-9661-4fb5-a4c1-8cfe74911943 | Bin 0 -> 1785 bytes .../c411adc1-9661-4fb5-a4c1-8cfe74911943.meta.json | 1 + .../cbf9e891-d079-2b28-5617-283450d463dd | Bin 0 -> 4286 bytes .../cbf9e891-d079-2b28-5617-283450d463dd.meta.json | 1 + .../d87f251c-3e12-a8bf-e2d0-afd43d36c5f9 | Bin 0 -> 159 bytes .../d87f251c-3e12-a8bf-e2d0-afd43d36c5f9.meta.json | 1 + .../e02f23df-8d48-2b1b-3b5c-6dd27302c61c | Bin 0 -> 2468 bytes .../e02f23df-8d48-2b1b-3b5c-6dd27302c61c.meta.json | 1 + .../e718e983-09aa-e8f6-b25f-cd4b395d4785 | Bin 0 -> 304 bytes .../e718e983-09aa-e8f6-b25f-cd4b395d4785.meta.json | 1 + .../e7547f62-187b-b641-d462-e54a3f813d9a | Bin 0 -> 1150 bytes .../e7547f62-187b-b641-d462-e54a3f813d9a.meta.json | 1 + .../f312610a-ebfb-a106-ea92-fd643c5d3636 | Bin 0 -> 5430 bytes .../f312610a-ebfb-a106-ea92-fd643c5d3636.meta.json | 1 + .../fa0fc42c-d91d-fca7-34eb-806ff46062dc | Bin 0 -> 5430 bytes .../fa0fc42c-d91d-fca7-34eb-806ff46062dc.meta.json | 1 + .../fca3e3ee-56cd-f474-dc31-307fd24a891d | Bin 0 -> 3638 bytes .../fca3e3ee-56cd-f474-dc31-307fd24a891d.meta.json | 1 + .../fed4f021-ff3e-942a-010e-afa43fda2136 | Bin 0 -> 5430 bytes .../fed4f021-ff3e-942a-010e-afa43fda2136.meta.json | 1 + .../dumps/main/search-config-overrides-v2.json | 32 + .../dumps/main/search-config-overrides.json | 33 + services/settings/dumps/main/search-config-v2.json | 7345 +++++ services/settings/dumps/main/search-config.json | 2400 ++ .../main/search-default-override-allowlist.json | 67 + .../settings/dumps/main/search-telemetry-v2.json | 657 + .../settings/dumps/main/sites-classification.json | 463 + services/settings/dumps/main/top-sites.json | 401 + .../main/translations-identification-models.json | 20 + .../settings/dumps/main/translations-models.json | 2723 ++ .../settings/dumps/main/translations-wasm.json | 78 + .../dumps/main/url-classifier-skip-urls.json | 20 + .../websites-with-shared-credential-backends.json | 744 + services/settings/dumps/moz.build | 16 + services/settings/dumps/readme.md | 12 + .../dumps/security-state/intermediates.json | 30569 +++++++++++++++++++ services/settings/dumps/security-state/moz.build | 11 + services/settings/dumps/security-state/onecrl.json | 24125 +++++++++++++++ services/settings/moz.build | 37 + services/settings/remote-settings.sys.mjs | 610 + services/settings/servicesSettings.manifest | 7 + .../settings/static-dumps/main/doh-config.json | 15 + .../settings/static-dumps/main/doh-providers.json | 23 + services/settings/static-dumps/main/moz.build | 11 + services/settings/static-dumps/moz.build | 7 + services/settings/static-dumps/readme.md | 14 + .../test/unit/test_attachments_downloader.js | 703 + .../65650a0f-7c22-4c10-9744-2d67e301f5f4.pem | 26 + .../dump-collection/filename-of-dump.txt | 1 + .../dump-collection/filename-of-dump.txt.meta.json | 10 + .../filename-without-content.txt.meta.json | 8 + .../dump-collection/filename-without-meta.txt | 1 + .../settings/test/unit/test_remote_settings.js | 1681 + .../unit/test_remote_settings_dump_lastmodified.js | 55 + .../test/unit/test_remote_settings_jexl_filters.js | 216 + .../test/unit/test_remote_settings_offline.js | 141 + .../test/unit/test_remote_settings_poll.js | 1386 + .../unit/test_remote_settings_recover_broken.js | 153 + .../unit/test_remote_settings_release_prefs.js | 206 + .../test/unit/test_remote_settings_signatures.js | 830 + .../collection_signing_ee.pem | 16 + .../collection_signing_ee.pem.certspec | 5 + .../collection_signing_int.pem | 19 + .../collection_signing_int.pem.certspec | 4 + .../test/unit/test_remote_settings_sync_history.js | 69 + .../test/unit/test_remote_settings_utils.js | 166 + .../unit/test_remote_settings_utils_telemetry.js | 90 + .../test/unit/test_remote_settings_worker.js | 138 + .../settings/test/unit/test_shutdown_handling.js | 139 + services/settings/test/unit/xpcshell.toml | 36 + services/sync/SyncComponents.manifest | 3 + services/sync/Weave.sys.mjs | 190 + services/sync/components.conf | 20 + services/sync/docs/engines.rst | 133 + services/sync/docs/external.rst | 8 + services/sync/docs/index.rst | 17 + services/sync/docs/overview.rst | 81 + services/sync/docs/payload-evolution.md | 168 + services/sync/docs/rust-engines.rst | 37 + services/sync/golden_gate/Cargo.toml | 25 + services/sync/golden_gate/src/error.rs | 71 + services/sync/golden_gate/src/ferry.rs | 74 + services/sync/golden_gate/src/lib.rs | 119 + services/sync/golden_gate/src/log.rs | 161 + services/sync/golden_gate/src/task.rs | 355 + services/sync/modules-testing/fakeservices.sys.mjs | 114 + services/sync/modules-testing/fxa_utils.sys.mjs | 55 + services/sync/modules-testing/rotaryengine.sys.mjs | 120 + services/sync/modules-testing/utils.sys.mjs | 319 + services/sync/modules/SyncDisconnect.sys.mjs | 235 + services/sync/modules/SyncedTabs.sys.mjs | 348 + services/sync/modules/UIState.sys.mjs | 285 + services/sync/modules/addonsreconciler.sys.mjs | 584 + services/sync/modules/addonutils.sys.mjs | 391 + services/sync/modules/bridged_engine.sys.mjs | 499 + services/sync/modules/collection_validator.sys.mjs | 267 + services/sync/modules/constants.sys.mjs | 133 + services/sync/modules/doctor.sys.mjs | 201 + services/sync/modules/engines.sys.mjs | 2274 ++ services/sync/modules/engines/addons.sys.mjs | 818 + services/sync/modules/engines/bookmarks.sys.mjs | 950 + services/sync/modules/engines/clients.sys.mjs | 1122 + .../sync/modules/engines/extension-storage.sys.mjs | 308 + services/sync/modules/engines/forms.sys.mjs | 298 + services/sync/modules/engines/history.sys.mjs | 654 + services/sync/modules/engines/passwords.sys.mjs | 546 + services/sync/modules/engines/prefs.sys.mjs | 503 + services/sync/modules/engines/tabs.sys.mjs | 625 + services/sync/modules/keys.sys.mjs | 166 + services/sync/modules/main.sys.mjs | 23 + services/sync/modules/policies.sys.mjs | 1055 + services/sync/modules/record.sys.mjs | 1335 + services/sync/modules/resource.sys.mjs | 292 + services/sync/modules/service.sys.mjs | 1643 + services/sync/modules/stages/declined.sys.mjs | 78 + services/sync/modules/stages/enginesync.sys.mjs | 412 + services/sync/modules/status.sys.mjs | 135 + services/sync/modules/sync_auth.sys.mjs | 655 + services/sync/modules/telemetry.sys.mjs | 1279 + services/sync/modules/util.sys.mjs | 780 + services/sync/moz.build | 72 + services/sync/tests/tps/.eslintrc.js | 28 + .../api/restartless-xpi@tests.mozilla.org.json | 21 + .../api/test-webext@quality.mozilla.org.json | 21 + services/sync/tests/tps/addons/restartless.xpi | Bin 0 -> 485 bytes services/sync/tests/tps/addons/webextension.xpi | Bin 0 -> 3412 bytes services/sync/tests/tps/all_tests.json | 34 + services/sync/tests/tps/test_addon_reconciling.js | 45 + .../sync/tests/tps/test_addon_restartless_xpi.js | 64 + services/sync/tests/tps/test_addon_webext_xpi.js | 65 + services/sync/tests/tps/test_addon_wipe.js | 31 + services/sync/tests/tps/test_addresses.js | 84 + services/sync/tests/tps/test_bookmark_conflict.js | 138 + .../tps/test_bookmarks_in_same_named_folder.js | 53 + services/sync/tests/tps/test_bug501528.js | 75 + services/sync/tests/tps/test_bug530717.js | 47 + services/sync/tests/tps/test_bug531489.js | 43 + services/sync/tests/tps/test_bug535326.js | 148 + services/sync/tests/tps/test_bug538298.js | 78 + services/sync/tests/tps/test_bug546807.js | 38 + services/sync/tests/tps/test_bug556509.js | 32 + services/sync/tests/tps/test_bug562515.js | 90 + services/sync/tests/tps/test_bug575423.js | 67 + services/sync/tests/tps/test_client_wipe.js | 142 + services/sync/tests/tps/test_creditcards.js | 62 + services/sync/tests/tps/test_existing_bookmarks.js | 80 + services/sync/tests/tps/test_extstorage.js | 154 + services/sync/tests/tps/test_formdata.js | 63 + services/sync/tests/tps/test_history.js | 129 + services/sync/tests/tps/test_history_collision.js | 98 + services/sync/tests/tps/test_passwords.js | 119 + services/sync/tests/tps/test_prefs.js | 35 + services/sync/tests/tps/test_privbrw_passwords.js | 105 + services/sync/tests/tps/test_privbrw_tabs.js | 86 + services/sync/tests/tps/test_special_tabs.js | 63 + services/sync/tests/tps/test_sync.js | 403 + services/sync/tests/tps/test_tabs.js | 42 + services/sync/tests/unit/addon1-search.json | 21 + services/sync/tests/unit/bootstrap1-search.json | 21 + services/sync/tests/unit/head_appinfo.js | 58 + .../sync/tests/unit/head_errorhandler_common.js | 195 + services/sync/tests/unit/head_helpers.js | 709 + services/sync/tests/unit/head_http_server.js | 1265 + services/sync/tests/unit/missing-sourceuri.json | 20 + services/sync/tests/unit/missing-xpi-search.json | 21 + services/sync/tests/unit/prefs_test_prefs_store.js | 47 + services/sync/tests/unit/rewrite-search.json | 21 + services/sync/tests/unit/sync_ping_schema.json | 262 + services/sync/tests/unit/systemaddon-search.json | 21 + services/sync/tests/unit/test_412.js | 60 + services/sync/tests/unit/test_addon_utils.js | 156 + services/sync/tests/unit/test_addons_engine.js | 277 + services/sync/tests/unit/test_addons_reconciler.js | 209 + services/sync/tests/unit/test_addons_store.js | 750 + services/sync/tests/unit/test_addons_tracker.js | 174 + services/sync/tests/unit/test_addons_validator.js | 65 + .../sync/tests/unit/test_bookmark_batch_fail.js | 25 + .../tests/unit/test_bookmark_decline_undecline.js | 48 + services/sync/tests/unit/test_bookmark_engine.js | 1555 + services/sync/tests/unit/test_bookmark_order.js | 586 + .../unit/test_bookmark_places_query_rewriting.js | 57 + services/sync/tests/unit/test_bookmark_record.js | 64 + services/sync/tests/unit/test_bookmark_store.js | 425 + services/sync/tests/unit/test_bookmark_tracker.js | 1275 + services/sync/tests/unit/test_bridged_engine.js | 248 + services/sync/tests/unit/test_clients_engine.js | 2108 ++ services/sync/tests/unit/test_clients_escape.js | 57 + .../sync/tests/unit/test_collection_getBatched.js | 187 + .../sync/tests/unit/test_collections_recovery.js | 95 + services/sync/tests/unit/test_corrupt_keys.js | 248 + services/sync/tests/unit/test_declined.js | 194 + .../sync/tests/unit/test_disconnect_shutdown.js | 101 + services/sync/tests/unit/test_engine.js | 246 + services/sync/tests/unit/test_engine_abort.js | 79 + .../tests/unit/test_engine_changes_during_sync.js | 611 + services/sync/tests/unit/test_enginemanager.js | 232 + services/sync/tests/unit/test_errorhandler_1.js | 341 + services/sync/tests/unit/test_errorhandler_2.js | 550 + .../sync/tests/unit/test_errorhandler_filelog.js | 473 + .../test_errorhandler_sync_checkServerError.js | 294 + .../tests/unit/test_extension_storage_engine.js | 275 + .../unit/test_extension_storage_engine_kinto.js | 136 + .../unit/test_extension_storage_migration_telem.js | 81 + .../unit/test_extension_storage_tracker_kinto.js | 44 + services/sync/tests/unit/test_form_validator.js | 86 + services/sync/tests/unit/test_forms_store.js | 176 + services/sync/tests/unit/test_forms_tracker.js | 78 + .../sync/tests/unit/test_fxa_node_reassignment.js | 399 + .../sync/tests/unit/test_fxa_service_cluster.js | 58 + services/sync/tests/unit/test_history_engine.js | 429 + services/sync/tests/unit/test_history_store.js | 570 + services/sync/tests/unit/test_history_tracker.js | 251 + services/sync/tests/unit/test_hmac_error.js | 250 + services/sync/tests/unit/test_httpd_sync_server.js | 250 + services/sync/tests/unit/test_interval_triggers.js | 472 + services/sync/tests/unit/test_keys.js | 242 + services/sync/tests/unit/test_load_modules.js | 59 + services/sync/tests/unit/test_node_reassignment.js | 523 + services/sync/tests/unit/test_password_engine.js | 1257 + services/sync/tests/unit/test_password_store.js | 398 + services/sync/tests/unit/test_password_tracker.js | 248 + .../sync/tests/unit/test_password_validator.js | 176 + services/sync/tests/unit/test_postqueue.js | 985 + services/sync/tests/unit/test_prefs_engine.js | 134 + services/sync/tests/unit/test_prefs_store.js | 391 + services/sync/tests/unit/test_prefs_tracker.js | 93 + services/sync/tests/unit/test_records_crypto.js | 189 + services/sync/tests/unit/test_records_wbo.js | 85 + services/sync/tests/unit/test_resource.js | 554 + services/sync/tests/unit/test_resource_header.js | 63 + services/sync/tests/unit/test_resource_ua.js | 96 + services/sync/tests/unit/test_score_triggers.js | 151 + .../sync/tests/unit/test_service_attributes.js | 92 + services/sync/tests/unit/test_service_cluster.js | 61 + .../sync/tests/unit/test_service_detect_upgrade.js | 274 + services/sync/tests/unit/test_service_login.js | 224 + services/sync/tests/unit/test_service_startOver.js | 91 + services/sync/tests/unit/test_service_startup.js | 60 + services/sync/tests/unit/test_service_sync_401.js | 90 + .../sync/tests/unit/test_service_sync_locked.js | 47 + .../tests/unit/test_service_sync_remoteSetup.js | 241 + .../sync/tests/unit/test_service_sync_specified.js | 150 + .../unit/test_service_sync_updateEnabledEngines.js | 587 + .../sync/tests/unit/test_service_verifyLogin.js | 118 + .../sync/tests/unit/test_service_wipeClient.js | 78 + .../sync/tests/unit/test_service_wipeServer.js | 240 + services/sync/tests/unit/test_status.js | 83 + services/sync/tests/unit/test_status_checkSetup.js | 26 + services/sync/tests/unit/test_sync_auth_manager.js | 1027 + services/sync/tests/unit/test_syncedtabs.js | 342 + services/sync/tests/unit/test_syncengine.js | 302 + services/sync/tests/unit/test_syncengine_sync.js | 1781 ++ services/sync/tests/unit/test_syncscheduler.js | 1195 + services/sync/tests/unit/test_tab_engine.js | 226 + services/sync/tests/unit/test_tab_provider.js | 64 + services/sync/tests/unit/test_tab_quickwrite.js | 204 + services/sync/tests/unit/test_tab_tracker.js | 371 + services/sync/tests/unit/test_telemetry.js | 1462 + .../sync/tests/unit/test_tracker_addChanged.js | 59 + services/sync/tests/unit/test_uistate.js | 324 + services/sync/tests/unit/test_utils_catch.js | 119 + services/sync/tests/unit/test_utils_deepEquals.js | 51 + services/sync/tests/unit/test_utils_deferGetSet.js | 50 + services/sync/tests/unit/test_utils_json.js | 95 + services/sync/tests/unit/test_utils_keyEncoding.js | 23 + services/sync/tests/unit/test_utils_lock.js | 76 + services/sync/tests/unit/test_utils_makeGUID.js | 44 + services/sync/tests/unit/test_utils_notify.js | 97 + services/sync/tests/unit/test_utils_passphrase.js | 45 + services/sync/tests/unit/xpcshell.toml | 304 + services/sync/tps/extensions/tps/api.js | 77 + services/sync/tps/extensions/tps/manifest.json | 23 + .../tps/resource/auth/fxaccounts.sys.mjs | 209 + .../tps/extensions/tps/resource/logger.sys.mjs | 168 + .../extensions/tps/resource/modules/addons.sys.mjs | 93 + .../tps/resource/modules/bookmarkValidator.sys.mjs | 1063 + .../tps/resource/modules/bookmarks.sys.mjs | 833 + .../tps/resource/modules/formautofill.sys.mjs | 128 + .../extensions/tps/resource/modules/forms.sys.mjs | 205 + .../tps/resource/modules/history.sys.mjs | 158 + .../tps/resource/modules/passwords.sys.mjs | 187 + .../extensions/tps/resource/modules/prefs.sys.mjs | 122 + .../extensions/tps/resource/modules/tabs.sys.mjs | 92 + .../tps/resource/modules/windows.sys.mjs | 32 + .../sync/tps/extensions/tps/resource/quit.sys.mjs | 38 + .../sync/tps/extensions/tps/resource/tps.sys.mjs | 1583 + services/sync/tps/extensions/tps/schema.json | 1 + 508 files changed, 224592 insertions(+) create mode 100644 services/automation/AutomationComponents.manifest create mode 100644 services/automation/ServicesAutomation.sys.mjs create mode 100644 services/automation/moz.build create mode 100644 services/common/app_services_logger/AppServicesLoggerComponents.h create mode 100644 services/common/app_services_logger/Cargo.toml create mode 100644 services/common/app_services_logger/components.conf create mode 100644 services/common/app_services_logger/src/lib.rs create mode 100644 services/common/async.sys.mjs create mode 100644 services/common/hawkclient.sys.mjs create mode 100644 services/common/hawkrequest.sys.mjs create mode 100644 services/common/kinto-http-client.sys.mjs create mode 100644 services/common/kinto-offline-client.js create mode 100644 services/common/kinto-storage-adapter.sys.mjs create mode 100644 services/common/logmanager.sys.mjs create mode 100644 services/common/modules-testing/logging.sys.mjs create mode 100644 services/common/moz.build create mode 100644 services/common/observers.sys.mjs create mode 100644 services/common/rest.sys.mjs create mode 100644 services/common/servicesComponents.manifest create mode 100644 services/common/tests/moz.build create mode 100644 services/common/tests/unit/head_global.js create mode 100644 services/common/tests/unit/head_helpers.js create mode 100644 services/common/tests/unit/head_http.js create mode 100644 services/common/tests/unit/moz.build create mode 100644 services/common/tests/unit/test_async_chain.js create mode 100644 services/common/tests/unit/test_async_foreach.js create mode 100644 services/common/tests/unit/test_hawkclient.js create mode 100644 services/common/tests/unit/test_hawkrequest.js create mode 100644 services/common/tests/unit/test_kinto.js create mode 100644 services/common/tests/unit/test_load_modules.js create mode 100644 services/common/tests/unit/test_logmanager.js create mode 100644 services/common/tests/unit/test_observers.js create mode 100644 services/common/tests/unit/test_restrequest.js create mode 100644 services/common/tests/unit/test_storage_adapter.js create mode 100644 services/common/tests/unit/test_storage_adapter/empty.sqlite create mode 100644 services/common/tests/unit/test_storage_adapter/v1.sqlite create mode 100644 services/common/tests/unit/test_storage_adapter_shutdown.js create mode 100644 services/common/tests/unit/test_tokenauthenticatedrequest.js create mode 100644 services/common/tests/unit/test_tokenserverclient.js create mode 100644 services/common/tests/unit/test_uptake_telemetry.js create mode 100644 services/common/tests/unit/test_utils_atob.js create mode 100644 services/common/tests/unit/test_utils_convert_string.js create mode 100644 services/common/tests/unit/test_utils_dateprefs.js create mode 100644 services/common/tests/unit/test_utils_encodeBase32.js create mode 100644 services/common/tests/unit/test_utils_encodeBase64URL.js create mode 100644 services/common/tests/unit/test_utils_ensureMillisecondsTimestamp.js create mode 100644 services/common/tests/unit/test_utils_makeURI.js create mode 100644 services/common/tests/unit/test_utils_namedTimer.js create mode 100644 services/common/tests/unit/test_utils_sets.js create mode 100644 services/common/tests/unit/test_utils_utf8.js create mode 100644 services/common/tests/unit/test_utils_uuid.js create mode 100644 services/common/tests/unit/xpcshell.toml create mode 100644 services/common/tokenserverclient.sys.mjs create mode 100644 services/common/uptake-telemetry.sys.mjs create mode 100644 services/common/utils.sys.mjs create mode 100644 services/crypto/cryptoComponents.manifest create mode 100644 services/crypto/modules/WeaveCrypto.sys.mjs create mode 100644 services/crypto/modules/jwcrypto.sys.mjs create mode 100644 services/crypto/modules/utils.sys.mjs create mode 100644 services/crypto/moz.build create mode 100644 services/crypto/tests/unit/head_helpers.js create mode 100644 services/crypto/tests/unit/test_crypto_crypt.js create mode 100644 services/crypto/tests/unit/test_crypto_random.js create mode 100644 services/crypto/tests/unit/test_jwcrypto.js create mode 100644 services/crypto/tests/unit/test_load_modules.js create mode 100644 services/crypto/tests/unit/test_utils_hawk.js create mode 100644 services/crypto/tests/unit/test_utils_httpmac.js create mode 100644 services/crypto/tests/unit/xpcshell.toml create mode 100644 services/docs/index.rst create mode 100644 services/docs/moz.build create mode 100644 services/fxaccounts/Credentials.sys.mjs create mode 100644 services/fxaccounts/FxAccounts.sys.mjs create mode 100644 services/fxaccounts/FxAccountsClient.sys.mjs create mode 100644 services/fxaccounts/FxAccountsCommands.sys.mjs create mode 100644 services/fxaccounts/FxAccountsCommon.sys.mjs create mode 100644 services/fxaccounts/FxAccountsConfig.sys.mjs create mode 100644 services/fxaccounts/FxAccountsDevice.sys.mjs create mode 100644 services/fxaccounts/FxAccountsKeys.sys.mjs create mode 100644 services/fxaccounts/FxAccountsOAuth.sys.mjs create mode 100644 services/fxaccounts/FxAccountsPairing.sys.mjs create mode 100644 services/fxaccounts/FxAccountsPairingChannel.sys.mjs create mode 100644 services/fxaccounts/FxAccountsProfile.sys.mjs create mode 100644 services/fxaccounts/FxAccountsProfileClient.sys.mjs create mode 100644 services/fxaccounts/FxAccountsPush.sys.mjs create mode 100644 services/fxaccounts/FxAccountsStorage.sys.mjs create mode 100644 services/fxaccounts/FxAccountsTelemetry.sys.mjs create mode 100644 services/fxaccounts/FxAccountsWebChannel.sys.mjs create mode 100644 services/fxaccounts/components.conf create mode 100644 services/fxaccounts/moz.build create mode 100644 services/fxaccounts/tests/browser/browser.toml create mode 100644 services/fxaccounts/tests/browser/browser_device_connected.js create mode 100644 services/fxaccounts/tests/browser/browser_verify_login.js create mode 100644 services/fxaccounts/tests/browser/head.js create mode 100644 services/fxaccounts/tests/mochitest/chrome.toml create mode 100644 services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs create mode 100644 services/fxaccounts/tests/mochitest/test_invalidEmailCase.html create mode 100644 services/fxaccounts/tests/xpcshell/head.js create mode 100644 services/fxaccounts/tests/xpcshell/test_accounts.js create mode 100644 services/fxaccounts/tests/xpcshell/test_accounts_config.js create mode 100644 services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js create mode 100644 services/fxaccounts/tests/xpcshell/test_client.js create mode 100644 services/fxaccounts/tests/xpcshell/test_commands.js create mode 100644 services/fxaccounts/tests/xpcshell/test_credentials.js create mode 100644 services/fxaccounts/tests/xpcshell/test_device.js create mode 100644 services/fxaccounts/tests/xpcshell/test_keys.js create mode 100644 services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js create mode 100644 services/fxaccounts/tests/xpcshell/test_oauth_flow.js create mode 100644 services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js create mode 100644 services/fxaccounts/tests/xpcshell/test_oauth_tokens.js create mode 100644 services/fxaccounts/tests/xpcshell/test_pairing.js create mode 100644 services/fxaccounts/tests/xpcshell/test_profile.js create mode 100644 services/fxaccounts/tests/xpcshell/test_profile_client.js create mode 100644 services/fxaccounts/tests/xpcshell/test_push_service.js create mode 100644 services/fxaccounts/tests/xpcshell/test_storage_manager.js create mode 100644 services/fxaccounts/tests/xpcshell/test_telemetry.js create mode 100644 services/fxaccounts/tests/xpcshell/test_web_channel.js create mode 100644 services/fxaccounts/tests/xpcshell/xpcshell.toml create mode 100644 services/interfaces/moz.build create mode 100644 services/interfaces/mozIAppServicesLogger.idl create mode 100644 services/interfaces/mozIBridgedSyncEngine.idl create mode 100644 services/interfaces/mozIInterruptible.idl create mode 100644 services/interfaces/mozIServicesLogSink.idl create mode 100644 services/moz.build create mode 100644 services/settings/Attachments.sys.mjs create mode 100644 services/settings/Database.sys.mjs create mode 100644 services/settings/IDBHelpers.sys.mjs create mode 100644 services/settings/RemoteSettings.worker.mjs create mode 100644 services/settings/RemoteSettingsClient.sys.mjs create mode 100644 services/settings/RemoteSettingsComponents.sys.mjs create mode 100644 services/settings/RemoteSettingsWorker.sys.mjs create mode 100644 services/settings/SharedUtils.sys.mjs create mode 100644 services/settings/SyncHistory.sys.mjs create mode 100644 services/settings/Utils.sys.mjs create mode 100644 services/settings/components.conf create mode 100644 services/settings/docs/index.rst create mode 100644 services/settings/docs/synchronization-flow.svg create mode 100644 services/settings/dumps/blocklists/addons-bloomfilters.json create mode 100644 services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin create mode 100644 services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin.meta.json create mode 100644 services/settings/dumps/blocklists/addons.json create mode 100644 services/settings/dumps/blocklists/gfx.json create mode 100644 services/settings/dumps/blocklists/moz.build create mode 100644 services/settings/dumps/blocklists/plugins.json create mode 100644 services/settings/dumps/gen_last_modified.py create mode 100644 services/settings/dumps/main/anti-tracking-url-decoration.json create mode 100644 services/settings/dumps/main/cookie-banner-rules-list.json create mode 100644 services/settings/dumps/main/devtools-compatibility-browsers.json create mode 100644 services/settings/dumps/main/devtools-devices.json create mode 100644 services/settings/dumps/main/example.json create mode 100644 services/settings/dumps/main/hijack-blocklists.json create mode 100644 services/settings/dumps/main/language-dictionaries.json create mode 100644 services/settings/dumps/main/moz.build create mode 100644 services/settings/dumps/main/password-recipes.json create mode 100644 services/settings/dumps/main/password-rules.json create mode 100644 services/settings/dumps/main/search-config-icons.json create mode 100644 services/settings/dumps/main/search-config-icons/001500a9-1a6c-3f5a-ba15-a5f5a075d256 create mode 100644 services/settings/dumps/main/search-config-icons/001500a9-1a6c-3f5a-ba15-a5f5a075d256.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/06cf7432-efd7-f244-927b-5e423005e1ea create mode 100644 services/settings/dumps/main/search-config-icons/06cf7432-efd7-f244-927b-5e423005e1ea.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/0d7668a8-c3f4-cfee-cbc8-536511528937 create mode 100644 services/settings/dumps/main/search-config-icons/0d7668a8-c3f4-cfee-cbc8-536511528937.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/0eec5640-6fde-d6fe-322a-c72c6d5bd5a2 create mode 100644 services/settings/dumps/main/search-config-icons/0eec5640-6fde-d6fe-322a-c72c6d5bd5a2.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/101ce01d-2691-b729-7f16-9d389803384b create mode 100644 services/settings/dumps/main/search-config-icons/101ce01d-2691-b729-7f16-9d389803384b.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/177aba42-9bed-4078-e36b-580e8794cd7f create mode 100644 services/settings/dumps/main/search-config-icons/177aba42-9bed-4078-e36b-580e8794cd7f.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/25de0352-aabb-d31f-15f7-bf9299fb004c create mode 100644 services/settings/dumps/main/search-config-icons/25de0352-aabb-d31f-15f7-bf9299fb004c.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335 create mode 100644 services/settings/dumps/main/search-config-icons/2bbe48f4-d3b8-c9e0-86e3-a54c37ec3335.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/2e835b0e-9709-d1bb-9725-87f59f3445ca create mode 100644 services/settings/dumps/main/search-config-icons/2e835b0e-9709-d1bb-9725-87f59f3445ca.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/32d26d19-aeb0-5c01-32e8-f8970be9246f create mode 100644 services/settings/dumps/main/search-config-icons/32d26d19-aeb0-5c01-32e8-f8970be9246f.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/47da97b5-600f-c450-fd15-a52bb2169c11 create mode 100644 services/settings/dumps/main/search-config-icons/47da97b5-600f-c450-fd15-a52bb2169c11.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/4e271681-3e0f-91ac-9750-03f665efc171 create mode 100644 services/settings/dumps/main/search-config-icons/4e271681-3e0f-91ac-9750-03f665efc171.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/50f6171f-8e7a-b41b-862e-f97397038fb2 create mode 100644 services/settings/dumps/main/search-config-icons/50f6171f-8e7a-b41b-862e-f97397038fb2.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/5ded611d-44b2-dc46-fd67-fb116888d75d create mode 100644 services/settings/dumps/main/search-config-icons/5ded611d-44b2-dc46-fd67-fb116888d75d.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41 create mode 100644 services/settings/dumps/main/search-config-icons/5e03d6f4-6ee9-8bc8-cf22-7a5f2cf55c41.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/6a83583a-f0ba-fd39-2fdb-fd2b6990ea3b create mode 100644 services/settings/dumps/main/search-config-icons/6a83583a-f0ba-fd39-2fdb-fd2b6990ea3b.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/6d10d702-7bd6-1452-90a5-3df665a38f66 create mode 100644 services/settings/dumps/main/search-config-icons/6d10d702-7bd6-1452-90a5-3df665a38f66.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/6f4da442-d31e-28f8-03af-797d16bbdd27 create mode 100644 services/settings/dumps/main/search-config-icons/6f4da442-d31e-28f8-03af-797d16bbdd27.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/70fdd651-6c50-b7bb-09ec-7e85da259173 create mode 100644 services/settings/dumps/main/search-config-icons/70fdd651-6c50-b7bb-09ec-7e85da259173.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/74793ce1-a918-a5eb-d3c0-2aadaff3c88c create mode 100644 services/settings/dumps/main/search-config-icons/74793ce1-a918-a5eb-d3c0-2aadaff3c88c.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/7bbe6c5c-fdb8-2845-a4f4-e1382e708a0e create mode 100644 services/settings/dumps/main/search-config-icons/7bbe6c5c-fdb8-2845-a4f4-e1382e708a0e.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/7efbed51-813c-581d-d8d3-f8758434e451 create mode 100644 services/settings/dumps/main/search-config-icons/7efbed51-813c-581d-d8d3-f8758434e451.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/84bb4962-e571-227a-9ef6-2ac5f2aac361 create mode 100644 services/settings/dumps/main/search-config-icons/84bb4962-e571-227a-9ef6-2ac5f2aac361.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/87ac4cde-f581-398b-1e32-eb4079183b36 create mode 100644 services/settings/dumps/main/search-config-icons/87ac4cde-f581-398b-1e32-eb4079183b36.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/8831ce10-b1e4-6eb4-4975-83c67457288e create mode 100644 services/settings/dumps/main/search-config-icons/8831ce10-b1e4-6eb4-4975-83c67457288e.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/890de5c4-0941-a116-473a-5d240e79497a create mode 100644 services/settings/dumps/main/search-config-icons/890de5c4-0941-a116-473a-5d240e79497a.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/91a9672d-e945-8e1e-0996-aefdb0190716 create mode 100644 services/settings/dumps/main/search-config-icons/91a9672d-e945-8e1e-0996-aefdb0190716.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b create mode 100644 services/settings/dumps/main/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/a06db97d-1210-ea2e-5474-0e2f7d295bfd create mode 100644 services/settings/dumps/main/search-config-icons/a06db97d-1210-ea2e-5474-0e2f7d295bfd.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3 create mode 100644 services/settings/dumps/main/search-config-icons/a06dc3fd-4bdb-41f3-2ebc-4cbed06a9bd3.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/a2c7d4e9-f770-51e1-0963-3c2c8401631d create mode 100644 services/settings/dumps/main/search-config-icons/a2c7d4e9-f770-51e1-0963-3c2c8401631d.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/b64f09fd-52d1-c48e-af23-4ce918e7bf3b create mode 100644 services/settings/dumps/main/search-config-icons/b64f09fd-52d1-c48e-af23-4ce918e7bf3b.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/b8ca5a94-8fff-27ad-6e00-96e244a32e21 create mode 100644 services/settings/dumps/main/search-config-icons/b8ca5a94-8fff-27ad-6e00-96e244a32e21.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/c411adc1-9661-4fb5-a4c1-8cfe74911943 create mode 100644 services/settings/dumps/main/search-config-icons/c411adc1-9661-4fb5-a4c1-8cfe74911943.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/cbf9e891-d079-2b28-5617-283450d463dd create mode 100644 services/settings/dumps/main/search-config-icons/cbf9e891-d079-2b28-5617-283450d463dd.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/d87f251c-3e12-a8bf-e2d0-afd43d36c5f9 create mode 100644 services/settings/dumps/main/search-config-icons/d87f251c-3e12-a8bf-e2d0-afd43d36c5f9.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/e02f23df-8d48-2b1b-3b5c-6dd27302c61c create mode 100644 services/settings/dumps/main/search-config-icons/e02f23df-8d48-2b1b-3b5c-6dd27302c61c.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/e718e983-09aa-e8f6-b25f-cd4b395d4785 create mode 100644 services/settings/dumps/main/search-config-icons/e718e983-09aa-e8f6-b25f-cd4b395d4785.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/e7547f62-187b-b641-d462-e54a3f813d9a create mode 100644 services/settings/dumps/main/search-config-icons/e7547f62-187b-b641-d462-e54a3f813d9a.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/f312610a-ebfb-a106-ea92-fd643c5d3636 create mode 100644 services/settings/dumps/main/search-config-icons/f312610a-ebfb-a106-ea92-fd643c5d3636.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/fa0fc42c-d91d-fca7-34eb-806ff46062dc create mode 100644 services/settings/dumps/main/search-config-icons/fa0fc42c-d91d-fca7-34eb-806ff46062dc.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/fca3e3ee-56cd-f474-dc31-307fd24a891d create mode 100644 services/settings/dumps/main/search-config-icons/fca3e3ee-56cd-f474-dc31-307fd24a891d.meta.json create mode 100644 services/settings/dumps/main/search-config-icons/fed4f021-ff3e-942a-010e-afa43fda2136 create mode 100644 services/settings/dumps/main/search-config-icons/fed4f021-ff3e-942a-010e-afa43fda2136.meta.json create mode 100644 services/settings/dumps/main/search-config-overrides-v2.json create mode 100644 services/settings/dumps/main/search-config-overrides.json create mode 100644 services/settings/dumps/main/search-config-v2.json create mode 100644 services/settings/dumps/main/search-config.json create mode 100644 services/settings/dumps/main/search-default-override-allowlist.json create mode 100644 services/settings/dumps/main/search-telemetry-v2.json create mode 100644 services/settings/dumps/main/sites-classification.json create mode 100644 services/settings/dumps/main/top-sites.json create mode 100644 services/settings/dumps/main/translations-identification-models.json create mode 100644 services/settings/dumps/main/translations-models.json create mode 100644 services/settings/dumps/main/translations-wasm.json create mode 100644 services/settings/dumps/main/url-classifier-skip-urls.json create mode 100644 services/settings/dumps/main/websites-with-shared-credential-backends.json create mode 100644 services/settings/dumps/moz.build create mode 100644 services/settings/dumps/readme.md create mode 100644 services/settings/dumps/security-state/intermediates.json create mode 100644 services/settings/dumps/security-state/moz.build create mode 100644 services/settings/dumps/security-state/onecrl.json create mode 100644 services/settings/moz.build create mode 100644 services/settings/remote-settings.sys.mjs create mode 100644 services/settings/servicesSettings.manifest create mode 100644 services/settings/static-dumps/main/doh-config.json create mode 100644 services/settings/static-dumps/main/doh-providers.json create mode 100644 services/settings/static-dumps/main/moz.build create mode 100644 services/settings/static-dumps/moz.build create mode 100644 services/settings/static-dumps/readme.md create mode 100644 services/settings/test/unit/test_attachments_downloader.js create mode 100644 services/settings/test/unit/test_attachments_downloader/65650a0f-7c22-4c10-9744-2d67e301f5f4.pem create mode 100644 services/settings/test/unit/test_attachments_downloader/settings/dump-bucket/dump-collection/filename-of-dump.txt create mode 100644 services/settings/test/unit/test_attachments_downloader/settings/dump-bucket/dump-collection/filename-of-dump.txt.meta.json create mode 100644 services/settings/test/unit/test_attachments_downloader/settings/dump-bucket/dump-collection/filename-without-content.txt.meta.json create mode 100644 services/settings/test/unit/test_attachments_downloader/settings/dump-bucket/dump-collection/filename-without-meta.txt create mode 100644 services/settings/test/unit/test_remote_settings.js create mode 100644 services/settings/test/unit/test_remote_settings_dump_lastmodified.js create mode 100644 services/settings/test/unit/test_remote_settings_jexl_filters.js create mode 100644 services/settings/test/unit/test_remote_settings_offline.js create mode 100644 services/settings/test/unit/test_remote_settings_poll.js create mode 100644 services/settings/test/unit/test_remote_settings_recover_broken.js create mode 100644 services/settings/test/unit/test_remote_settings_release_prefs.js create mode 100644 services/settings/test/unit/test_remote_settings_signatures.js create mode 100644 services/settings/test/unit/test_remote_settings_signatures/collection_signing_ee.pem create mode 100644 services/settings/test/unit/test_remote_settings_signatures/collection_signing_ee.pem.certspec create mode 100644 services/settings/test/unit/test_remote_settings_signatures/collection_signing_int.pem create mode 100644 services/settings/test/unit/test_remote_settings_signatures/collection_signing_int.pem.certspec create mode 100644 services/settings/test/unit/test_remote_settings_sync_history.js create mode 100644 services/settings/test/unit/test_remote_settings_utils.js create mode 100644 services/settings/test/unit/test_remote_settings_utils_telemetry.js create mode 100644 services/settings/test/unit/test_remote_settings_worker.js create mode 100644 services/settings/test/unit/test_shutdown_handling.js create mode 100644 services/settings/test/unit/xpcshell.toml create mode 100644 services/sync/SyncComponents.manifest create mode 100644 services/sync/Weave.sys.mjs create mode 100644 services/sync/components.conf create mode 100644 services/sync/docs/engines.rst create mode 100644 services/sync/docs/external.rst create mode 100644 services/sync/docs/index.rst create mode 100644 services/sync/docs/overview.rst create mode 100644 services/sync/docs/payload-evolution.md create mode 100644 services/sync/docs/rust-engines.rst create mode 100644 services/sync/golden_gate/Cargo.toml create mode 100644 services/sync/golden_gate/src/error.rs create mode 100644 services/sync/golden_gate/src/ferry.rs create mode 100644 services/sync/golden_gate/src/lib.rs create mode 100644 services/sync/golden_gate/src/log.rs create mode 100644 services/sync/golden_gate/src/task.rs create mode 100644 services/sync/modules-testing/fakeservices.sys.mjs create mode 100644 services/sync/modules-testing/fxa_utils.sys.mjs create mode 100644 services/sync/modules-testing/rotaryengine.sys.mjs create mode 100644 services/sync/modules-testing/utils.sys.mjs create mode 100644 services/sync/modules/SyncDisconnect.sys.mjs create mode 100644 services/sync/modules/SyncedTabs.sys.mjs create mode 100644 services/sync/modules/UIState.sys.mjs create mode 100644 services/sync/modules/addonsreconciler.sys.mjs create mode 100644 services/sync/modules/addonutils.sys.mjs create mode 100644 services/sync/modules/bridged_engine.sys.mjs create mode 100644 services/sync/modules/collection_validator.sys.mjs create mode 100644 services/sync/modules/constants.sys.mjs create mode 100644 services/sync/modules/doctor.sys.mjs create mode 100644 services/sync/modules/engines.sys.mjs create mode 100644 services/sync/modules/engines/addons.sys.mjs create mode 100644 services/sync/modules/engines/bookmarks.sys.mjs create mode 100644 services/sync/modules/engines/clients.sys.mjs create mode 100644 services/sync/modules/engines/extension-storage.sys.mjs create mode 100644 services/sync/modules/engines/forms.sys.mjs create mode 100644 services/sync/modules/engines/history.sys.mjs create mode 100644 services/sync/modules/engines/passwords.sys.mjs create mode 100644 services/sync/modules/engines/prefs.sys.mjs create mode 100644 services/sync/modules/engines/tabs.sys.mjs create mode 100644 services/sync/modules/keys.sys.mjs create mode 100644 services/sync/modules/main.sys.mjs create mode 100644 services/sync/modules/policies.sys.mjs create mode 100644 services/sync/modules/record.sys.mjs create mode 100644 services/sync/modules/resource.sys.mjs create mode 100644 services/sync/modules/service.sys.mjs create mode 100644 services/sync/modules/stages/declined.sys.mjs create mode 100644 services/sync/modules/stages/enginesync.sys.mjs create mode 100644 services/sync/modules/status.sys.mjs create mode 100644 services/sync/modules/sync_auth.sys.mjs create mode 100644 services/sync/modules/telemetry.sys.mjs create mode 100644 services/sync/modules/util.sys.mjs create mode 100644 services/sync/moz.build create mode 100644 services/sync/tests/tps/.eslintrc.js create mode 100644 services/sync/tests/tps/addons/api/restartless-xpi@tests.mozilla.org.json create mode 100644 services/sync/tests/tps/addons/api/test-webext@quality.mozilla.org.json create mode 100644 services/sync/tests/tps/addons/restartless.xpi create mode 100644 services/sync/tests/tps/addons/webextension.xpi create mode 100644 services/sync/tests/tps/all_tests.json create mode 100644 services/sync/tests/tps/test_addon_reconciling.js create mode 100644 services/sync/tests/tps/test_addon_restartless_xpi.js create mode 100644 services/sync/tests/tps/test_addon_webext_xpi.js create mode 100644 services/sync/tests/tps/test_addon_wipe.js create mode 100644 services/sync/tests/tps/test_addresses.js create mode 100644 services/sync/tests/tps/test_bookmark_conflict.js create mode 100644 services/sync/tests/tps/test_bookmarks_in_same_named_folder.js create mode 100644 services/sync/tests/tps/test_bug501528.js create mode 100644 services/sync/tests/tps/test_bug530717.js create mode 100644 services/sync/tests/tps/test_bug531489.js create mode 100644 services/sync/tests/tps/test_bug535326.js create mode 100644 services/sync/tests/tps/test_bug538298.js create mode 100644 services/sync/tests/tps/test_bug546807.js create mode 100644 services/sync/tests/tps/test_bug556509.js create mode 100644 services/sync/tests/tps/test_bug562515.js create mode 100644 services/sync/tests/tps/test_bug575423.js create mode 100644 services/sync/tests/tps/test_client_wipe.js create mode 100644 services/sync/tests/tps/test_creditcards.js create mode 100644 services/sync/tests/tps/test_existing_bookmarks.js create mode 100644 services/sync/tests/tps/test_extstorage.js create mode 100644 services/sync/tests/tps/test_formdata.js create mode 100644 services/sync/tests/tps/test_history.js create mode 100644 services/sync/tests/tps/test_history_collision.js create mode 100644 services/sync/tests/tps/test_passwords.js create mode 100644 services/sync/tests/tps/test_prefs.js create mode 100644 services/sync/tests/tps/test_privbrw_passwords.js create mode 100644 services/sync/tests/tps/test_privbrw_tabs.js create mode 100644 services/sync/tests/tps/test_special_tabs.js create mode 100644 services/sync/tests/tps/test_sync.js create mode 100644 services/sync/tests/tps/test_tabs.js create mode 100644 services/sync/tests/unit/addon1-search.json create mode 100644 services/sync/tests/unit/bootstrap1-search.json create mode 100644 services/sync/tests/unit/head_appinfo.js create mode 100644 services/sync/tests/unit/head_errorhandler_common.js create mode 100644 services/sync/tests/unit/head_helpers.js create mode 100644 services/sync/tests/unit/head_http_server.js create mode 100644 services/sync/tests/unit/missing-sourceuri.json create mode 100644 services/sync/tests/unit/missing-xpi-search.json create mode 100644 services/sync/tests/unit/prefs_test_prefs_store.js create mode 100644 services/sync/tests/unit/rewrite-search.json create mode 100644 services/sync/tests/unit/sync_ping_schema.json create mode 100644 services/sync/tests/unit/systemaddon-search.json create mode 100644 services/sync/tests/unit/test_412.js create mode 100644 services/sync/tests/unit/test_addon_utils.js create mode 100644 services/sync/tests/unit/test_addons_engine.js create mode 100644 services/sync/tests/unit/test_addons_reconciler.js create mode 100644 services/sync/tests/unit/test_addons_store.js create mode 100644 services/sync/tests/unit/test_addons_tracker.js create mode 100644 services/sync/tests/unit/test_addons_validator.js create mode 100644 services/sync/tests/unit/test_bookmark_batch_fail.js create mode 100644 services/sync/tests/unit/test_bookmark_decline_undecline.js create mode 100644 services/sync/tests/unit/test_bookmark_engine.js create mode 100644 services/sync/tests/unit/test_bookmark_order.js create mode 100644 services/sync/tests/unit/test_bookmark_places_query_rewriting.js create mode 100644 services/sync/tests/unit/test_bookmark_record.js create mode 100644 services/sync/tests/unit/test_bookmark_store.js create mode 100644 services/sync/tests/unit/test_bookmark_tracker.js create mode 100644 services/sync/tests/unit/test_bridged_engine.js create mode 100644 services/sync/tests/unit/test_clients_engine.js create mode 100644 services/sync/tests/unit/test_clients_escape.js create mode 100644 services/sync/tests/unit/test_collection_getBatched.js create mode 100644 services/sync/tests/unit/test_collections_recovery.js create mode 100644 services/sync/tests/unit/test_corrupt_keys.js create mode 100644 services/sync/tests/unit/test_declined.js create mode 100644 services/sync/tests/unit/test_disconnect_shutdown.js create mode 100644 services/sync/tests/unit/test_engine.js create mode 100644 services/sync/tests/unit/test_engine_abort.js create mode 100644 services/sync/tests/unit/test_engine_changes_during_sync.js create mode 100644 services/sync/tests/unit/test_enginemanager.js create mode 100644 services/sync/tests/unit/test_errorhandler_1.js create mode 100644 services/sync/tests/unit/test_errorhandler_2.js create mode 100644 services/sync/tests/unit/test_errorhandler_filelog.js create mode 100644 services/sync/tests/unit/test_errorhandler_sync_checkServerError.js create mode 100644 services/sync/tests/unit/test_extension_storage_engine.js create mode 100644 services/sync/tests/unit/test_extension_storage_engine_kinto.js create mode 100644 services/sync/tests/unit/test_extension_storage_migration_telem.js create mode 100644 services/sync/tests/unit/test_extension_storage_tracker_kinto.js create mode 100644 services/sync/tests/unit/test_form_validator.js create mode 100644 services/sync/tests/unit/test_forms_store.js create mode 100644 services/sync/tests/unit/test_forms_tracker.js create mode 100644 services/sync/tests/unit/test_fxa_node_reassignment.js create mode 100644 services/sync/tests/unit/test_fxa_service_cluster.js create mode 100644 services/sync/tests/unit/test_history_engine.js create mode 100644 services/sync/tests/unit/test_history_store.js create mode 100644 services/sync/tests/unit/test_history_tracker.js create mode 100644 services/sync/tests/unit/test_hmac_error.js create mode 100644 services/sync/tests/unit/test_httpd_sync_server.js create mode 100644 services/sync/tests/unit/test_interval_triggers.js create mode 100644 services/sync/tests/unit/test_keys.js create mode 100644 services/sync/tests/unit/test_load_modules.js create mode 100644 services/sync/tests/unit/test_node_reassignment.js create mode 100644 services/sync/tests/unit/test_password_engine.js create mode 100644 services/sync/tests/unit/test_password_store.js create mode 100644 services/sync/tests/unit/test_password_tracker.js create mode 100644 services/sync/tests/unit/test_password_validator.js create mode 100644 services/sync/tests/unit/test_postqueue.js create mode 100644 services/sync/tests/unit/test_prefs_engine.js create mode 100644 services/sync/tests/unit/test_prefs_store.js create mode 100644 services/sync/tests/unit/test_prefs_tracker.js create mode 100644 services/sync/tests/unit/test_records_crypto.js create mode 100644 services/sync/tests/unit/test_records_wbo.js create mode 100644 services/sync/tests/unit/test_resource.js create mode 100644 services/sync/tests/unit/test_resource_header.js create mode 100644 services/sync/tests/unit/test_resource_ua.js create mode 100644 services/sync/tests/unit/test_score_triggers.js create mode 100644 services/sync/tests/unit/test_service_attributes.js create mode 100644 services/sync/tests/unit/test_service_cluster.js create mode 100644 services/sync/tests/unit/test_service_detect_upgrade.js create mode 100644 services/sync/tests/unit/test_service_login.js create mode 100644 services/sync/tests/unit/test_service_startOver.js create mode 100644 services/sync/tests/unit/test_service_startup.js create mode 100644 services/sync/tests/unit/test_service_sync_401.js create mode 100644 services/sync/tests/unit/test_service_sync_locked.js create mode 100644 services/sync/tests/unit/test_service_sync_remoteSetup.js create mode 100644 services/sync/tests/unit/test_service_sync_specified.js create mode 100644 services/sync/tests/unit/test_service_sync_updateEnabledEngines.js create mode 100644 services/sync/tests/unit/test_service_verifyLogin.js create mode 100644 services/sync/tests/unit/test_service_wipeClient.js create mode 100644 services/sync/tests/unit/test_service_wipeServer.js create mode 100644 services/sync/tests/unit/test_status.js create mode 100644 services/sync/tests/unit/test_status_checkSetup.js create mode 100644 services/sync/tests/unit/test_sync_auth_manager.js create mode 100644 services/sync/tests/unit/test_syncedtabs.js create mode 100644 services/sync/tests/unit/test_syncengine.js create mode 100644 services/sync/tests/unit/test_syncengine_sync.js create mode 100644 services/sync/tests/unit/test_syncscheduler.js create mode 100644 services/sync/tests/unit/test_tab_engine.js create mode 100644 services/sync/tests/unit/test_tab_provider.js create mode 100644 services/sync/tests/unit/test_tab_quickwrite.js create mode 100644 services/sync/tests/unit/test_tab_tracker.js create mode 100644 services/sync/tests/unit/test_telemetry.js create mode 100644 services/sync/tests/unit/test_tracker_addChanged.js create mode 100644 services/sync/tests/unit/test_uistate.js create mode 100644 services/sync/tests/unit/test_utils_catch.js create mode 100644 services/sync/tests/unit/test_utils_deepEquals.js create mode 100644 services/sync/tests/unit/test_utils_deferGetSet.js create mode 100644 services/sync/tests/unit/test_utils_json.js create mode 100644 services/sync/tests/unit/test_utils_keyEncoding.js create mode 100644 services/sync/tests/unit/test_utils_lock.js create mode 100644 services/sync/tests/unit/test_utils_makeGUID.js create mode 100644 services/sync/tests/unit/test_utils_notify.js create mode 100644 services/sync/tests/unit/test_utils_passphrase.js create mode 100644 services/sync/tests/unit/xpcshell.toml create mode 100644 services/sync/tps/extensions/tps/api.js create mode 100644 services/sync/tps/extensions/tps/manifest.json create mode 100644 services/sync/tps/extensions/tps/resource/auth/fxaccounts.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/logger.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/addons.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/bookmarkValidator.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/bookmarks.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/formautofill.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/forms.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/history.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/passwords.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/prefs.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/tabs.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/modules/windows.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/quit.sys.mjs create mode 100644 services/sync/tps/extensions/tps/resource/tps.sys.mjs create mode 100644 services/sync/tps/extensions/tps/schema.json (limited to 'services') diff --git a/services/automation/AutomationComponents.manifest b/services/automation/AutomationComponents.manifest new file mode 100644 index 0000000000..d44fe90a22 --- /dev/null +++ b/services/automation/AutomationComponents.manifest @@ -0,0 +1,2 @@ +# Register resource aliases +resource services-automation resource://gre/modules/services-automation/ diff --git a/services/automation/ServicesAutomation.sys.mjs b/services/automation/ServicesAutomation.sys.mjs new file mode 100644 index 0000000000..24316d8827 --- /dev/null +++ b/services/automation/ServicesAutomation.sys.mjs @@ -0,0 +1,416 @@ +/* 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/. */ + +/* + * This module is used in automation to connect the browser to + * a specific FxA account and trigger FX Sync. + * + * To use it, you can call this sequence: + * + * initConfig("https://accounts.stage.mozaws.net"); + * await Authentication.signIn(username, password); + * await Sync.triggerSync(); + * await Authentication.signOut(); + * + * + * Where username is your FxA e-mail. it will connect your browser + * to that account and trigger a Sync (on stage servers.) + * + * You can also use the convenience function that does everything: + * + * await triggerSync(username, password, "https://accounts.stage.mozaws.net"); + * + */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs", + FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs", + Log: "resource://gre/modules/Log.sys.mjs", + Svc: "resource://services-sync/util.sys.mjs", + Weave: "resource://services-sync/main.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton(); +}); + +const AUTOCONFIG_PREF = "identity.fxaccounts.autoconfig.uri"; + +/* + * Log helpers. + */ +var _LOG = []; + +function LOG(msg, error) { + console.debug(msg); + _LOG.push(msg); + if (error) { + console.debug(JSON.stringify(error)); + _LOG.push(JSON.stringify(error)); + } +} + +function dumpLogs() { + let res = _LOG.join("\n"); + _LOG = []; + return res; +} + +function promiseObserver(aEventName) { + LOG("wait for " + aEventName); + return new Promise(resolve => { + let handler = () => { + lazy.Svc.Obs.remove(aEventName, handler); + resolve(); + }; + let handlerTimeout = () => { + lazy.Svc.Obs.remove(aEventName, handler); + LOG("handler timed out " + aEventName); + resolve(); + }; + lazy.Svc.Obs.add(aEventName, handler); + lazy.setTimeout(handlerTimeout, 3000); + }); +} + +/* + * Authentication + * + * Used to sign in an FxA account, takes care of + * the e-mail verification flow. + * + * Usage: + * + * await Authentication.signIn(username, password); + */ +export var Authentication = { + async isLoggedIn() { + return !!(await this.getSignedInUser()); + }, + + async isReady() { + let user = await this.getSignedInUser(); + if (user) { + LOG("current user " + JSON.stringify(user)); + } + return user && user.verified; + }, + + async getSignedInUser() { + try { + return await lazy.fxAccounts.getSignedInUser(); + } catch (error) { + LOG("getSignedInUser() failed", error); + throw error; + } + }, + + async shortWaitForVerification(ms) { + LOG("shortWaitForVerification"); + let userData = await this.getSignedInUser(); + let timeoutID; + LOG("set a timeout"); + let timeoutPromise = new Promise(resolve => { + timeoutID = lazy.setTimeout(() => { + LOG(`Warning: no verification after ${ms}ms.`); + resolve(); + }, ms); + }); + LOG("set a fxAccounts.whenVerified"); + await Promise.race([ + lazy.fxAccounts + .whenVerified(userData) + .finally(() => lazy.clearTimeout(timeoutID)), + timeoutPromise, + ]); + LOG("done"); + return this.isReady(); + }, + + async _confirmUser(uri) { + LOG("Open new tab and load verification page"); + let mainWindow = Services.wm.getMostRecentWindow("navigator:browser"); + let newtab = mainWindow.gBrowser.addWebTab(uri); + let win = mainWindow.gBrowser.getBrowserForTab(newtab); + win.addEventListener("load", function (e) { + LOG("load"); + }); + + win.addEventListener("loadstart", function (e) { + LOG("loadstart"); + }); + + win.addEventListener("error", function (msg, url, lineNo, columnNo, error) { + var string = msg.toLowerCase(); + var substring = "script error"; + if (string.indexOf(substring) > -1) { + LOG("Script Error: See Browser Console for Detail"); + } else { + var message = [ + "Message: " + msg, + "URL: " + url, + "Line: " + lineNo, + "Column: " + columnNo, + "Error object: " + JSON.stringify(error), + ].join(" - "); + + LOG(message); + } + }); + + LOG("wait for page to load"); + await new Promise(resolve => { + let handlerTimeout = () => { + LOG("timed out "); + resolve(); + }; + var timer = lazy.setTimeout(handlerTimeout, 10000); + win.addEventListener("loadend", function () { + resolve(); + lazy.clearTimeout(timer); + }); + }); + LOG("Page Loaded"); + let didVerify = await this.shortWaitForVerification(10000); + LOG("remove tab"); + mainWindow.gBrowser.removeTab(newtab); + return didVerify; + }, + + /* + * This whole verification process may be bypassed if the + * account is allow-listed. + */ + async _completeVerification(username) { + LOG("Fetching mail (from restmail) for user " + username); + let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent( + username + )}`; + let triedAlready = new Set(); + const tries = 10; + const normalWait = 4000; + for (let i = 0; i < tries; ++i) { + let resp = await fetch(restmailURI); + let messages = await resp.json(); + // Sort so that the most recent emails are first. + messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt)); + for (let m of messages) { + // We look for a link that has a x-link that we haven't yet tried. + if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) { + continue; + } + if (!m.headers["x-verify-code"]) { + continue; + } + let confirmLink = m.headers["x-link"]; + triedAlready.add(confirmLink); + LOG("Trying confirmation link " + confirmLink); + try { + if (await this._confirmUser(confirmLink)) { + LOG("confirmation done"); + return true; + } + LOG("confirmation failed"); + } catch (e) { + LOG( + "Warning: Failed to follow confirmation link: " + + lazy.Log.exceptionStr(e) + ); + } + } + if (i === 0) { + // first time through after failing we'll do this. + LOG("resendVerificationEmail"); + await lazy.fxAccounts.resendVerificationEmail(); + } + if (await this.shortWaitForVerification(normalWait)) { + return true; + } + } + // One last try. + return this.shortWaitForVerification(normalWait); + }, + + async signIn(username, password) { + LOG("Login user: " + username); + try { + // Required here since we don't go through the real login page + LOG("Calling FxAccountsConfig.ensureConfigured"); + await lazy.FxAccountsConfig.ensureConfigured(); + let client = new lazy.FxAccountsClient(); + LOG("Signing in"); + let credentials = await client.signIn(username, password, true); + LOG("Signed in, setting up the signed user in fxAccounts"); + await lazy.fxAccounts._internal.setSignedInUser(credentials); + + // If the account is not allow-listed for tests, we need to verify it + if (!credentials.verified) { + LOG("We need to verify the account"); + await this._completeVerification(username); + } else { + LOG("Credentials already verified"); + } + return true; + } catch (error) { + LOG("signIn() failed", error); + throw error; + } + }, + + async signOut() { + if (await Authentication.isLoggedIn()) { + // Note: This will clean up the device ID. + await lazy.fxAccounts.signOut(); + } + }, +}; + +/* + * Sync + * + * Used to trigger sync. + * + * usage: + * + * await Sync.triggerSync(); + */ +export var Sync = { + getSyncLogsDirectory() { + return PathUtils.join(PathUtils.profileDir, "weave", "logs"); + }, + + async init() { + lazy.Svc.Obs.add("weave:service:sync:error", this); + lazy.Svc.Obs.add("weave:service:setup-complete", this); + lazy.Svc.Obs.add("weave:service:tracking-started", this); + // Delay the automatic sync operations, so we can trigger it manually + lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 7200); + lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.idleInterval", 7200); + lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.activeInterval", 7200); + lazy.Weave.Svc.PrefBranch.setIntPref("syncThreshold", 10000000); + // Wipe all the logs + await this.wipeLogs(); + }, + + observe(subject, topic, data) { + LOG("Event received " + topic); + }, + + async configureSync() { + // todo, enable all sync engines here + // the addon engine requires kinto creds... + LOG("configuring sync"); + console.assert(await Authentication.isReady(), "You are not connected"); + await lazy.Weave.Service.configure(); + if (!lazy.Weave.Status.ready) { + await promiseObserver("weave:service:ready"); + } + if (lazy.Weave.Service.locked) { + await promiseObserver("weave:service:resyncs-finished"); + } + }, + + /* + * triggerSync() runs the whole process of Syncing. + * + * returns 1 on success, 0 on failure. + */ + async triggerSync() { + if (!(await Authentication.isLoggedIn())) { + LOG("Not connected"); + return 1; + } + await this.init(); + let result = 1; + try { + await this.configureSync(); + LOG("Triggering a sync"); + await lazy.Weave.Service.sync(); + + // wait a second for things to settle... + await new Promise(resolve => lazy.setTimeout(resolve, 1000)); + + LOG("Sync done"); + result = 0; + } catch (error) { + LOG("triggerSync() failed", error); + } + + return result; + }, + + async wipeLogs() { + let outputDirectory = this.getSyncLogsDirectory(); + if (!(await IOUtils.exists(outputDirectory))) { + return; + } + LOG("Wiping existing Sync logs"); + try { + await IOUtils.remove(outputDirectory, { recursive: true }); + } catch (error) { + LOG("wipeLogs() failed", error); + } + }, + + async getLogs() { + let outputDirectory = this.getSyncLogsDirectory(); + let entries = []; + + if (await IOUtils.exists(outputDirectory)) { + // Iterate through the directory + for (const path of await IOUtils.getChildren(outputDirectory)) { + const info = await IOUtils.stat(path); + + entries.push({ + path, + name: PathUtils.filename(path), + lastModified: info.lastModified, + }); + } + + entries.sort(function (a, b) { + return b.lastModified - a.lastModified; + }); + } + + const promises = entries.map(async entry => { + const content = await IOUtils.readUTF8(entry.path); + return { + name: entry.name, + content, + }; + }); + return Promise.all(promises); + }, +}; + +export function initConfig(autoconfig) { + Services.prefs.setStringPref(AUTOCONFIG_PREF, autoconfig); +} + +export async function triggerSync(username, password, autoconfig) { + initConfig(autoconfig); + await Authentication.signIn(username, password); + var result = await Sync.triggerSync(); + await Authentication.signOut(); + var logs = { + sync: await Sync.getLogs(), + condprof: [ + { + name: "console.txt", + content: dumpLogs(), + }, + ], + }; + return { + result, + logs, + }; +} diff --git a/services/automation/moz.build b/services/automation/moz.build new file mode 100644 index 0000000000..61b4815f42 --- /dev/null +++ b/services/automation/moz.build @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Sync") + +EXTRA_COMPONENTS += [ + "AutomationComponents.manifest", +] + +EXTRA_JS_MODULES["services-automation"] += [ + "ServicesAutomation.sys.mjs", +] diff --git a/services/common/app_services_logger/AppServicesLoggerComponents.h b/services/common/app_services_logger/AppServicesLoggerComponents.h new file mode 100644 index 0000000000..015eae230b --- /dev/null +++ b/services/common/app_services_logger/AppServicesLoggerComponents.h @@ -0,0 +1,38 @@ +/* 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/. */ + +#ifndef mozilla_services_AppServicesLoggerComponents_h_ +#define mozilla_services_AppServicesLoggerComponents_h_ + +#include "mozIAppServicesLogger.h" +#include "nsCOMPtr.h" + +extern "C" { + +// Implemented in Rust, in the `app_services_logger` crate. +nsresult NS_NewAppServicesLogger(mozIAppServicesLogger** aResult); + +} // extern "C" + +namespace mozilla { +namespace appservices { + +// The C++ constructor for a `services.appServicesLogger` service. This wrapper +// exists because `components.conf` requires a component class constructor to +// return an `already_AddRefed`, but Rust doesn't have such a type. So we +// call the Rust constructor using a `nsCOMPtr` (which is compatible with Rust's +// `xpcom::RefPtr`) out param, and return that. +already_AddRefed NewLogService() { + nsCOMPtr logger; + nsresult rv = NS_NewAppServicesLogger(getter_AddRefs(logger)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return logger.forget(); +} + +} // namespace appservices +} // namespace mozilla + +#endif // mozilla_services_AppServicesLoggerComponents_h_ diff --git a/services/common/app_services_logger/Cargo.toml b/services/common/app_services_logger/Cargo.toml new file mode 100644 index 0000000000..d68c378845 --- /dev/null +++ b/services/common/app_services_logger/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "app_services_logger" +version = "0.1.0" +authors = ["lougeniac64 "] +edition = "2018" +license = "MPL-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cstr = "0.2" +golden_gate = { path = "../../../services/sync/golden_gate" } +log = "0.4" +once_cell = "1.4.0" +nserror = { path = "../../../xpcom/rust/nserror" } +nsstring = { path = "../../../xpcom/rust/nsstring" } +xpcom = { path = "../../../xpcom/rust/xpcom" } diff --git a/services/common/app_services_logger/components.conf b/services/common/app_services_logger/components.conf new file mode 100644 index 0000000000..cdea60a04b --- /dev/null +++ b/services/common/app_services_logger/components.conf @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{d2716568-f5fa-4989-91dd-e11599e932a1}', + 'contract_ids': ['@mozilla.org/appservices/logger;1'], + 'type': 'mozIAppServicesLogger', + 'headers': ['mozilla/appservices/AppServicesLoggerComponents.h'], + 'constructor': 'mozilla::appservices::NewLogService', + }, +] diff --git a/services/common/app_services_logger/src/lib.rs b/services/common/app_services_logger/src/lib.rs new file mode 100644 index 0000000000..92125b62c4 --- /dev/null +++ b/services/common/app_services_logger/src/lib.rs @@ -0,0 +1,128 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! This provides a XPCOM service to send app services logs to the desktop + +#[macro_use] +extern crate cstr; + +#[macro_use] +extern crate xpcom; + +use golden_gate::log::LogSink; +use nserror::{nsresult, NS_OK}; +use nsstring::nsAString; +use once_cell::sync::Lazy; +use std::os::raw::c_char; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + RwLock, + }, +}; +use xpcom::{ + interfaces::{mozIAppServicesLogger, mozIServicesLogSink, nsIObserverService, nsISupports}, + RefPtr, +}; + +/// A flag that's set after we register our observer to clear the map of loggers +/// on shutdown. +static SHUTDOWN_OBSERVED: AtomicBool = AtomicBool::new(false); + +#[xpcom(implement(mozIAppServicesLogger), nonatomic)] +pub struct AppServicesLogger {} + +pub static LOGGERS_BY_TARGET: Lazy>> = Lazy::new(|| { + let h: HashMap = HashMap::new(); + let m = RwLock::new(h); + m +}); + +impl AppServicesLogger { + xpcom_method!(register => Register(target: *const nsAString, logger: *const mozIServicesLogSink)); + fn register(&self, target: &nsAString, logger: &mozIServicesLogSink) -> Result<(), nsresult> { + let log_sink_logger = LogSink::with_logger(Some(logger))?; + + ensure_observing_shutdown(); + + LOGGERS_BY_TARGET + .write() + .unwrap() + .insert(target.to_string(), log_sink_logger); + Ok(()) + } + + pub fn is_app_services_logger_registered(target: String) -> bool { + match LOGGERS_BY_TARGET.read() { + Ok(loggers_by_target) => loggers_by_target.contains_key(&target), + Err(_e) => false, + } + } +} + +// Import the `NS_IsMainThread` symbol from Gecko... +extern "C" { + fn NS_IsMainThread() -> bool; +} + +/// Registers an observer to clear the loggers map on `xpcom-shutdown`. This +/// function must be called from the main thread, because the observer service +//// is main thread-only. +fn ensure_observing_shutdown() { + assert!(unsafe { NS_IsMainThread() }); + // If we've already registered our observer, bail. Relaxed ordering is safe + // here and below, because we've asserted we're only called from the main + // thread, and only check the flag here. + if SHUTDOWN_OBSERVED.load(Ordering::Relaxed) { + return; + } + if let Ok(service) = xpcom::components::Observer::service::() { + let observer = ShutdownObserver::allocate(InitShutdownObserver {}); + let rv = unsafe { + service.AddObserver(observer.coerce(), cstr!("xpcom-shutdown").as_ptr(), false) + }; + // If we fail to register the observer now, or fail to get the observer + // service, the flag will remain `false`, and we'll try again on the + // next call to `ensure_observing_shutdown`. + SHUTDOWN_OBSERVED.store(rv.succeeded(), Ordering::Relaxed); + } +} + +#[xpcom(implement(nsIObserver), nonatomic)] +struct ShutdownObserver {} + +impl ShutdownObserver { + xpcom_method!(observe => Observe(_subject: *const nsISupports, topic: *const c_char, _data: *const u16)); + /// Remove our shutdown observer and clear the map. + fn observe( + &self, + _subject: &nsISupports, + topic: *const c_char, + _data: *const u16, + ) -> Result<(), nsresult> { + LOGGERS_BY_TARGET.write().unwrap().clear(); + if let Ok(service) = xpcom::components::Observer::service::() { + // Ignore errors, since we're already shutting down. + let _ = unsafe { service.RemoveObserver(self.coerce(), topic) }; + } + Ok(()) + } +} + +/// The constructor for an `AppServicesLogger` service. This uses C linkage so that it +/// can be called from C++. See `AppServicesLoggerComponents.h` for the C++ +/// constructor that's passed to the component manager. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences `result`. +#[no_mangle] +pub unsafe extern "C" fn NS_NewAppServicesLogger( + result: *mut *const mozIAppServicesLogger, +) -> nsresult { + let logger = AppServicesLogger::allocate(InitAppServicesLogger {}); + RefPtr::new(logger.coerce::()).forget(&mut *result); + NS_OK +} diff --git a/services/common/async.sys.mjs b/services/common/async.sys.mjs new file mode 100644 index 0000000000..564b46a071 --- /dev/null +++ b/services/common/async.sys.mjs @@ -0,0 +1,301 @@ +/* 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 Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer"); + +/* + * Helpers for various async operations. + */ +export var Async = { + /** + * Execute an arbitrary number of asynchronous functions one after the + * other, passing the callback arguments on to the next one. All functions + * must take a callback function as their last argument. The 'this' object + * will be whatever chain()'s is. + * + * @usage this._chain = Async.chain; + * this._chain(this.foo, this.bar, this.baz)(args, for, foo) + * + * This is equivalent to: + * + * let self = this; + * self.foo(args, for, foo, function (bars, args) { + * self.bar(bars, args, function (baz, params) { + * self.baz(baz, params); + * }); + * }); + */ + chain: function chain(...funcs) { + let thisObj = this; + return function callback() { + if (funcs.length) { + let args = [...arguments, callback]; + let f = funcs.shift(); + f.apply(thisObj, args); + } + }; + }, + + /** + * Check if the app is still ready (not quitting). Returns true, or throws an + * exception if not ready. + */ + checkAppReady: function checkAppReady() { + // Watch for app-quit notification to stop any sync calls + Services.obs.addObserver(function onQuitApplication() { + Services.obs.removeObserver(onQuitApplication, "quit-application"); + Async.checkAppReady = Async.promiseYield = function () { + let exception = Components.Exception( + "App. Quitting", + Cr.NS_ERROR_ABORT + ); + exception.appIsShuttingDown = true; + throw exception; + }; + }, "quit-application"); + // In the common case, checkAppReady just returns true + return (Async.checkAppReady = function () { + return true; + })(); + }, + + /** + * Check if the app is still ready (not quitting). Returns true if the app + * is ready, or false if it is being shut down. + */ + isAppReady() { + try { + return Async.checkAppReady(); + } catch (ex) { + if (!Async.isShutdownException(ex)) { + throw ex; + } + } + return false; + }, + + /** + * Check if the passed exception is one raised by checkAppReady. Typically + * this will be used in exception handlers to allow such exceptions to + * make their way to the top frame and allow the app to actually terminate. + */ + isShutdownException(exception) { + return exception && exception.appIsShuttingDown === true; + }, + + /** + * A "tight loop" of promises can still lock up the browser for some time. + * Periodically waiting for a promise returned by this function will solve + * that. + * You should probably not use this method directly and instead use jankYielder + * below. + * Some reference here: + * - https://gist.github.com/jesstelford/bbb30b983bddaa6e5fef2eb867d37678 + * - https://bugzilla.mozilla.org/show_bug.cgi?id=1094248 + */ + promiseYield() { + return new Promise(resolve => { + Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL); + }); + }, + + /** + * Shared state for yielding every N calls. + * + * Can be passed to multiple Async.yieldingForEach to have them overall yield + * every N iterations. + */ + yieldState(yieldEvery = 50) { + let iterations = 0; + + return { + shouldYield() { + ++iterations; + return iterations % yieldEvery === 0; + }, + }; + }, + + /** + * Apply the given function to each element of the iterable, yielding the + * event loop every yieldEvery iterations. + * + * @param iterable {Iterable} + * The iterable or iterator to iterate through. + * + * @param fn {(*) -> void|boolean} + * The function to be called on each element of the iterable. + * + * Returning true from the function will stop the iteration. + * + * @param [yieldEvery = 50] {number|object} + * The number of iterations to complete before yielding back to the event + * loop. + * + * @return {boolean} + * Whether or not the function returned early. + */ + async yieldingForEach(iterable, fn, yieldEvery = 50) { + const yieldState = + typeof yieldEvery === "number" + ? Async.yieldState(yieldEvery) + : yieldEvery; + let iteration = 0; + + for (const item of iterable) { + let result = fn(item, iteration++); + if (typeof result !== "undefined" && typeof result.then !== "undefined") { + // If we await result when it is not a Promise, we create an + // automatically resolved promise, which is exactly the case that we + // are trying to avoid. + result = await result; + } + + if (result === true) { + return true; + } + + if (yieldState.shouldYield()) { + await Async.promiseYield(); + Async.checkAppReady(); + } + } + + return false; + }, + + asyncQueueCaller(log) { + return new AsyncQueueCaller(log); + }, + + asyncObserver(log, obj) { + return new AsyncObserver(log, obj); + }, + + watchdog() { + return new Watchdog(); + }, +}; + +/** + * Allows consumers to enqueue asynchronous callbacks to be called in order. + * Typically this is used when providing a callback to a caller that doesn't + * await on promises. + */ +class AsyncQueueCaller { + constructor(log) { + this._log = log; + this._queue = Promise.resolve(); + this.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]); + } + + /** + * /!\ Never await on another function that calls enqueueCall /!\ + * on the same queue or we will deadlock. + */ + enqueueCall(func) { + this._queue = (async () => { + await this._queue; + try { + return await func(); + } catch (e) { + this._log.error(e); + return false; + } + })(); + } + + promiseCallsComplete() { + return this._queue; + } +} + +/* + * Subclass of AsyncQueueCaller that can be used with Services.obs directly. + * When this observe() is called, it will enqueue a call to the consumers's + * observe(). + */ +class AsyncObserver extends AsyncQueueCaller { + constructor(obj, log) { + super(log); + this.obj = obj; + } + + observe(subject, topic, data) { + this.enqueueCall(() => this.obj.observe(subject, topic, data)); + } + + promiseObserversComplete() { + return this.promiseCallsComplete(); + } +} + +/** + * Woof! Signals an operation to abort, either at shutdown or after a timeout. + * The buffered engine uses this to abort long-running merges, so that they + * don't prevent Firefox from quitting, or block future syncs. + */ +class Watchdog { + constructor() { + this.controller = new AbortController(); + this.timer = new Timer(); + + /** + * The reason for signaling an abort. `null` if not signaled, + * `"timeout"` if the watchdog timer fired, or `"shutdown"` if the app is + * is quitting. + * + * @type {String?} + */ + this.abortReason = null; + } + + /** + * Returns the abort signal for this watchdog. This can be passed to APIs + * that take a signal for cancellation, like `SyncedBookmarksMirror::apply` + * or `fetch`. + * + * @type {AbortSignal} + */ + get signal() { + return this.controller.signal; + } + + /** + * Starts the watchdog timer, and listens for the app quitting. + * + * @param {Number} delay + * The time to wait before signaling the operation to abort. + */ + start(delay) { + if (!this.signal.aborted) { + Services.obs.addObserver(this, "quit-application"); + this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); + } + } + + /** + * Stops the watchdog timer and removes any listeners. This should be called + * after the operation finishes. + */ + stop() { + if (!this.signal.aborted) { + Services.obs.removeObserver(this, "quit-application"); + this.timer.cancel(); + } + } + + observe(subject, topic, data) { + if (topic == "timer-callback") { + this.abortReason = "timeout"; + } else if (topic == "quit-application") { + this.abortReason = "shutdown"; + } + this.stop(); + this.controller.abort(); + } +} diff --git a/services/common/hawkclient.sys.mjs b/services/common/hawkclient.sys.mjs new file mode 100644 index 0000000000..05a3fa7336 --- /dev/null +++ b/services/common/hawkclient.sys.mjs @@ -0,0 +1,336 @@ +/* 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/. */ + +/* + * HAWK is an HTTP authentication scheme using a message authentication code + * (MAC) algorithm to provide partial HTTP request cryptographic verification. + * + * For details, see: https://github.com/hueniverse/hawk + * + * With HAWK, it is essential that the clocks on clients and server not have an + * absolute delta of greater than one minute, as the HAWK protocol uses + * timestamps to reduce the possibility of replay attacks. However, it is + * likely that some clients' clocks will be more than a little off, especially + * in mobile devices, which would break HAWK-based services (like sync and + * firefox accounts) for those clients. + * + * This library provides a stateful HAWK client that calculates (roughly) the + * clock delta on the client vs the server. The library provides an interface + * for deriving HAWK credentials and making HAWK-authenticated REST requests to + * a single remote server. Therefore, callers who want to interact with + * multiple HAWK services should instantiate one HawkClient per service. + */ + +import { HAWKAuthenticatedRESTRequest } from "resource://services-common/hawkrequest.sys.mjs"; + +import { Observers } from "resource://services-common/observers.sys.mjs"; +import { Log } from "resource://gre/modules/Log.sys.mjs"; + +// log.appender.dump should be one of "Fatal", "Error", "Warn", "Info", "Config", +// "Debug", "Trace" or "All". If none is specified, "Error" will be used by +// default. +// Note however that Sync will also add this log to *its* DumpAppender, so +// in a Sync context it shouldn't be necessary to adjust this - however, that +// also means error logs are likely to be dump'd twice but that's OK. +const PREF_LOG_LEVEL = "services.common.hawk.log.appender.dump"; + +// A pref that can be set so "sensitive" information (eg, personally +// identifiable info, credentials, etc) will be logged. +const PREF_LOG_SENSITIVE_DETAILS = "services.common.hawk.log.sensitive"; + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "log", function () { + let log = Log.repository.getLogger("Hawk"); + // We set the log itself to "debug" and set the level from the preference to + // the appender. This allows other things to send the logs to different + // appenders, while still allowing the pref to control what is seen via dump() + log.level = Log.Level.Debug; + let appender = new Log.DumpAppender(); + log.addAppender(appender); + appender.level = Log.Level.Error; + try { + let level = + Services.prefs.getPrefType(PREF_LOG_LEVEL) == + Ci.nsIPrefBranch.PREF_STRING && + Services.prefs.getStringPref(PREF_LOG_LEVEL); + appender.level = Log.Level[level] || Log.Level.Error; + } catch (e) { + log.error(e); + } + + return log; +}); + +// A boolean to indicate if personally identifiable information (or anything +// else sensitive, such as credentials) should be logged. +ChromeUtils.defineLazyGetter(lazy, "logPII", function () { + try { + return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS); + } catch (_) { + return false; + } +}); + +/* + * A general purpose client for making HAWK authenticated requests to a single + * host. Keeps track of the clock offset between the client and the host for + * computation of the timestamp in the HAWK Authorization header. + * + * Clients should create one HawkClient object per each server they wish to + * interact with. + * + * @param host + * The url of the host + */ +export var HawkClient = function (host) { + this.host = host; + + // Clock offset in milliseconds between our client's clock and the date + // reported in responses from our host. + this._localtimeOffsetMsec = 0; +}; + +HawkClient.prototype = { + /* + * Construct an error message for a response. Private. + * + * @param restResponse + * A RESTResponse object from a RESTRequest + * + * @param error + * A string or object describing the error + */ + _constructError(restResponse, error) { + let errorObj = { + error, + // This object is likely to be JSON.stringify'd, but neither Error() + // objects nor Components.Exception objects do the right thing there, + // so we add a new element which is simply the .toString() version of + // the error object, so it does appear in JSON'd values. + errorString: error.toString(), + message: restResponse.statusText, + code: restResponse.status, + errno: restResponse.status, + toString() { + return this.code + ": " + this.message; + }, + }; + let retryAfter = + restResponse.headers && restResponse.headers["retry-after"]; + retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter; + if (retryAfter) { + errorObj.retryAfter = retryAfter; + // and notify observers of the retry interval + if (this.observerPrefix) { + Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter); + } + } + return errorObj; + }, + + /* + * + * Update clock offset by determining difference from date gives in the (RFC + * 1123) Date header of a server response. Because HAWK tolerates a window + * of one minute of clock skew (so two minutes total since the skew can be + * positive or negative), the simple method of calculating offset here is + * probably good enough. We keep the value in milliseconds to make life + * easier, even though the value will not have millisecond accuracy. + * + * @param dateString + * An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT") + * + * For HAWK clock skew and replay protection, see + * https://github.com/hueniverse/hawk#replay-protection + */ + _updateClockOffset(dateString) { + try { + let serverDateMsec = Date.parse(dateString); + this._localtimeOffsetMsec = serverDateMsec - this.now(); + lazy.log.debug( + "Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec + ); + } catch (err) { + lazy.log.warn("Bad date header in server response: " + dateString); + } + }, + + /* + * Get the current clock offset in milliseconds. + * + * The offset is the number of milliseconds that must be added to the client + * clock to make it equal to the server clock. For example, if the client is + * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. + */ + get localtimeOffsetMsec() { + return this._localtimeOffsetMsec; + }, + + /* + * return current time in milliseconds + */ + now() { + return Date.now(); + }, + + /* A general method for sending raw RESTRequest calls authorized using HAWK + * + * @param path + * API endpoint path + * @param method + * The HTTP request method + * @param credentials + * Hawk credentials + * @param payloadObj + * An object that can be encodable as JSON as the payload of the + * request + * @param extraHeaders + * An object with header/value pairs to send with the request. + * @return Promise + * Returns a promise that resolves to the response of the API call, + * or is rejected with an error. If the server response can be parsed + * as JSON and contains an 'error' property, the promise will be + * rejected with this JSON-parsed response. + */ + async request( + path, + method, + credentials = null, + payloadObj = {}, + extraHeaders = {}, + retryOK = true + ) { + method = method.toLowerCase(); + + let uri = this.host + path; + + let extra = { + now: this.now(), + localtimeOffsetMsec: this.localtimeOffsetMsec, + headers: extraHeaders, + }; + + let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra); + let error; + let restResponse = await request[method](payloadObj).catch(e => { + // Keep a reference to the error, log a message about it, and return the + // response anyway. + error = e; + lazy.log.warn("hawk request error", error); + return request.response; + }); + + // This shouldn't happen anymore, but it's not exactly difficult to handle. + if (!restResponse) { + throw error; + } + + let status = restResponse.status; + + lazy.log.debug( + "(Response) " + + path + + ": code: " + + status + + " - Status text: " + + restResponse.statusText + ); + if (lazy.logPII) { + lazy.log.debug("Response text", restResponse.body); + } + + // All responses may have backoff headers, which are a server-side safety + // valve to allow slowing down clients without hurting performance. + this._maybeNotifyBackoff(restResponse, "x-weave-backoff"); + this._maybeNotifyBackoff(restResponse, "x-backoff"); + + if (error) { + // When things really blow up, reconstruct an error object that follows + // the general format of the server on error responses. + throw this._constructError(restResponse, error); + } + + this._updateClockOffset(restResponse.headers.date); + + if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) { + // Retry once if we were rejected due to a bad timestamp. + // Clock offset is adjusted already in the top of this function. + lazy.log.debug("Received 401 for " + path + ": retrying"); + return this.request( + path, + method, + credentials, + payloadObj, + extraHeaders, + false + ); + } + + // If the server returned a json error message, use it in the rejection + // of the promise. + // + // In the case of a 401, in which we are probably being rejected for a + // bad timestamp, retry exactly once, during which time clock offset will + // be adjusted. + + let jsonResponse = {}; + try { + jsonResponse = JSON.parse(restResponse.body); + } catch (notJSON) {} + + let okResponse = 200 <= status && status < 300; + if (!okResponse || jsonResponse.error) { + if (jsonResponse.error) { + throw jsonResponse; + } + throw this._constructError(restResponse, "Request failed"); + } + + // It's up to the caller to know how to decode the response. + // We just return the whole response. + return restResponse; + }, + + /* + * The prefix used for all notifications sent by this module. This + * allows the handler of notifications to be sure they are handling + * notifications for the service they expect. + * + * If not set, no notifications will be sent. + */ + observerPrefix: null, + + // Given an optional header value, notify that a backoff has been requested. + _maybeNotifyBackoff(response, headerName) { + if (!this.observerPrefix || !response.headers) { + return; + } + let headerVal = response.headers[headerName]; + if (!headerVal) { + return; + } + let backoffInterval; + try { + backoffInterval = parseInt(headerVal, 10); + } catch (ex) { + lazy.log.error( + "hawkclient response had invalid backoff value in '" + + headerName + + "' header: " + + headerVal + ); + return; + } + Observers.notify( + this.observerPrefix + ":backoff:interval", + backoffInterval + ); + }, + + // override points for testing. + newHAWKAuthenticatedRESTRequest(uri, credentials, extra) { + return new HAWKAuthenticatedRESTRequest(uri, credentials, extra); + }, +}; diff --git a/services/common/hawkrequest.sys.mjs b/services/common/hawkrequest.sys.mjs new file mode 100644 index 0000000000..a856ef032d --- /dev/null +++ b/services/common/hawkrequest.sys.mjs @@ -0,0 +1,197 @@ +/* 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 { Log } from "resource://gre/modules/Log.sys.mjs"; + +import { RESTRequest } from "resource://services-common/rest.sys.mjs"; +import { CommonUtils } from "resource://services-common/utils.sys.mjs"; +import { Credentials } from "resource://gre/modules/Credentials.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CryptoUtils: "resource://services-crypto/utils.sys.mjs", +}); + +/** + * Single-use HAWK-authenticated HTTP requests to RESTish resources. + * + * @param uri + * (String) URI for the RESTRequest constructor + * + * @param credentials + * (Object) Optional credentials for computing HAWK authentication + * header. + * + * @param payloadObj + * (Object) Optional object to be converted to JSON payload + * + * @param extra + * (Object) Optional extra params for HAWK header computation. + * Valid properties are: + * + * now: , + * localtimeOffsetMsec: , + * headers: + * + * extra.localtimeOffsetMsec is the value in milliseconds that must be added to + * the local clock to make it agree with the server's clock. For instance, if + * the local clock is two minutes ahead of the server, the time offset in + * milliseconds will be -120000. + */ + +export var HAWKAuthenticatedRESTRequest = function HawkAuthenticatedRESTRequest( + uri, + credentials, + extra = {} +) { + RESTRequest.call(this, uri); + + this.credentials = credentials; + this.now = extra.now || Date.now(); + this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0; + this._log.trace( + "local time, offset: " + this.now + ", " + this.localtimeOffsetMsec + ); + this.extraHeaders = extra.headers || {}; + + // Expose for testing + this._intl = getIntl(); +}; + +HAWKAuthenticatedRESTRequest.prototype = { + async dispatch(method, data) { + let contentType = "text/plain"; + if (method == "POST" || method == "PUT" || method == "PATCH") { + contentType = "application/json"; + } + if (this.credentials) { + let options = { + now: this.now, + localtimeOffsetMsec: this.localtimeOffsetMsec, + credentials: this.credentials, + payload: (data && JSON.stringify(data)) || "", + contentType, + }; + let header = await lazy.CryptoUtils.computeHAWK( + this.uri, + method, + options + ); + this.setHeader("Authorization", header.field); + } + + for (let header in this.extraHeaders) { + this.setHeader(header, this.extraHeaders[header]); + } + + this.setHeader("Content-Type", contentType); + + this.setHeader("Accept-Language", this._intl.accept_languages); + + return super.dispatch(method, data); + }, +}; + +Object.setPrototypeOf( + HAWKAuthenticatedRESTRequest.prototype, + RESTRequest.prototype +); + +/** + * Generic function to derive Hawk credentials. + * + * Hawk credentials are derived using shared secrets, which depend on the token + * in use. + * + * @param tokenHex + * The current session token encoded in hex + * @param context + * A context for the credentials. A protocol version will be prepended + * to the context, see Credentials.keyWord for more information. + * @param size + * The size in bytes of the expected derived buffer, + * defaults to 3 * 32. + * @return credentials + * Returns an object: + * { + * id: the Hawk id (from the first 32 bytes derived) + * key: the Hawk key (from bytes 32 to 64) + * extra: size - 64 extra bytes (if size > 64) + * } + */ +export async function deriveHawkCredentials(tokenHex, context, size = 96) { + let token = CommonUtils.hexToBytes(tokenHex); + let out = await lazy.CryptoUtils.hkdfLegacy( + token, + undefined, + Credentials.keyWord(context), + size + ); + + let result = { + key: out.slice(32, 64), + id: CommonUtils.bytesAsHex(out.slice(0, 32)), + }; + if (size > 64) { + result.extra = out.slice(64); + } + + return result; +} + +// With hawk request, we send the user's accepted-languages with each request. +// To keep the number of times we read this pref at a minimum, maintain the +// preference in a stateful object that notices and updates itself when the +// pref is changed. +function Intl() { + // We won't actually query the pref until the first time we need it + this._accepted = ""; + this._everRead = false; + this.init(); +} + +Intl.prototype = { + init() { + Services.prefs.addObserver("intl.accept_languages", this); + }, + + uninit() { + Services.prefs.removeObserver("intl.accept_languages", this); + }, + + observe(subject, topic, data) { + this.readPref(); + }, + + readPref() { + this._everRead = true; + try { + this._accepted = Services.prefs.getComplexValue( + "intl.accept_languages", + Ci.nsIPrefLocalizedString + ).data; + } catch (err) { + let log = Log.repository.getLogger("Services.Common.RESTRequest"); + log.error("Error reading intl.accept_languages pref", err); + } + }, + + get accept_languages() { + if (!this._everRead) { + this.readPref(); + } + return this._accepted; + }, +}; + +// Singleton getter for Intl, creating an instance only when we first need it. +var intl = null; +function getIntl() { + if (!intl) { + intl = new Intl(); + } + return intl; +} diff --git a/services/common/kinto-http-client.sys.mjs b/services/common/kinto-http-client.sys.mjs new file mode 100644 index 0000000000..6ccaa520c1 --- /dev/null +++ b/services/common/kinto-http-client.sys.mjs @@ -0,0 +1,3061 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is generated from kinto.js - do not modify directly. + */ + +import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +/* + * Version 15.0.0 - c8775d9 + */ + +import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; + +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise */ + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, + r = + c < 3 + ? target + : desc === null + ? (desc = Object.getOwnPropertyDescriptor(target, key)) + : desc, + d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") + r = Reflect.decorate(decorators, target, key, desc); + else + for (var i = decorators.length - 1; i >= 0; i--) + if ((d = decorators[i])) + r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +/** + * Chunks an array into n pieces. + * + * @private + * @param {Array} array + * @param {Number} n + * @return {Array} + */ +function partition(array, n) { + if (n <= 0) { + return [array]; + } + return array.reduce((acc, x, i) => { + if (i === 0 || i % n === 0) { + acc.push([x]); + } else { + acc[acc.length - 1].push(x); + } + return acc; + }, []); +} +/** + * Returns a Promise always resolving after the specified amount in milliseconds. + * + * @return Promise + */ +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +/** + * Always returns a resource data object from the provided argument. + * + * @private + * @param {Object|String} resource + * @return {Object} + */ +function toDataBody(resource) { + if (isObject(resource)) { + return resource; + } + if (typeof resource === "string") { + return { id: resource }; + } + throw new Error("Invalid argument."); +} +/** + * Transforms an object into an URL query string, stripping out any undefined + * values. + * + * @param {Object} obj + * @return {String} + */ +function qsify(obj) { + const encode = v => + encodeURIComponent(typeof v === "boolean" ? String(v) : v); + const stripped = cleanUndefinedProperties(obj); + return Object.keys(stripped) + .map(k => { + const ks = encode(k) + "="; + if (Array.isArray(stripped[k])) { + return ks + stripped[k].map(v => encode(v)).join(","); + } + return ks + encode(stripped[k]); + }) + .join("&"); +} +/** + * Checks if a version is within the provided range. + * + * @param {String} version The version to check. + * @param {String} minVersion The minimum supported version (inclusive). + * @param {String} maxVersion The minimum supported version (exclusive). + * @throws {Error} If the version is outside of the provided range. + */ +function checkVersion(version, minVersion, maxVersion) { + const extract = str => str.split(".").map(x => parseInt(x, 10)); + const [verMajor, verMinor] = extract(version); + const [minMajor, minMinor] = extract(minVersion); + const [maxMajor, maxMinor] = extract(maxVersion); + const checks = [ + verMajor < minMajor, + verMajor === minMajor && verMinor < minMinor, + verMajor > maxMajor, + verMajor === maxMajor && verMinor >= maxMinor, + ]; + if (checks.some(x => x)) { + throw new Error( + `Version ${version} doesn't satisfy ${minVersion} <= x < ${maxVersion}` + ); + } +} +/** + * Generates a decorator function ensuring a version check is performed against + * the provided requirements before executing it. + * + * @param {String} min The required min version (inclusive). + * @param {String} max The required max version (inclusive). + * @return {Function} + */ +function support(min, max) { + return function ( + // @ts-ignore + target, + key, + descriptor + ) { + const fn = descriptor.value; + return { + configurable: true, + get() { + const wrappedMethod = (...args) => { + // "this" is the current instance which its method is decorated. + const client = this.client ? this.client : this; + return client + .fetchHTTPApiVersion() + .then(version => checkVersion(version, min, max)) + .then(() => fn.apply(this, args)); + }; + Object.defineProperty(this, key, { + value: wrappedMethod, + configurable: true, + writable: true, + }); + return wrappedMethod; + }, + }; + }; +} +/** + * Generates a decorator function ensuring that the specified capabilities are + * available on the server before executing it. + * + * @param {Array} capabilities The required capabilities. + * @return {Function} + */ +function capable(capabilities) { + return function ( + // @ts-ignore + target, + key, + descriptor + ) { + const fn = descriptor.value; + return { + configurable: true, + get() { + const wrappedMethod = (...args) => { + // "this" is the current instance which its method is decorated. + const client = this.client ? this.client : this; + return client + .fetchServerCapabilities() + .then(available => { + const missing = capabilities.filter(c => !(c in available)); + if (missing.length) { + const missingStr = missing.join(", "); + throw new Error( + `Required capabilities ${missingStr} not present on server` + ); + } + }) + .then(() => fn.apply(this, args)); + }; + Object.defineProperty(this, key, { + value: wrappedMethod, + configurable: true, + writable: true, + }); + return wrappedMethod; + }, + }; + }; +} +/** + * Generates a decorator function ensuring an operation is not performed from + * within a batch request. + * + * @param {String} message The error message to throw. + * @return {Function} + */ +function nobatch(message) { + return function ( + // @ts-ignore + target, + key, + descriptor + ) { + const fn = descriptor.value; + return { + configurable: true, + get() { + const wrappedMethod = (...args) => { + // "this" is the current instance which its method is decorated. + if (this._isBatch) { + throw new Error(message); + } + return fn.apply(this, args); + }; + Object.defineProperty(this, key, { + value: wrappedMethod, + configurable: true, + writable: true, + }); + return wrappedMethod; + }, + }; + }; +} +/** + * Returns true if the specified value is an object (i.e. not an array nor null). + * @param {Object} thing The value to inspect. + * @return {bool} + */ +function isObject(thing) { + return typeof thing === "object" && thing !== null && !Array.isArray(thing); +} +/** + * Parses a data url. + * @param {String} dataURL The data url. + * @return {Object} + */ +function parseDataURL(dataURL) { + const regex = /^data:(.*);base64,(.*)/; + const match = dataURL.match(regex); + if (!match) { + throw new Error(`Invalid data-url: ${String(dataURL).substring(0, 32)}...`); + } + const props = match[1]; + const base64 = match[2]; + const [type, ...rawParams] = props.split(";"); + const params = rawParams.reduce((acc, param) => { + const [key, value] = param.split("="); + return { ...acc, [key]: value }; + }, {}); + return { ...params, type, base64 }; +} +/** + * Extracts file information from a data url. + * @param {String} dataURL The data url. + * @return {Object} + */ +function extractFileInfo(dataURL) { + const { name, type, base64 } = parseDataURL(dataURL); + const binary = atob(base64); + const array = []; + for (let i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)); + } + const blob = new Blob([new Uint8Array(array)], { type }); + return { blob, name }; +} +/** + * Creates a FormData instance from a data url and an existing JSON response + * body. + * @param {String} dataURL The data url. + * @param {Object} body The response body. + * @param {Object} [options={}] The options object. + * @param {Object} [options.filename] Force attachment file name. + * @return {FormData} + */ +function createFormData(dataURL, body, options = {}) { + const { filename = "untitled" } = options; + const { blob, name } = extractFileInfo(dataURL); + const formData = new FormData(); + formData.append("attachment", blob, name || filename); + for (const property in body) { + if (typeof body[property] !== "undefined") { + formData.append(property, JSON.stringify(body[property])); + } + } + return formData; +} +/** + * Clones an object with all its undefined keys removed. + * @private + */ +function cleanUndefinedProperties(obj) { + const result = {}; + for (const key in obj) { + if (typeof obj[key] !== "undefined") { + result[key] = obj[key]; + } + } + return result; +} +/** + * Handle common query parameters for Kinto requests. + * + * @param {String} [path] The endpoint base path. + * @param {Array} [options.fields] Fields to limit the + * request to. + * @param {Object} [options.query={}] Additional query arguments. + */ +function addEndpointOptions(path, options = {}) { + const query = { ...options.query }; + if (options.fields) { + query._fields = options.fields; + } + const queryString = qsify(query); + if (queryString) { + return path + "?" + queryString; + } + return path; +} +/** + * Replace authorization header with an obscured version + */ +function obscureAuthorizationHeader(headers) { + const h = new Headers(headers); + if (h.has("authorization")) { + h.set("authorization", "**** (suppressed)"); + } + const obscuredHeaders = {}; + for (const [header, value] of h.entries()) { + obscuredHeaders[header] = value; + } + return obscuredHeaders; +} + +/** + * Kinto server error code descriptors. + */ +const ERROR_CODES = { + 104: "Missing Authorization Token", + 105: "Invalid Authorization Token", + 106: "Request body was not valid JSON", + 107: "Invalid request parameter", + 108: "Missing request parameter", + 109: "Invalid posted data", + 110: "Invalid Token / id", + 111: "Missing Token / id", + 112: "Content-Length header was not provided", + 113: "Request body too large", + 114: "Resource was created, updated or deleted meanwhile", + 115: "Method not allowed on this end point (hint: server may be readonly)", + 116: "Requested version not available on this server", + 117: "Client has sent too many requests", + 121: "Resource access is forbidden for this user", + 122: "Another resource violates constraint", + 201: "Service Temporary unavailable due to high load", + 202: "Service deprecated", + 999: "Internal Server Error", +}; +class NetworkTimeoutError extends Error { + constructor(url, options) { + super( + `Timeout while trying to access ${url} with ${JSON.stringify(options)}` + ); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, NetworkTimeoutError); + } + this.url = url; + this.options = options; + } +} +class UnparseableResponseError extends Error { + constructor(response, body, error) { + const { status } = response; + super( + `Response from server unparseable (HTTP ${ + status || 0 + }; ${error}): ${body}` + ); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, UnparseableResponseError); + } + this.status = status; + this.response = response; + this.stack = error.stack; + this.error = error; + } +} +/** + * "Error" subclass representing a >=400 response from the server. + * + * Whether or not this is an error depends on your application. + * + * The `json` field can be undefined if the server responded with an + * empty response body. This shouldn't generally happen. Most "bad" + * responses come with a JSON error description, or (if they're + * fronted by a CDN or nginx or something) occasionally non-JSON + * responses (which become UnparseableResponseErrors, above). + */ +class ServerResponse extends Error { + constructor(response, json) { + const { status } = response; + let { statusText } = response; + let errnoMsg; + if (json) { + // Try to fill in information from the JSON error. + statusText = json.error || statusText; + // Take errnoMsg from either ERROR_CODES or json.message. + if (json.errno && json.errno in ERROR_CODES) { + errnoMsg = ERROR_CODES[json.errno]; + } else if (json.message) { + errnoMsg = json.message; + } + // If we had both ERROR_CODES and json.message, and they differ, + // combine them. + if (errnoMsg && json.message && json.message !== errnoMsg) { + errnoMsg += ` (${json.message})`; + } + } + let message = `HTTP ${status} ${statusText}`; + if (errnoMsg) { + message += `: ${errnoMsg}`; + } + super(message.trim()); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ServerResponse); + } + this.response = response; + this.data = json; + } +} + +var errors = /*#__PURE__*/ Object.freeze({ + __proto__: null, + NetworkTimeoutError, + ServerResponse, + UnparseableResponseError, + default: ERROR_CODES, +}); + +/** + * Enhanced HTTP client for the Kinto protocol. + * @private + */ +class HTTP { + /** + * Default HTTP request headers applied to each outgoing request. + * + * @type {Object} + */ + static get DEFAULT_REQUEST_HEADERS() { + return { + Accept: "application/json", + "Content-Type": "application/json", + }; + } + /** + * Default options. + * + * @type {Object} + */ + static get defaultOptions() { + return { timeout: null, requestMode: "cors" }; + } + /** + * Constructor. + * + * @param {EventEmitter} events The event handler. + * @param {Object} [options={}} The options object. + * @param {Number} [options.timeout=null] The request timeout in ms, if any (default: `null`). + * @param {String} [options.requestMode="cors"] The HTTP request mode (default: `"cors"`). + */ + constructor(events, options = {}) { + // public properties + /** + * The event emitter instance. + * @type {EventEmitter} + */ + this.events = events; + /** + * The request mode. + * @see https://fetch.spec.whatwg.org/#requestmode + * @type {String} + */ + this.requestMode = options.requestMode || HTTP.defaultOptions.requestMode; + /** + * The request timeout. + * @type {Number} + */ + this.timeout = options.timeout || HTTP.defaultOptions.timeout; + /** + * The fetch() function. + * @type {Function} + */ + this.fetchFunc = options.fetchFunc || globalThis.fetch.bind(globalThis); + } + /** + * @private + */ + timedFetch(url, options) { + let hasTimedout = false; + return new Promise((resolve, reject) => { + // Detect if a request has timed out. + let _timeoutId; + if (this.timeout) { + _timeoutId = setTimeout(() => { + hasTimedout = true; + if (options && options.headers) { + options = { + ...options, + headers: obscureAuthorizationHeader(options.headers), + }; + } + reject(new NetworkTimeoutError(url, options)); + }, this.timeout); + } + function proceedWithHandler(fn) { + return arg => { + if (!hasTimedout) { + if (_timeoutId) { + clearTimeout(_timeoutId); + } + fn(arg); + } + }; + } + this.fetchFunc(url, options) + .then(proceedWithHandler(resolve)) + .catch(proceedWithHandler(reject)); + }); + } + /** + * @private + */ + async processResponse(response) { + const { status, headers } = response; + const text = await response.text(); + // Check if we have a body; if so parse it as JSON. + let json; + if (text.length !== 0) { + try { + json = JSON.parse(text); + } catch (err) { + throw new UnparseableResponseError(response, text, err); + } + } + if (status >= 400) { + throw new ServerResponse(response, json); + } + return { status, json: json, headers }; + } + /** + * @private + */ + async retry(url, retryAfter, request, options) { + await delay(retryAfter); + return this.request(url, request, { + ...options, + retry: options.retry - 1, + }); + } + /** + * Performs an HTTP request to the Kinto server. + * + * Resolves with an objet containing the following HTTP response properties: + * - `{Number} status` The HTTP status code. + * - `{Object} json` The JSON response body. + * - `{Headers} headers` The response headers object; see the ES6 fetch() spec. + * + * @param {String} url The URL. + * @param {Object} [request={}] The request object, passed to + * fetch() as its options object. + * @param {Object} [request.headers] The request headers object (default: {}) + * @param {Object} [options={}] Options for making the + * request + * @param {Number} [options.retry] Number of retries (default: 0) + * @return {Promise} + */ + async request(url, request = { headers: {} }, options = { retry: 0 }) { + // Ensure default request headers are always set + request.headers = { ...HTTP.DEFAULT_REQUEST_HEADERS, ...request.headers }; + // If a multipart body is provided, remove any custom Content-Type header as + // the fetch() implementation will add the correct one for us. + if (request.body && request.body instanceof FormData) { + if (request.headers instanceof Headers) { + request.headers.delete("Content-Type"); + } else if (!Array.isArray(request.headers)) { + delete request.headers["Content-Type"]; + } + } + request.mode = this.requestMode; + const response = await this.timedFetch(url, request); + const { headers } = response; + this._checkForDeprecationHeader(headers); + this._checkForBackoffHeader(headers); + // Check if the server summons the client to retry after a while. + const retryAfter = this._checkForRetryAfterHeader(headers); + // If number of allowed of retries is not exhausted, retry the same request. + if (retryAfter && options.retry > 0) { + return this.retry(url, retryAfter, request, options); + } + return this.processResponse(response); + } + _checkForDeprecationHeader(headers) { + const alertHeader = headers.get("Alert"); + if (!alertHeader) { + return; + } + let alert; + try { + alert = JSON.parse(alertHeader); + } catch (err) { + console.warn("Unable to parse Alert header message", alertHeader); + return; + } + console.warn(alert.message, alert.url); + if (this.events) { + this.events.emit("deprecated", alert); + } + } + _checkForBackoffHeader(headers) { + let backoffMs; + const backoffHeader = headers.get("Backoff"); + const backoffSeconds = backoffHeader ? parseInt(backoffHeader, 10) : 0; + if (backoffSeconds > 0) { + backoffMs = new Date().getTime() + backoffSeconds * 1000; + } else { + backoffMs = 0; + } + if (this.events) { + this.events.emit("backoff", backoffMs); + } + } + _checkForRetryAfterHeader(headers) { + const retryAfter = headers.get("Retry-After"); + if (!retryAfter) { + return null; + } + const delay = parseInt(retryAfter, 10) * 1000; + const tryAgainAfter = new Date().getTime() + delay; + if (this.events) { + this.events.emit("retry-after", tryAgainAfter); + } + return delay; + } +} + +/** + * Endpoints templates. + * @type {Object} + */ +const ENDPOINTS = { + root: () => "/", + batch: () => "/batch", + permissions: () => "/permissions", + bucket: bucket => "/buckets" + (bucket ? `/${bucket}` : ""), + history: bucket => `${ENDPOINTS.bucket(bucket)}/history`, + collection: (bucket, coll) => + `${ENDPOINTS.bucket(bucket)}/collections` + (coll ? `/${coll}` : ""), + group: (bucket, group) => + `${ENDPOINTS.bucket(bucket)}/groups` + (group ? `/${group}` : ""), + record: (bucket, coll, id) => + `${ENDPOINTS.collection(bucket, coll)}/records` + (id ? `/${id}` : ""), + attachment: (bucket, coll, id) => + `${ENDPOINTS.record(bucket, coll, id)}/attachment`, +}; + +const requestDefaults = { + safe: false, + // check if we should set default content type here + headers: {}, + patch: false, +}; +/** + * @private + */ +function safeHeader(safe, last_modified) { + if (!safe) { + return {}; + } + if (last_modified) { + return { "If-Match": `"${last_modified}"` }; + } + return { "If-None-Match": "*" }; +} +/** + * @private + */ +function createRequest(path, { data, permissions }, options = {}) { + const { headers, safe } = { + ...requestDefaults, + ...options, + }; + const method = options.method || (data && data.id) ? "PUT" : "POST"; + return { + method, + path, + headers: { ...headers, ...safeHeader(safe) }, + body: { data, permissions }, + }; +} +/** + * @private + */ +function updateRequest(path, { data, permissions }, options = {}) { + const { headers, safe, patch } = { ...requestDefaults, ...options }; + const { last_modified } = { ...data, ...options }; + const hasNoData = + data && + Object.keys(data).filter(k => k !== "id" && k !== "last_modified") + .length === 0; + if (hasNoData) { + data = undefined; + } + return { + method: patch ? "PATCH" : "PUT", + path, + headers: { ...headers, ...safeHeader(safe, last_modified) }, + body: { data, permissions }, + }; +} +/** + * @private + */ +function jsonPatchPermissionsRequest(path, permissions, opType, options = {}) { + const { headers, safe, last_modified } = { ...requestDefaults, ...options }; + const ops = []; + for (const [type, principals] of Object.entries(permissions)) { + if (principals) { + for (const principal of principals) { + ops.push({ + op: opType, + path: `/permissions/${type}/${principal}`, + }); + } + } + } + return { + method: "PATCH", + path, + headers: { + ...headers, + ...safeHeader(safe, last_modified), + "Content-Type": "application/json-patch+json", + }, + body: ops, + }; +} +/** + * @private + */ +function deleteRequest(path, options = {}) { + const { headers, safe, last_modified } = { + ...requestDefaults, + ...options, + }; + if (safe && !last_modified) { + throw new Error("Safe concurrency check requires a last_modified value."); + } + return { + method: "DELETE", + path, + headers: { ...headers, ...safeHeader(safe, last_modified) }, + }; +} +/** + * @private + */ +function addAttachmentRequest( + path, + dataURI, + { data, permissions } = {}, + options = {} +) { + const { headers, safe, gzipped } = { ...requestDefaults, ...options }; + const { last_modified } = { ...data, ...options }; + const body = { data, permissions }; + const formData = createFormData(dataURI, body, options); + const customPath = `${path}${ + gzipped !== null ? "?gzipped=" + (gzipped ? "true" : "false") : "" + }`; + return { + method: "POST", + path: customPath, + headers: { ...headers, ...safeHeader(safe, last_modified) }, + body: formData, + }; +} + +/** + * Exports batch responses as a result object. + * + * @private + * @param {Array} responses The batch subrequest responses. + * @param {Array} requests The initial issued requests. + * @return {Object} + */ +function aggregate(responses = [], requests = []) { + if (responses.length !== requests.length) { + throw new Error("Responses length should match requests one."); + } + const results = { + errors: [], + published: [], + conflicts: [], + skipped: [], + }; + return responses.reduce((acc, response, index) => { + const { status } = response; + const request = requests[index]; + if (status >= 200 && status < 400) { + acc.published.push(response.body); + } else if (status === 404) { + // Extract the id manually from request path while waiting for Kinto/kinto#818 + const regex = /(buckets|groups|collections|records)\/([^/]+)$/; + const extracts = request.path.match(regex); + const id = extracts && extracts.length === 3 ? extracts[2] : undefined; + acc.skipped.push({ + id, + path: request.path, + error: response.body, + }); + } else if (status === 412) { + acc.conflicts.push({ + // XXX: specifying the type is probably superfluous + type: "outgoing", + local: request.body, + remote: + (response.body.details && response.body.details.existing) || null, + }); + } else { + acc.errors.push({ + path: request.path, + sent: request, + error: response.body, + }); + } + return acc; + }, results); +} + +// Unique ID creation requires a high quality random # generator. In the browser we therefore +// require the crypto API and do not support built-in fallback to lower quality random number +// generators (like Math.random()). +let getRandomValues; +const rnds8 = new Uint8Array(16); +function rng() { + // lazy load so that environments that need to polyfill have a chance to do so + if (!getRandomValues) { + // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. + getRandomValues = + typeof crypto !== "undefined" && + crypto.getRandomValues && + crypto.getRandomValues.bind(crypto); + + if (!getRandomValues) { + throw new Error( + "crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported" + ); + } + } + + return getRandomValues(rnds8); +} + +/** + * Convert array of 16 byte values to UUID string format of the form: + * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + */ + +const byteToHex = []; + +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 0x100).toString(16).slice(1)); +} + +function unsafeStringify(arr, offset = 0) { + // Note: Be careful editing this code! It's been tuned for performance + // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 + return ( + byteToHex[arr[offset + 0]] + + byteToHex[arr[offset + 1]] + + byteToHex[arr[offset + 2]] + + byteToHex[arr[offset + 3]] + + "-" + + byteToHex[arr[offset + 4]] + + byteToHex[arr[offset + 5]] + + "-" + + byteToHex[arr[offset + 6]] + + byteToHex[arr[offset + 7]] + + "-" + + byteToHex[arr[offset + 8]] + + byteToHex[arr[offset + 9]] + + "-" + + byteToHex[arr[offset + 10]] + + byteToHex[arr[offset + 11]] + + byteToHex[arr[offset + 12]] + + byteToHex[arr[offset + 13]] + + byteToHex[arr[offset + 14]] + + byteToHex[arr[offset + 15]] + ).toLowerCase(); +} + +const randomUUID = + typeof crypto !== "undefined" && + crypto.randomUUID && + crypto.randomUUID.bind(crypto); +var native = { + randomUUID, +}; + +function v4(options, buf, offset) { + if (native.randomUUID && !buf && !options) { + return native.randomUUID(); + } + + options = options || {}; + const rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; // Copy bytes to buffer, if provided + + if (buf) { + offset = offset || 0; + + for (let i = 0; i < 16; ++i) { + buf[offset + i] = rnds[i]; + } + + return buf; + } + + return unsafeStringify(rnds); +} + +/** + * Abstract representation of a selected collection. + * + */ +class Collection { + /** + * Constructor. + * + * @param {KintoClient} client The client instance. + * @param {Bucket} bucket The bucket instance. + * @param {String} name The collection name. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.retry] The retry option. + * @param {Boolean} [options.batch] (Private) Whether this + * Collection is operating as part of a batch. + */ + constructor(client, bucket, name, options = {}) { + /** + * @ignore + */ + this.client = client; + /** + * @ignore + */ + this.bucket = bucket; + /** + * The collection name. + * @type {String} + */ + this.name = name; + this._endpoints = client.endpoints; + /** + * @ignore + */ + this._retry = options.retry || 0; + this._safe = !!options.safe; + // FIXME: This is kind of ugly; shouldn't the bucket be responsible + // for doing the merge? + this._headers = { + ...this.bucket.headers, + ...options.headers, + }; + } + get execute() { + return this.client.execute.bind(this.client); + } + /** + * Get the value of "headers" for a given request, merging the + * per-request headers with our own "default" headers. + * + * @private + */ + _getHeaders(options) { + return { + ...this._headers, + ...options.headers, + }; + } + /** + * Get the value of "safe" for a given request, using the + * per-request option if present or falling back to our default + * otherwise. + * + * @private + * @param {Object} options The options for a request. + * @returns {Boolean} + */ + _getSafe(options) { + return { safe: this._safe, ...options }.safe; + } + /** + * As _getSafe, but for "retry". + * + * @private + */ + _getRetry(options) { + return { retry: this._retry, ...options }.retry; + } + /** + * Retrieves the total number of records in this collection. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getTotalRecords(options = {}) { + const path = this._endpoints.record(this.bucket.name, this.name); + const request = { + headers: this._getHeaders(options), + path, + method: "HEAD", + }; + const { headers } = await this.client.execute(request, { + raw: true, + retry: this._getRetry(options), + }); + return parseInt(headers.get("Total-Records"), 10); + } + /** + * Retrieves the ETag of the records list, for use with the `since` filtering option. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getRecordsTimestamp(options = {}) { + const path = this._endpoints.record(this.bucket.name, this.name); + const request = { + headers: this._getHeaders(options), + path, + method: "HEAD", + }; + const { headers } = await this.client.execute(request, { + raw: true, + retry: this._getRetry(options), + }); + return headers.get("ETag"); + } + /** + * Retrieves collection data. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Object} [options.query] Query parameters to pass in + * the request. This might be useful for features that aren't + * yet supported by this library. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getData(options = {}) { + const path = this._endpoints.collection(this.bucket.name, this.name); + const request = { headers: this._getHeaders(options), path }; + const { data } = await this.client.execute(request, { + retry: this._getRetry(options), + query: options.query, + fields: options.fields, + }); + return data; + } + /** + * Set collection data. + * @param {Object} data The collection data object. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Boolean} [options.patch] The patch option. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async setData(data, options = {}) { + if (!isObject(data)) { + throw new Error("A collection object is required."); + } + const { patch, permissions } = options; + const { last_modified } = { ...data, ...options }; + const path = this._endpoints.collection(this.bucket.name, this.name); + const request = updateRequest( + path, + { data, permissions }, + { + last_modified, + patch, + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Retrieves the list of permissions for this collection. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getPermissions(options = {}) { + const path = this._endpoints.collection(this.bucket.name, this.name); + const request = { headers: this._getHeaders(options), path }; + const { permissions } = await this.client.execute(request, { + retry: this._getRetry(options), + }); + return permissions; + } + /** + * Replaces all existing collection permissions with the ones provided. + * + * @param {Object} permissions The permissions object. + * @param {Object} [options={}] The options object + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async setPermissions(permissions, options = {}) { + if (!isObject(permissions)) { + throw new Error("A permissions object is required."); + } + const path = this._endpoints.collection(this.bucket.name, this.name); + const data = { last_modified: options.last_modified }; + const request = updateRequest( + path, + { data, permissions }, + { + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Append principals to the collection permissions. + * + * @param {Object} permissions The permissions object. + * @param {Object} [options={}] The options object + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async addPermissions(permissions, options = {}) { + if (!isObject(permissions)) { + throw new Error("A permissions object is required."); + } + const path = this._endpoints.collection(this.bucket.name, this.name); + const { last_modified } = options; + const request = jsonPatchPermissionsRequest(path, permissions, "add", { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Remove principals from the collection permissions. + * + * @param {Object} permissions The permissions object. + * @param {Object} [options={}] The options object + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async removePermissions(permissions, options = {}) { + if (!isObject(permissions)) { + throw new Error("A permissions object is required."); + } + const path = this._endpoints.collection(this.bucket.name, this.name); + const { last_modified } = options; + const request = jsonPatchPermissionsRequest(path, permissions, "remove", { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Creates a record in current collection. + * + * @param {Object} record The record to create. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.permissions] The permissions option. + * @return {Promise} + */ + async createRecord(record, options = {}) { + const { permissions } = options; + const path = this._endpoints.record(this.bucket.name, this.name, record.id); + const request = createRequest( + path, + { data: record, permissions }, + { + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Adds an attachment to a record, creating the record when it doesn't exist. + * + * @param {String} dataURL The data url. + * @param {Object} [record={}] The record data. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.last_modified] The last_modified option. + * @param {Object} [options.permissions] The permissions option. + * @param {String} [options.filename] Force the attachment filename. + * @param {String} [options.gzipped] Force the attachment to be gzipped or not. + * @return {Promise} + */ + async addAttachment(dataURI, record = {}, options = {}) { + const { permissions } = options; + const id = record.id || v4(); + const path = this._endpoints.attachment(this.bucket.name, this.name, id); + const { last_modified } = { ...record, ...options }; + const addAttachmentRequest$1 = addAttachmentRequest( + path, + dataURI, + { data: record, permissions }, + { + last_modified, + filename: options.filename, + gzipped: options.gzipped, + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + await this.client.execute(addAttachmentRequest$1, { + stringify: false, + retry: this._getRetry(options), + }); + return this.getRecord(id); + } + /** + * Removes an attachment from a given record. + * + * @param {Object} recordId The record id. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.last_modified] The last_modified option. + */ + async removeAttachment(recordId, options = {}) { + const { last_modified } = options; + const path = this._endpoints.attachment( + this.bucket.name, + this.name, + recordId + ); + const request = deleteRequest(path, { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Updates a record in current collection. + * + * @param {Object} record The record to update. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.last_modified] The last_modified option. + * @param {Object} [options.permissions] The permissions option. + * @return {Promise} + */ + async updateRecord(record, options = {}) { + if (!isObject(record)) { + throw new Error("A record object is required."); + } + if (!record.id) { + throw new Error("A record id is required."); + } + const { permissions } = options; + const { last_modified } = { ...record, ...options }; + const path = this._endpoints.record(this.bucket.name, this.name, record.id); + const request = updateRequest( + path, + { data: record, permissions }, + { + headers: this._getHeaders(options), + safe: this._getSafe(options), + last_modified, + patch: !!options.patch, + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Deletes a record from the current collection. + * + * @param {Object|String} record The record to delete. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async deleteRecord(record, options = {}) { + const recordObj = toDataBody(record); + if (!recordObj.id) { + throw new Error("A record id is required."); + } + const { id } = recordObj; + const { last_modified } = { ...recordObj, ...options }; + const path = this._endpoints.record(this.bucket.name, this.name, id); + const request = deleteRequest(path, { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Deletes records from the current collection. + * + * Sorting is done by passing a `sort` string option: + * + * - The field to order the results by, prefixed with `-` for descending. + * Default: `-last_modified`. + * + * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html + * + * Filtering is done by passing a `filters` option object: + * + * - `{fieldname: "value"}` + * - `{min_fieldname: 4000}` + * - `{in_fieldname: "1,2,3"}` + * - `{not_fieldname: 0}` + * - `{exclude_fieldname: "0,1"}` + * + * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.filters={}] The filters object. + * @param {String} [options.sort="-last_modified"] The sort field. + * @param {String} [options.at] The timestamp to get a snapshot at. + * @param {String} [options.limit=null] The limit field. + * @param {String} [options.pages=1] The number of result pages to aggregate. + * @param {Number} [options.since=null] Only retrieve records modified since the provided timestamp. + * @param {Array} [options.fields] Limit response to just some fields. + * @return {Promise} + */ + async deleteRecords(options = {}) { + const path = this._endpoints.record(this.bucket.name, this.name); + return this.client.paginatedDelete(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Retrieves a record from the current collection. + * + * @param {String} id The record id to retrieve. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Object} [options.query] Query parameters to pass in + * the request. This might be useful for features that aren't + * yet supported by this library. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getRecord(id, options = {}) { + const path = this._endpoints.record(this.bucket.name, this.name, id); + const request = { headers: this._getHeaders(options), path }; + return this.client.execute(request, { + retry: this._getRetry(options), + query: options.query, + fields: options.fields, + }); + } + /** + * Lists records from the current collection. + * + * Sorting is done by passing a `sort` string option: + * + * - The field to order the results by, prefixed with `-` for descending. + * Default: `-last_modified`. + * + * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html + * + * Filtering is done by passing a `filters` option object: + * + * - `{fieldname: "value"}` + * - `{min_fieldname: 4000}` + * - `{in_fieldname: "1,2,3"}` + * - `{not_fieldname: 0}` + * - `{exclude_fieldname: "0,1"}` + * + * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html + * + * Paginating is done by passing a `limit` option, then calling the `next()` + * method from the resolved result object to fetch the next page, if any. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.filters={}] The filters object. + * @param {String} [options.sort="-last_modified"] The sort field. + * @param {String} [options.at] The timestamp to get a snapshot at. + * @param {String} [options.limit=null] The limit field. + * @param {String} [options.pages=1] The number of result pages to aggregate. + * @param {Number} [options.since=null] Only retrieve records modified since the provided timestamp. + * @param {Array} [options.fields] Limit response to just some fields. + * @return {Promise} + */ + async listRecords(options = {}) { + const path = this._endpoints.record(this.bucket.name, this.name); + if (options.at) { + return this.getSnapshot(options.at); + } + return this.client.paginatedList(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * @private + */ + async isHistoryComplete() { + // We consider that if we have the collection creation event part of the + // history, then all records change events have been tracked. + const { + data: [oldestHistoryEntry], + } = await this.bucket.listHistory({ + limit: 1, + filters: { + action: "create", + resource_name: "collection", + collection_id: this.name, + }, + }); + return !!oldestHistoryEntry; + } + /** + * @private + */ + async getSnapshot(at) { + if (!at || !Number.isInteger(at) || at <= 0) { + throw new Error("Invalid argument, expected a positive integer."); + } + // Retrieve history and check it covers the required time range. + // Ensure we have enough history data to retrieve the complete list of + // changes. + if (!(await this.isHistoryComplete())) { + throw new Error( + "Computing a snapshot is only possible when the full history for a " + + "collection is available. Here, the history plugin seems to have " + + "been enabled after the creation of the collection." + ); + } + // Because of https://github.com/Kinto/kinto-http.js/issues/963 + // we cannot simply rely on the history endpoint. + // Our strategy here is to clean-up the history entries from the + // records that were deleted via the plural endpoint. + // We will detect them by comparing the current state of the collection + // and the full history of the collection since its genesis. + // List full history of collection. + const { data: fullHistory } = await this.bucket.listHistory({ + pages: Infinity, + sort: "last_modified", + filters: { + resource_name: "record", + collection_id: this.name, + }, + }); + // Keep latest entry ever, and latest within snapshot window. + // (history is sorted chronologically) + const latestEver = new Map(); + const latestInSnapshot = new Map(); + for (const entry of fullHistory) { + if (entry.target.data.last_modified <= at) { + // Snapshot includes changes right on timestamp. + latestInSnapshot.set(entry.record_id, entry); + } + latestEver.set(entry.record_id, entry); + } + // Current records ids in the collection. + const { data: current } = await this.listRecords({ + pages: Infinity, + fields: ["id"], // we don't need attributes. + }); + const currentIds = new Set(current.map(record => record.id)); + // If a record is not in the current collection, and its + // latest history entry isn't a delete then this means that + // it was deleted via the plural endpoint (and that we lost track + // of this deletion because of bug #963) + const deletedViaPlural = new Set(); + for (const entry of latestEver.values()) { + if (entry.action != "delete" && !currentIds.has(entry.record_id)) { + deletedViaPlural.add(entry.record_id); + } + } + // Now reconstruct the collection based on latest version in snapshot + // filtering all deleted records. + const reconstructed = []; + for (const entry of latestInSnapshot.values()) { + if (entry.action != "delete" && !deletedViaPlural.has(entry.record_id)) { + reconstructed.push(entry.target.data); + } + } + return { + last_modified: String(at), + data: Array.from(reconstructed).sort( + (a, b) => b.last_modified - a.last_modified + ), + next: () => { + throw new Error("Snapshots don't support pagination"); + }, + hasNextPage: false, + totalRecords: reconstructed.length, + }; + } + /** + * Performs batch operations at the current collection level. + * + * @param {Function} fn The batch operation function. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.retry] The retry option. + * @param {Boolean} [options.aggregate] Produces a grouped result object. + * @return {Promise} + */ + async batch(fn, options = {}) { + return this.client.batch(fn, { + bucket: this.bucket.name, + collection: this.name, + headers: this._getHeaders(options), + retry: this._getRetry(options), + safe: this._getSafe(options), + aggregate: !!options.aggregate, + }); + } +} +__decorate( + [capable(["attachments"])], + Collection.prototype, + "addAttachment", + null +); +__decorate( + [capable(["attachments"])], + Collection.prototype, + "removeAttachment", + null +); +__decorate([capable(["history"])], Collection.prototype, "getSnapshot", null); + +/** + * Abstract representation of a selected bucket. + * + */ +class Bucket { + /** + * Constructor. + * + * @param {KintoClient} client The client instance. + * @param {String} name The bucket name. + * @param {Object} [options={}] The headers object option. + * @param {Object} [options.headers] The headers object option. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.retry] The retry option. + */ + constructor(client, name, options = {}) { + /** + * @ignore + */ + this.client = client; + /** + * The bucket name. + * @type {String} + */ + this.name = name; + this._endpoints = client.endpoints; + /** + * @ignore + */ + this._headers = options.headers || {}; + this._retry = options.retry || 0; + this._safe = !!options.safe; + } + get execute() { + return this.client.execute.bind(this.client); + } + get headers() { + return this._headers; + } + /** + * Get the value of "headers" for a given request, merging the + * per-request headers with our own "default" headers. + * + * @private + */ + _getHeaders(options) { + return { + ...this._headers, + ...options.headers, + }; + } + /** + * Get the value of "safe" for a given request, using the + * per-request option if present or falling back to our default + * otherwise. + * + * @private + * @param {Object} options The options for a request. + * @returns {Boolean} + */ + _getSafe(options) { + return { safe: this._safe, ...options }.safe; + } + /** + * As _getSafe, but for "retry". + * + * @private + */ + _getRetry(options) { + return { retry: this._retry, ...options }.retry; + } + /** + * Selects a collection. + * + * @param {String} name The collection name. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Boolean} [options.safe] The safe option. + * @return {Collection} + */ + collection(name, options = {}) { + return new Collection(this.client, this, name, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + safe: this._getSafe(options), + }); + } + /** + * Retrieves the ETag of the collection list, for use with the `since` filtering option. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getCollectionsTimestamp(options = {}) { + const path = this._endpoints.collection(this.name); + const request = { + headers: this._getHeaders(options), + path, + method: "HEAD", + }; + const { headers } = await this.client.execute(request, { + raw: true, + retry: this._getRetry(options), + }); + return headers.get("ETag"); + } + /** + * Retrieves the ETag of the group list, for use with the `since` filtering option. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getGroupsTimestamp(options = {}) { + const path = this._endpoints.group(this.name); + const request = { + headers: this._getHeaders(options), + path, + method: "HEAD", + }; + const { headers } = await this.client.execute(request, { + raw: true, + retry: this._getRetry(options), + }); + return headers.get("ETag"); + } + /** + * Retrieves bucket data. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Object} [options.query] Query parameters to pass in + * the request. This might be useful for features that aren't + * yet supported by this library. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getData(options = {}) { + const path = this._endpoints.bucket(this.name); + const request = { + headers: this._getHeaders(options), + path, + }; + const { data } = await this.client.execute(request, { + retry: this._getRetry(options), + query: options.query, + fields: options.fields, + }); + return data; + } + /** + * Set bucket data. + * @param {Object} data The bucket data object. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers={}] The headers object option. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.patch] The patch option. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async setData(data, options = {}) { + if (!isObject(data)) { + throw new Error("A bucket object is required."); + } + const bucket = { + ...data, + id: this.name, + }; + // For default bucket, we need to drop the id from the data object. + // Bug in Kinto < 3.1.1 + const bucketId = bucket.id; + if (bucket.id === "default") { + delete bucket.id; + } + const path = this._endpoints.bucket(bucketId); + const { patch, permissions } = options; + const { last_modified } = { ...data, ...options }; + const request = updateRequest( + path, + { data: bucket, permissions }, + { + last_modified, + patch, + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Retrieves the list of history entries in the current bucket. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise, Error>} + */ + async listHistory(options = {}) { + const path = this._endpoints.history(this.name); + return this.client.paginatedList(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Retrieves the list of collections in the current bucket. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.filters={}] The filters object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @return {Promise, Error>} + */ + async listCollections(options = {}) { + const path = this._endpoints.collection(this.name); + return this.client.paginatedList(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Creates a new collection in current bucket. + * + * @param {String|undefined} id The collection id. + * @param {Object} [options={}] The options object. + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.permissions] The permissions object. + * @param {Object} [options.data] The data object. + * @return {Promise} + */ + async createCollection(id, options = {}) { + const { permissions, data = {} } = options; + data.id = id; + const path = this._endpoints.collection(this.name, id); + const request = createRequest( + path, + { data, permissions }, + { + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Deletes a collection from the current bucket. + * + * @param {Object|String} collection The collection to delete. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async deleteCollection(collection, options = {}) { + const collectionObj = toDataBody(collection); + if (!collectionObj.id) { + throw new Error("A collection id is required."); + } + const { id } = collectionObj; + const { last_modified } = { ...collectionObj, ...options }; + const path = this._endpoints.collection(this.name, id); + const request = deleteRequest(path, { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Deletes collections from the current bucket. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.filters={}] The filters object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @return {Promise, Error>} + */ + async deleteCollections(options = {}) { + const path = this._endpoints.collection(this.name); + return this.client.paginatedDelete(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Retrieves the list of groups in the current bucket. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.filters={}] The filters object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @return {Promise, Error>} + */ + async listGroups(options = {}) { + const path = this._endpoints.group(this.name); + return this.client.paginatedList(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Fetches a group in current bucket. + * + * @param {String} id The group id. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.query] Query parameters to pass in + * the request. This might be useful for features that aren't + * yet supported by this library. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @return {Promise} + */ + async getGroup(id, options = {}) { + const path = this._endpoints.group(this.name, id); + const request = { + headers: this._getHeaders(options), + path, + }; + return this.client.execute(request, { + retry: this._getRetry(options), + query: options.query, + fields: options.fields, + }); + } + /** + * Creates a new group in current bucket. + * + * @param {String|undefined} id The group id. + * @param {Array} [members=[]] The list of principals. + * @param {Object} [options={}] The options object. + * @param {Object} [options.data] The data object. + * @param {Object} [options.permissions] The permissions object. + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async createGroup(id, members = [], options = {}) { + const data = { + ...options.data, + id, + members, + }; + const path = this._endpoints.group(this.name, id); + const { permissions } = options; + const request = createRequest( + path, + { data, permissions }, + { + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Updates an existing group in current bucket. + * + * @param {Object} group The group object. + * @param {Object} [options={}] The options object. + * @param {Object} [options.data] The data object. + * @param {Object} [options.permissions] The permissions object. + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async updateGroup(group, options = {}) { + if (!isObject(group)) { + throw new Error("A group object is required."); + } + if (!group.id) { + throw new Error("A group id is required."); + } + const data = { + ...options.data, + ...group, + }; + const path = this._endpoints.group(this.name, group.id); + const { patch, permissions } = options; + const { last_modified } = { ...data, ...options }; + const request = updateRequest( + path, + { data, permissions }, + { + last_modified, + patch, + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Deletes a group from the current bucket. + * + * @param {Object|String} group The group to delete. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async deleteGroup(group, options = {}) { + const groupObj = toDataBody(group); + const { id } = groupObj; + const { last_modified } = { ...groupObj, ...options }; + const path = this._endpoints.group(this.name, id); + const request = deleteRequest(path, { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Deletes groups from the current bucket. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.filters={}] The filters object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @return {Promise, Error>} + */ + async deleteGroups(options = {}) { + const path = this._endpoints.group(this.name); + return this.client.paginatedDelete(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Retrieves the list of permissions for this bucket. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async getPermissions(options = {}) { + const request = { + headers: this._getHeaders(options), + path: this._endpoints.bucket(this.name), + }; + const { permissions } = await this.client.execute(request, { + retry: this._getRetry(options), + }); + return permissions; + } + /** + * Replaces all existing bucket permissions with the ones provided. + * + * @param {Object} permissions The permissions object. + * @param {Object} [options={}] The options object + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers={}] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async setPermissions(permissions, options = {}) { + if (!isObject(permissions)) { + throw new Error("A permissions object is required."); + } + const path = this._endpoints.bucket(this.name); + const { last_modified } = options; + const data = { last_modified }; + const request = updateRequest( + path, + { data, permissions }, + { + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Append principals to the bucket permissions. + * + * @param {Object} permissions The permissions object. + * @param {Object} [options={}] The options object + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async addPermissions(permissions, options = {}) { + if (!isObject(permissions)) { + throw new Error("A permissions object is required."); + } + const path = this._endpoints.bucket(this.name); + const { last_modified } = options; + const request = jsonPatchPermissionsRequest(path, permissions, "add", { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Remove principals from the bucket permissions. + * + * @param {Object} permissions The permissions object. + * @param {Object} [options={}] The options object + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async removePermissions(permissions, options = {}) { + if (!isObject(permissions)) { + throw new Error("A permissions object is required."); + } + const path = this._endpoints.bucket(this.name); + const { last_modified } = options; + const request = jsonPatchPermissionsRequest(path, permissions, "remove", { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }); + return this.client.execute(request, { + retry: this._getRetry(options), + }); + } + /** + * Performs batch operations at the current bucket level. + * + * @param {Function} fn The batch operation function. + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.retry=0] The retry option. + * @param {Boolean} [options.aggregate] Produces a grouped result object. + * @return {Promise} + */ + async batch(fn, options = {}) { + return this.client.batch(fn, { + bucket: this.name, + headers: this._getHeaders(options), + retry: this._getRetry(options), + safe: this._getSafe(options), + aggregate: !!options.aggregate, + }); + } +} +__decorate([capable(["history"])], Bucket.prototype, "listHistory", null); + +/** + * Currently supported protocol version. + * @type {String} + */ +const SUPPORTED_PROTOCOL_VERSION = "v1"; +/** + * High level HTTP client for the Kinto API. + * + * @example + * const client = new KintoClient("https://demo.kinto-storage.org/v1"); + * client.bucket("default") + * .collection("my-blog") + * .createRecord({title: "First article"}) + * .then(console.log.bind(console)) + * .catch(console.error.bind(console)); + */ +class KintoClientBase { + /** + * Constructor. + * + * @param {String} remote The remote URL. + * @param {Object} [options={}] The options object. + * @param {Boolean} [options.safe=true] Adds concurrency headers to every requests. + * @param {EventEmitter} [options.events=EventEmitter] The events handler instance. + * @param {Object} [options.headers={}] The key-value headers to pass to each request. + * @param {Object} [options.retry=0] Number of retries when request fails (default: 0) + * @param {String} [options.bucket="default"] The default bucket to use. + * @param {String} [options.requestMode="cors"] The HTTP request mode (from ES6 fetch spec). + * @param {Number} [options.timeout=null] The request timeout in ms, if any. + * @param {Function} [options.fetchFunc=fetch] The function to be used to execute HTTP requests. + */ + constructor(remote, options) { + if (typeof remote !== "string" || !remote.length) { + throw new Error("Invalid remote URL: " + remote); + } + if (remote[remote.length - 1] === "/") { + remote = remote.slice(0, -1); + } + this._backoffReleaseTime = null; + this._requests = []; + this._isBatch = !!options.batch; + this._retry = options.retry || 0; + this._safe = !!options.safe; + this._headers = options.headers || {}; + // public properties + /** + * The remote server base URL. + * @type {String} + */ + this.remote = remote; + /** + * Current server information. + * @ignore + * @type {Object|null} + */ + this.serverInfo = null; + /** + * The event emitter instance. Should comply with the `EventEmitter` + * interface. + * @ignore + * @type {Class} + */ + this.events = options.events; + this.endpoints = ENDPOINTS; + const { fetchFunc, requestMode, timeout } = options; + /** + * The HTTP instance. + * @ignore + * @type {HTTP} + */ + this.http = new HTTP(this.events, { fetchFunc, requestMode, timeout }); + this._registerHTTPEvents(); + } + /** + * The remote endpoint base URL. Setting the value will also extract and + * validate the version. + * @type {String} + */ + get remote() { + return this._remote; + } + /** + * @ignore + */ + set remote(url) { + let version; + try { + version = url.match(/\/(v\d+)\/?$/)[1]; + } catch (err) { + throw new Error("The remote URL must contain the version: " + url); + } + if (version !== SUPPORTED_PROTOCOL_VERSION) { + throw new Error(`Unsupported protocol version: ${version}`); + } + this._remote = url; + this._version = version; + } + /** + * The current server protocol version, eg. `v1`. + * @type {String} + */ + get version() { + return this._version; + } + /** + * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is + * ongoing. + * + * @type {Number} + */ + get backoff() { + const currentTime = new Date().getTime(); + if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) { + return this._backoffReleaseTime - currentTime; + } + return 0; + } + /** + * Registers HTTP events. + * @private + */ + _registerHTTPEvents() { + // Prevent registering event from a batch client instance + if (!this._isBatch && this.events) { + this.events.on("backoff", backoffMs => { + this._backoffReleaseTime = backoffMs; + }); + } + } + /** + * Retrieve a bucket object to perform operations on it. + * + * @param {String} name The bucket name. + * @param {Object} [options={}] The request options. + * @param {Boolean} [options.safe] The resulting safe option. + * @param {Number} [options.retry] The resulting retry option. + * @param {Object} [options.headers] The extended headers object option. + * @return {Bucket} + */ + bucket(name, options = {}) { + return new Bucket(this, name, { + headers: this._getHeaders(options), + safe: this._getSafe(options), + retry: this._getRetry(options), + }); + } + /** + * Set client "headers" for every request, updating previous headers (if any). + * + * @param {Object} headers The headers to merge with existing ones. + */ + setHeaders(headers) { + this._headers = { + ...this._headers, + ...headers, + }; + this.serverInfo = null; + } + /** + * Get the value of "headers" for a given request, merging the + * per-request headers with our own "default" headers. + * + * Note that unlike other options, headers aren't overridden, but + * merged instead. + * + * @private + * @param {Object} options The options for a request. + * @returns {Object} + */ + _getHeaders(options) { + return { + ...this._headers, + ...options.headers, + }; + } + /** + * Get the value of "safe" for a given request, using the + * per-request option if present or falling back to our default + * otherwise. + * + * @private + * @param {Object} options The options for a request. + * @returns {Boolean} + */ + _getSafe(options) { + return { safe: this._safe, ...options }.safe; + } + /** + * As _getSafe, but for "retry". + * + * @private + */ + _getRetry(options) { + return { retry: this._retry, ...options }.retry; + } + /** + * Retrieves the server's "hello" endpoint. This endpoint reveals + * server capabilities and settings as well as telling the client + * "who they are" according to their given authorization headers. + * + * @private + * @param {Object} [options={}] The request options. + * @param {Object} [options.headers={}] Headers to use when making + * this request. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async _getHello(options = {}) { + const path = this.remote + ENDPOINTS.root(); + const { json } = await this.http.request( + path, + { headers: this._getHeaders(options) }, + { retry: this._getRetry(options) } + ); + return json; + } + /** + * Retrieves server information and persist them locally. This operation is + * usually performed a single time during the instance lifecycle. + * + * @param {Object} [options={}] The request options. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async fetchServerInfo(options = {}) { + if (this.serverInfo) { + return this.serverInfo; + } + this.serverInfo = await this._getHello({ retry: this._getRetry(options) }); + return this.serverInfo; + } + /** + * Retrieves Kinto server settings. + * + * @param {Object} [options={}] The request options. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async fetchServerSettings(options = {}) { + const { settings } = await this.fetchServerInfo(options); + return settings; + } + /** + * Retrieve server capabilities information. + * + * @param {Object} [options={}] The request options. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async fetchServerCapabilities(options = {}) { + const { capabilities } = await this.fetchServerInfo(options); + return capabilities; + } + /** + * Retrieve authenticated user information. + * + * @param {Object} [options={}] The request options. + * @param {Object} [options.headers={}] Headers to use when making + * this request. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async fetchUser(options = {}) { + const { user } = await this._getHello(options); + return user; + } + /** + * Retrieve authenticated user information. + * + * @param {Object} [options={}] The request options. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async fetchHTTPApiVersion(options = {}) { + const { http_api_version } = await this.fetchServerInfo(options); + return http_api_version; + } + /** + * Process batch requests, chunking them according to the batch_max_requests + * server setting when needed. + * + * @param {Array} requests The list of batch subrequests to perform. + * @param {Object} [options={}] The options object. + * @return {Promise} + */ + async _batchRequests(requests, options = {}) { + const headers = this._getHeaders(options); + if (!requests.length) { + return []; + } + const serverSettings = await this.fetchServerSettings({ + retry: this._getRetry(options), + }); + const maxRequests = serverSettings.batch_max_requests; + if (maxRequests && requests.length > maxRequests) { + const chunks = partition(requests, maxRequests); + const results = []; + for (const chunk of chunks) { + const result = await this._batchRequests(chunk, options); + results.push(...result); + } + return results; + } + const { responses } = await this.execute( + { + // FIXME: is this really necessary, since it's also present in + // the "defaults"? + headers, + path: ENDPOINTS.batch(), + method: "POST", + body: { + defaults: { headers }, + requests, + }, + }, + { retry: this._getRetry(options) } + ); + return responses; + } + /** + * Sends batch requests to the remote server. + * + * Note: Reserved for internal use only. + * + * @ignore + * @param {Function} fn The function to use for describing batch ops. + * @param {Object} [options={}] The options object. + * @param {Boolean} [options.safe] The safe option. + * @param {Number} [options.retry] The retry option. + * @param {String} [options.bucket] The bucket name option. + * @param {String} [options.collection] The collection name option. + * @param {Object} [options.headers] The headers object option. + * @param {Boolean} [options.aggregate=false] Produces an aggregated result object. + * @return {Promise} + */ + async batch(fn, options = {}) { + const rootBatch = new KintoClientBase(this.remote, { + events: this.events, + batch: true, + safe: this._getSafe(options), + retry: this._getRetry(options), + }); + if (options.bucket && options.collection) { + fn(rootBatch.bucket(options.bucket).collection(options.collection)); + } else if (options.bucket) { + fn(rootBatch.bucket(options.bucket)); + } else { + fn(rootBatch); + } + const responses = await this._batchRequests(rootBatch._requests, options); + if (options.aggregate) { + return aggregate(responses, rootBatch._requests); + } + return responses; + } + async execute(request, options = {}) { + const { raw = false, stringify = true } = options; + // If we're within a batch, add the request to the stack to send at once. + if (this._isBatch) { + this._requests.push(request); + // Resolve with a message in case people attempt at consuming the result + // from within a batch operation. + const msg = + "This result is generated from within a batch " + + "operation and should not be consumed."; + return raw ? { status: 0, json: msg, headers: new Headers() } : msg; + } + const uri = this.remote + addEndpointOptions(request.path, options); + const result = await this.http.request( + uri, + cleanUndefinedProperties({ + // Limit requests to only those parts that would be allowed in + // a batch request -- don't pass through other fancy fetch() + // options like integrity, redirect, mode because they will + // break on a batch request. A batch request only allows + // headers, method, path (above), and body. + method: request.method, + headers: request.headers, + body: stringify ? JSON.stringify(request.body) : request.body, + }), + { retry: this._getRetry(options) } + ); + return raw ? result : result.json; + } + /** + * Perform an operation with a given HTTP method on some pages from + * a paginated list, following the `next-page` header automatically + * until we have processed the requested number of pages. Return a + * response with a `.next()` method that can be called to perform + * the requested HTTP method on more results. + * + * @private + * @param {String} path + * The path to make the request to. + * @param {Object} params + * The parameters to use when making the request. + * @param {String} [params.sort="-last_modified"] + * The sorting order to use when doing operation on pages. + * @param {Object} [params.filters={}] + * The filters to send in the request. + * @param {Number} [params.limit=undefined] + * The limit to send in the request. Undefined means no limit. + * @param {Number} [params.pages=undefined] + * The number of pages to operate on. Undefined means one page. Pass + * Infinity to operate on everything. + * @param {String} [params.since=undefined] + * The ETag from which to start doing operation on pages. + * @param {Array} [params.fields] + * Limit response to just some fields. + * @param {Object} [options={}] + * Additional request-level parameters to use in all requests. + * @param {Object} [options.headers={}] + * Headers to use during all requests. + * @param {Number} [options.retry=0] + * Number of times to retry each request if the server responds + * with Retry-After. + * @param {String} [options.method="GET"] + * The method to use in the request. + */ + async paginatedOperation(path, params = {}, options = {}) { + // FIXME: this is called even in batch requests, which doesn't + // make any sense (since all batch requests get a "dummy" + // response; see execute() above). + const { sort, filters, limit, pages, since, fields } = { + sort: "-last_modified", + ...params, + }; + // Safety/Consistency check on ETag value. + if (since && typeof since !== "string") { + throw new Error( + `Invalid value for since (${since}), should be ETag value.` + ); + } + const query = { + ...filters, + _sort: sort, + _limit: limit, + _since: since, + }; + if (fields) { + query._fields = fields; + } + const querystring = qsify(query); + let results = [], + current = 0; + const next = async function (nextPage) { + if (!nextPage) { + throw new Error("Pagination exhausted."); + } + return processNextPage(nextPage); + }; + const processNextPage = async nextPage => { + const { headers } = options; + return handleResponse(await this.http.request(nextPage, { headers })); + }; + const pageResults = (results, nextPage, etag) => { + // ETag string is supposed to be opaque and stored «as-is». + // ETag header values are quoted (because of * and W/"foo"). + return { + last_modified: etag ? etag.replace(/"/g, "") : etag, + data: results, + next: next.bind(null, nextPage), + hasNextPage: !!nextPage, + totalRecords: -1, + }; + }; + const handleResponse = async function ({ + headers = new Headers(), + json = {}, + }) { + const nextPage = headers.get("Next-Page"); + const etag = headers.get("ETag"); + if (!pages) { + return pageResults(json.data, nextPage, etag); + } + // Aggregate new results with previous ones + results = results.concat(json.data); + current += 1; + if (current >= pages || !nextPage) { + // Pagination exhausted + return pageResults(results, nextPage, etag); + } + // Follow next page + return processNextPage(nextPage); + }; + return handleResponse( + await this.execute( + // N.B.: This doesn't use _getHeaders, because all calls to + // `paginatedList` are assumed to come from calls that already + // have headers merged at e.g. the bucket or collection level. + { + headers: options.headers ? options.headers : {}, + path: path + "?" + querystring, + method: options.method, + }, + // N.B. This doesn't use _getRetry, because all calls to + // `paginatedList` are assumed to come from calls that already + // used `_getRetry` at e.g. the bucket or collection level. + { raw: true, retry: options.retry || 0 } + ) + ); + } + /** + * Fetch some pages from a paginated list, following the `next-page` + * header automatically until we have fetched the requested number + * of pages. Return a response with a `.next()` method that can be + * called to fetch more results. + * + * @private + * @param {String} path + * The path to make the request to. + * @param {Object} params + * The parameters to use when making the request. + * @param {String} [params.sort="-last_modified"] + * The sorting order to use when fetching. + * @param {Object} [params.filters={}] + * The filters to send in the request. + * @param {Number} [params.limit=undefined] + * The limit to send in the request. Undefined means no limit. + * @param {Number} [params.pages=undefined] + * The number of pages to fetch. Undefined means one page. Pass + * Infinity to fetch everything. + * @param {String} [params.since=undefined] + * The ETag from which to start fetching. + * @param {Array} [params.fields] + * Limit response to just some fields. + * @param {Object} [options={}] + * Additional request-level parameters to use in all requests. + * @param {Object} [options.headers={}] + * Headers to use during all requests. + * @param {Number} [options.retry=0] + * Number of times to retry each request if the server responds + * with Retry-After. + */ + async paginatedList(path, params = {}, options = {}) { + return this.paginatedOperation(path, params, options); + } + /** + * Delete multiple objects, following the pagination if the number of + * objects exceeds the page limit until we have deleted the requested + * number of pages. Return a response with a `.next()` method that can + * be called to delete more results. + * + * @private + * @param {String} path + * The path to make the request to. + * @param {Object} params + * The parameters to use when making the request. + * @param {String} [params.sort="-last_modified"] + * The sorting order to use when deleting. + * @param {Object} [params.filters={}] + * The filters to send in the request. + * @param {Number} [params.limit=undefined] + * The limit to send in the request. Undefined means no limit. + * @param {Number} [params.pages=undefined] + * The number of pages to delete. Undefined means one page. Pass + * Infinity to delete everything. + * @param {String} [params.since=undefined] + * The ETag from which to start deleting. + * @param {Array} [params.fields] + * Limit response to just some fields. + * @param {Object} [options={}] + * Additional request-level parameters to use in all requests. + * @param {Object} [options.headers={}] + * Headers to use during all requests. + * @param {Number} [options.retry=0] + * Number of times to retry each request if the server responds + * with Retry-After. + */ + paginatedDelete(path, params = {}, options = {}) { + const { headers, safe, last_modified } = options; + const deleteRequest$1 = deleteRequest(path, { + headers, + safe: safe ? safe : false, + last_modified, + }); + return this.paginatedOperation(path, params, { + ...options, + headers: deleteRequest$1.headers, + method: "DELETE", + }); + } + /** + * Lists all permissions. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers={}] Headers to use when making + * this request. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async listPermissions(options = {}) { + const path = ENDPOINTS.permissions(); + // Ensure the default sort parameter is something that exists in permissions + // entries, as `last_modified` doesn't; here, we pick "id". + const paginationOptions = { sort: "id", ...options }; + return this.paginatedList(path, paginationOptions, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Retrieves the list of buckets. + * + * @param {Object} [options={}] The options object. + * @param {Object} [options.headers={}] Headers to use when making + * this request. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.filters={}] The filters object. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @return {Promise} + */ + async listBuckets(options = {}) { + const path = ENDPOINTS.bucket(); + return this.paginatedList(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + }); + } + /** + * Creates a new bucket on the server. + * + * @param {String|null} id The bucket name (optional). + * @param {Object} [options={}] The options object. + * @param {Boolean} [options.data] The bucket data option. + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @return {Promise} + */ + async createBucket(id, options = {}) { + const { data, permissions } = options; + const _data = { ...data, id: id ? id : undefined }; + const path = _data.id ? ENDPOINTS.bucket(_data.id) : ENDPOINTS.bucket(); + return this.execute( + createRequest( + path, + { data: _data, permissions }, + { + headers: this._getHeaders(options), + safe: this._getSafe(options), + } + ), + { retry: this._getRetry(options) } + ); + } + /** + * Deletes a bucket from the server. + * + * @ignore + * @param {Object|String} bucket The bucket to delete. + * @param {Object} [options={}] The options object. + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async deleteBucket(bucket, options = {}) { + const bucketObj = toDataBody(bucket); + if (!bucketObj.id) { + throw new Error("A bucket id is required."); + } + const path = ENDPOINTS.bucket(bucketObj.id); + const { last_modified } = { ...bucketObj, ...options }; + return this.execute( + deleteRequest(path, { + last_modified, + headers: this._getHeaders(options), + safe: this._getSafe(options), + }), + { retry: this._getRetry(options) } + ); + } + /** + * Deletes buckets. + * + * @param {Object} [options={}] The options object. + * @param {Boolean} [options.safe] The safe option. + * @param {Object} [options.headers={}] Headers to use when making + * this request. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Object} [options.filters={}] The filters object. + * @param {Array} [options.fields] Limit response to + * just some fields. + * @param {Number} [options.last_modified] The last_modified option. + * @return {Promise} + */ + async deleteBuckets(options = {}) { + const path = ENDPOINTS.bucket(); + return this.paginatedDelete(path, options, { + headers: this._getHeaders(options), + retry: this._getRetry(options), + safe: options.safe, + last_modified: options.last_modified, + }); + } + async createAccount(username, password) { + return this.execute( + createRequest( + `/accounts/${username}`, + { data: { password } }, + { method: "PUT" } + ) + ); + } +} +__decorate( + [nobatch("This operation is not supported within a batch operation.")], + KintoClientBase.prototype, + "fetchServerSettings", + null +); +__decorate( + [nobatch("This operation is not supported within a batch operation.")], + KintoClientBase.prototype, + "fetchServerCapabilities", + null +); +__decorate( + [nobatch("This operation is not supported within a batch operation.")], + KintoClientBase.prototype, + "fetchUser", + null +); +__decorate( + [nobatch("This operation is not supported within a batch operation.")], + KintoClientBase.prototype, + "fetchHTTPApiVersion", + null +); +__decorate( + [nobatch("Can't use batch within a batch!")], + KintoClientBase.prototype, + "batch", + null +); +__decorate( + [capable(["permissions_endpoint"])], + KintoClientBase.prototype, + "listPermissions", + null +); +__decorate( + [support("1.4", "2.0")], + KintoClientBase.prototype, + "deleteBuckets", + null +); +__decorate( + [capable(["accounts"])], + KintoClientBase.prototype, + "createAccount", + null +); + +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* @ts-ignore */ +class KintoHttpClient extends KintoClientBase { + constructor(remote, options = {}) { + const events = {}; + EventEmitter.decorate(events); + super(remote, { events: events, ...options }); + } +} +KintoHttpClient.errors = errors; + +export { KintoHttpClient }; diff --git a/services/common/kinto-offline-client.js b/services/common/kinto-offline-client.js new file mode 100644 index 0000000000..7b11347555 --- /dev/null +++ b/services/common/kinto-offline-client.js @@ -0,0 +1,2643 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +"use strict"; + +/* + * This file is generated from kinto.js - do not modify directly. + */ + +// This is required because with Babel compiles ES2015 modules into a +// require() form that tries to keep its modules on "this", but +// doesn't specify "this", leaving it to default to the global +// object. However, in strict mode, "this" no longer defaults to the +// global object, so expose the global object explicitly. Babel's +// compiled output will use a variable called "global" if one is +// present. +// +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for +// more details. +const global = this; + +var EXPORTED_SYMBOLS = ["Kinto"]; + +/* + * Version 13.0.0 - 7fbf95d + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Kinto = factory()); +}(this, (function () { 'use strict'; + + /** + * Base db adapter. + * + * @abstract + */ + class BaseAdapter { + /** + * Deletes every records present in the database. + * + * @abstract + * @return {Promise} + */ + clear() { + throw new Error("Not Implemented."); + } + /** + * Executes a batch of operations within a single transaction. + * + * @abstract + * @param {Function} callback The operation callback. + * @param {Object} options The options object. + * @return {Promise} + */ + execute(callback, options = { preload: [] }) { + throw new Error("Not Implemented."); + } + /** + * Retrieve a record by its primary key from the database. + * + * @abstract + * @param {String} id The record id. + * @return {Promise} + */ + get(id) { + throw new Error("Not Implemented."); + } + /** + * Lists all records from the database. + * + * @abstract + * @param {Object} params The filters and order to apply to the results. + * @return {Promise} + */ + list(params = { filters: {}, order: "" }) { + throw new Error("Not Implemented."); + } + /** + * Store the lastModified value. + * + * @abstract + * @param {Number} lastModified + * @return {Promise} + */ + saveLastModified(lastModified) { + throw new Error("Not Implemented."); + } + /** + * Retrieve saved lastModified value. + * + * @abstract + * @return {Promise} + */ + getLastModified() { + throw new Error("Not Implemented."); + } + /** + * Load records in bulk that were exported from a server. + * + * @abstract + * @param {Array} records The records to load. + * @return {Promise} + */ + importBulk(records) { + throw new Error("Not Implemented."); + } + /** + * Load a dump of records exported from a server. + * + * @deprecated Use {@link importBulk} instead. + * @abstract + * @param {Array} records The records to load. + * @return {Promise} + */ + loadDump(records) { + throw new Error("Not Implemented."); + } + saveMetadata(metadata) { + throw new Error("Not Implemented."); + } + getMetadata() { + throw new Error("Not Implemented."); + } + } + + const RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/; + /** + * Checks if a value is undefined. + * @param {Any} value + * @return {Boolean} + */ + function _isUndefined(value) { + return typeof value === "undefined"; + } + /** + * Sorts records in a list according to a given ordering. + * + * @param {String} order The ordering, eg. `-last_modified`. + * @param {Array} list The collection to order. + * @return {Array} + */ + function sortObjects(order, list) { + const hasDash = order[0] === "-"; + const field = hasDash ? order.slice(1) : order; + const direction = hasDash ? -1 : 1; + return list.slice().sort((a, b) => { + if (a[field] && _isUndefined(b[field])) { + return direction; + } + if (b[field] && _isUndefined(a[field])) { + return -direction; + } + if (_isUndefined(a[field]) && _isUndefined(b[field])) { + return 0; + } + return a[field] > b[field] ? direction : -direction; + }); + } + /** + * Test if a single object matches all given filters. + * + * @param {Object} filters The filters object. + * @param {Object} entry The object to filter. + * @return {Boolean} + */ + function filterObject(filters, entry) { + return Object.keys(filters).every(filter => { + const value = filters[filter]; + if (Array.isArray(value)) { + return value.some(candidate => candidate === entry[filter]); + } + else if (typeof value === "object") { + return filterObject(value, entry[filter]); + } + else if (!Object.prototype.hasOwnProperty.call(entry, filter)) { + console.error(`The property ${filter} does not exist`); + return false; + } + return entry[filter] === value; + }); + } + /** + * Resolves a list of functions sequentially, which can be sync or async; in + * case of async, functions must return a promise. + * + * @param {Array} fns The list of functions. + * @param {Any} init The initial value. + * @return {Promise} + */ + function waterfall(fns, init) { + if (!fns.length) { + return Promise.resolve(init); + } + return fns.reduce((promise, nextFn) => { + return promise.then(nextFn); + }, Promise.resolve(init)); + } + /** + * Simple deep object comparison function. This only supports comparison of + * serializable JavaScript objects. + * + * @param {Object} a The source object. + * @param {Object} b The compared object. + * @return {Boolean} + */ + function deepEqual(a, b) { + if (a === b) { + return true; + } + if (typeof a !== typeof b) { + return false; + } + if (!(a && typeof a == "object") || !(b && typeof b == "object")) { + return false; + } + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + for (const k in a) { + if (!deepEqual(a[k], b[k])) { + return false; + } + } + return true; + } + /** + * Return an object without the specified keys. + * + * @param {Object} obj The original object. + * @param {Array} keys The list of keys to exclude. + * @return {Object} A copy without the specified keys. + */ + function omitKeys(obj, keys = []) { + const result = Object.assign({}, obj); + for (const key of keys) { + delete result[key]; + } + return result; + } + function arrayEqual(a, b) { + if (a.length !== b.length) { + return false; + } + for (let i = a.length; i--;) { + if (a[i] !== b[i]) { + return false; + } + } + return true; + } + function makeNestedObjectFromArr(arr, val, nestedFiltersObj) { + const last = arr.length - 1; + return arr.reduce((acc, cv, i) => { + if (i === last) { + return (acc[cv] = val); + } + else if (Object.prototype.hasOwnProperty.call(acc, cv)) { + return acc[cv]; + } + else { + return (acc[cv] = {}); + } + }, nestedFiltersObj); + } + function transformSubObjectFilters(filtersObj) { + const transformedFilters = {}; + for (const key in filtersObj) { + const keysArr = key.split("."); + const val = filtersObj[key]; + makeNestedObjectFromArr(keysArr, val, transformedFilters); + } + return transformedFilters; + } + + const INDEXED_FIELDS = ["id", "_status", "last_modified"]; + /** + * Small helper that wraps the opening of an IndexedDB into a Promise. + * + * @param dbname {String} The database name. + * @param version {Integer} Schema version + * @param onupgradeneeded {Function} The callback to execute if schema is + * missing or different. + * @return {Promise} + */ + async function open(dbname, { version, onupgradeneeded }) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(dbname, version); + request.onupgradeneeded = event => { + const db = event.target.result; + db.onerror = event => reject(event.target.error); + // When an upgrade is needed, a transaction is started. + const transaction = event.target.transaction; + transaction.onabort = event => { + const error = event.target.error || + transaction.error || + new DOMException("The operation has been aborted", "AbortError"); + reject(error); + }; + // Callback for store creation etc. + return onupgradeneeded(event); + }; + request.onerror = event => { + reject(event.target.error); + }; + request.onsuccess = event => { + const db = event.target.result; + resolve(db); + }; + }); + } + /** + * Helper to run the specified callback in a single transaction on the + * specified store. + * The helper focuses on transaction wrapping into a promise. + * + * @param db {IDBDatabase} The database instance. + * @param name {String} The store name. + * @param callback {Function} The piece of code to execute in the transaction. + * @param options {Object} Options. + * @param options.mode {String} Transaction mode (default: read). + * @return {Promise} any value returned by the callback. + */ + async function execute(db, name, callback, options = {}) { + const { mode } = options; + return new Promise((resolve, reject) => { + // On Safari, calling IDBDatabase.transaction with mode == undefined raises + // a TypeError. + const transaction = mode + ? db.transaction([name], mode) + : db.transaction([name]); + const store = transaction.objectStore(name); + // Let the callback abort this transaction. + const abort = e => { + transaction.abort(); + reject(e); + }; + // Execute the specified callback **synchronously**. + let result; + try { + result = callback(store, abort); + } + catch (e) { + abort(e); + } + transaction.onerror = event => reject(event.target.error); + transaction.oncomplete = event => resolve(result); + transaction.onabort = event => { + const error = event.target.error || + transaction.error || + new DOMException("The operation has been aborted", "AbortError"); + reject(error); + }; + }); + } + /** + * Helper to wrap the deletion of an IndexedDB database into a promise. + * + * @param dbName {String} the database to delete + * @return {Promise} + */ + async function deleteDatabase(dbName) { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(dbName); + request.onsuccess = event => resolve(event.target); + request.onerror = event => reject(event.target.error); + }); + } + /** + * IDB cursor handlers. + * @type {Object} + */ + const cursorHandlers = { + all(filters, done) { + const results = []; + return event => { + const cursor = event.target.result; + if (cursor) { + const { value } = cursor; + if (filterObject(filters, value)) { + results.push(value); + } + cursor.continue(); + } + else { + done(results); + } + }; + }, + in(values, filters, done) { + const results = []; + let i = 0; + return function (event) { + const cursor = event.target.result; + if (!cursor) { + done(results); + return; + } + const { key, value } = cursor; + // `key` can be an array of two values (see `keyPath` in indices definitions). + // `values` can be an array of arrays if we filter using an index whose key path + // is an array (eg. `cursorHandlers.in([["bid/cid", 42], ["bid/cid", 43]], ...)`) + while (key > values[i]) { + // The cursor has passed beyond this key. Check next. + ++i; + if (i === values.length) { + done(results); // There is no next. Stop searching. + return; + } + } + const isEqual = Array.isArray(key) + ? arrayEqual(key, values[i]) + : key === values[i]; + if (isEqual) { + if (filterObject(filters, value)) { + results.push(value); + } + cursor.continue(); + } + else { + cursor.continue(values[i]); + } + }; + }, + }; + /** + * Creates an IDB request and attach it the appropriate cursor event handler to + * perform a list query. + * + * Multiple matching values are handled by passing an array. + * + * @param {String} cid The collection id (ie. `{bid}/{cid}`) + * @param {IDBStore} store The IDB store. + * @param {Object} filters Filter the records by field. + * @param {Function} done The operation completion handler. + * @return {IDBRequest} + */ + function createListRequest(cid, store, filters, done) { + const filterFields = Object.keys(filters); + // If no filters, get all results in one bulk. + if (filterFields.length == 0) { + const request = store.index("cid").getAll(IDBKeyRange.only(cid)); + request.onsuccess = event => done(event.target.result); + return request; + } + // Introspect filters and check if they leverage an indexed field. + const indexField = filterFields.find(field => { + return INDEXED_FIELDS.includes(field); + }); + if (!indexField) { + // Iterate on all records for this collection (ie. cid) + const isSubQuery = Object.keys(filters).some(key => key.includes(".")); // (ie. filters: {"article.title": "hello"}) + if (isSubQuery) { + const newFilter = transformSubObjectFilters(filters); + const request = store.index("cid").openCursor(IDBKeyRange.only(cid)); + request.onsuccess = cursorHandlers.all(newFilter, done); + return request; + } + const request = store.index("cid").openCursor(IDBKeyRange.only(cid)); + request.onsuccess = cursorHandlers.all(filters, done); + return request; + } + // If `indexField` was used already, don't filter again. + const remainingFilters = omitKeys(filters, [indexField]); + // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`) + const value = filters[indexField]; + // For the "id" field, use the primary key. + const indexStore = indexField == "id" ? store : store.index(indexField); + // WHERE IN equivalent clause + if (Array.isArray(value)) { + if (value.length === 0) { + return done([]); + } + const values = value.map(i => [cid, i]).sort(); + const range = IDBKeyRange.bound(values[0], values[values.length - 1]); + const request = indexStore.openCursor(range); + request.onsuccess = cursorHandlers.in(values, remainingFilters, done); + return request; + } + // If no filters on custom attribute, get all results in one bulk. + if (remainingFilters.length == 0) { + const request = indexStore.getAll(IDBKeyRange.only([cid, value])); + request.onsuccess = event => done(event.target.result); + return request; + } + // WHERE field = value clause + const request = indexStore.openCursor(IDBKeyRange.only([cid, value])); + request.onsuccess = cursorHandlers.all(remainingFilters, done); + return request; + } + class IDBError extends Error { + constructor(method, err) { + super(`IndexedDB ${method}() ${err.message}`); + this.name = err.name; + this.stack = err.stack; + } + } + /** + * IndexedDB adapter. + * + * This adapter doesn't support any options. + */ + class IDB extends BaseAdapter { + /* Expose the IDBError class publicly */ + static get IDBError() { + return IDBError; + } + /** + * Constructor. + * + * @param {String} cid The key base for this collection (eg. `bid/cid`) + * @param {Object} options + * @param {String} options.dbName The IndexedDB name (default: `"KintoDB"`) + * @param {String} options.migrateOldData Whether old database data should be migrated (default: `false`) + */ + constructor(cid, options = {}) { + super(); + this.cid = cid; + this.dbName = options.dbName || "KintoDB"; + this._options = options; + this._db = null; + } + _handleError(method, err) { + throw new IDBError(method, err); + } + /** + * Ensures a connection to the IndexedDB database has been opened. + * + * @override + * @return {Promise} + */ + async open() { + if (this._db) { + return this; + } + // In previous versions, we used to have a database with name `${bid}/${cid}`. + // Check if it exists, and migrate data once new schema is in place. + // Note: the built-in migrations from IndexedDB can only be used if the + // database name does not change. + const dataToMigrate = this._options.migrateOldData + ? await migrationRequired(this.cid) + : null; + this._db = await open(this.dbName, { + version: 2, + onupgradeneeded: event => { + const db = event.target.result; + if (event.oldVersion < 1) { + // Records store + const recordsStore = db.createObjectStore("records", { + keyPath: ["_cid", "id"], + }); + // An index to obtain all the records in a collection. + recordsStore.createIndex("cid", "_cid"); + // Here we create indices for every known field in records by collection. + // Local record status ("synced", "created", "updated", "deleted") + recordsStore.createIndex("_status", ["_cid", "_status"]); + // Last modified field + recordsStore.createIndex("last_modified", ["_cid", "last_modified"]); + // Timestamps store + db.createObjectStore("timestamps", { + keyPath: "cid", + }); + } + if (event.oldVersion < 2) { + // Collections store + db.createObjectStore("collections", { + keyPath: "cid", + }); + } + }, + }); + if (dataToMigrate) { + const { records, timestamp } = dataToMigrate; + await this.importBulk(records); + await this.saveLastModified(timestamp); + console.log(`${this.cid}: data was migrated successfully.`); + // Delete the old database. + await deleteDatabase(this.cid); + console.warn(`${this.cid}: old database was deleted.`); + } + return this; + } + /** + * Closes current connection to the database. + * + * @override + * @return {Promise} + */ + close() { + if (this._db) { + this._db.close(); // indexedDB.close is synchronous + this._db = null; + } + return Promise.resolve(); + } + /** + * Returns a transaction and an object store for a store name. + * + * To determine if a transaction has completed successfully, we should rather + * listen to the transaction’s complete event rather than the IDBObjectStore + * request’s success event, because the transaction may still fail after the + * success event fires. + * + * @param {String} name Store name + * @param {Function} callback to execute + * @param {Object} options Options + * @param {String} options.mode Transaction mode ("readwrite" or undefined) + * @return {Object} + */ + async prepare(name, callback, options) { + await this.open(); + await execute(this._db, name, callback, options); + } + /** + * Deletes every records in the current collection. + * + * @override + * @return {Promise} + */ + async clear() { + try { + await this.prepare("records", store => { + const range = IDBKeyRange.only(this.cid); + const request = store.index("cid").openKeyCursor(range); + request.onsuccess = event => { + const cursor = event.target.result; + if (cursor) { + store.delete(cursor.primaryKey); + cursor.continue(); + } + }; + return request; + }, { mode: "readwrite" }); + } + catch (e) { + this._handleError("clear", e); + } + } + /** + * Executes the set of synchronous CRUD operations described in the provided + * callback within an IndexedDB transaction, for current db store. + * + * The callback will be provided an object exposing the following synchronous + * CRUD operation methods: get, create, update, delete. + * + * Important note: because limitations in IndexedDB implementations, no + * asynchronous code should be performed within the provided callback; the + * promise will therefore be rejected if the callback returns a Promise. + * + * Options: + * - {Array} preload: The list of record IDs to fetch and make available to + * the transaction object get() method (default: []) + * + * @example + * const db = new IDB("example"); + * const result = await db.execute(transaction => { + * transaction.create({id: 1, title: "foo"}); + * transaction.update({id: 2, title: "bar"}); + * transaction.delete(3); + * return "foo"; + * }); + * + * @override + * @param {Function} callback The operation description callback. + * @param {Object} options The options object. + * @return {Promise} + */ + async execute(callback, options = { preload: [] }) { + // Transactions in IndexedDB are autocommited when a callback does not + // perform any additional operation. + // The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394) + // prevents using within an opened transaction. + // To avoid managing asynchronocity in the specified `callback`, we preload + // a list of record in order to execute the `callback` synchronously. + // See also: + // - http://stackoverflow.com/a/28388805/330911 + // - http://stackoverflow.com/a/10405196 + // - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ + let result; + await this.prepare("records", (store, abort) => { + const runCallback = (preloaded = []) => { + // Expose a consistent API for every adapter instead of raw store methods. + const proxy = transactionProxy(this, store, preloaded); + // The callback is executed synchronously within the same transaction. + try { + const returned = callback(proxy); + if (returned instanceof Promise) { + // XXX: investigate how to provide documentation details in error. + throw new Error("execute() callback should not return a Promise."); + } + // Bring to scope that will be returned (once promise awaited). + result = returned; + } + catch (e) { + // The callback has thrown an error explicitly. Abort transaction cleanly. + abort(e); + } + }; + // No option to preload records, go straight to `callback`. + if (!options.preload.length) { + return runCallback(); + } + // Preload specified records using a list request. + const filters = { id: options.preload }; + createListRequest(this.cid, store, filters, records => { + // Store obtained records by id. + const preloaded = {}; + for (const record of records) { + delete record["_cid"]; + preloaded[record.id] = record; + } + runCallback(preloaded); + }); + }, { mode: "readwrite" }); + return result; + } + /** + * Retrieve a record by its primary key from the IndexedDB database. + * + * @override + * @param {String} id The record id. + * @return {Promise} + */ + async get(id) { + try { + let record; + await this.prepare("records", store => { + store.get([this.cid, id]).onsuccess = e => (record = e.target.result); + }); + return record; + } + catch (e) { + this._handleError("get", e); + } + } + /** + * Lists all records from the IndexedDB database. + * + * @override + * @param {Object} params The filters and order to apply to the results. + * @return {Promise} + */ + async list(params = { filters: {} }) { + const { filters } = params; + try { + let results = []; + await this.prepare("records", store => { + createListRequest(this.cid, store, filters, _results => { + // we have received all requested records that match the filters, + // we now park them within current scope and hide the `_cid` attribute. + for (const result of _results) { + delete result["_cid"]; + } + results = _results; + }); + }); + // The resulting list of records is sorted. + // XXX: with some efforts, this could be fully implemented using IDB API. + return params.order ? sortObjects(params.order, results) : results; + } + catch (e) { + this._handleError("list", e); + } + } + /** + * Store the lastModified value into metadata store. + * + * @override + * @param {Number} lastModified + * @return {Promise} + */ + async saveLastModified(lastModified) { + const value = parseInt(lastModified, 10) || null; + try { + await this.prepare("timestamps", store => { + if (value === null) { + store.delete(this.cid); + } + else { + store.put({ cid: this.cid, value }); + } + }, { mode: "readwrite" }); + return value; + } + catch (e) { + this._handleError("saveLastModified", e); + } + } + /** + * Retrieve saved lastModified value. + * + * @override + * @return {Promise} + */ + async getLastModified() { + try { + let entry = null; + await this.prepare("timestamps", store => { + store.get(this.cid).onsuccess = e => (entry = e.target.result); + }); + return entry ? entry.value : null; + } + catch (e) { + this._handleError("getLastModified", e); + } + } + /** + * Load a dump of records exported from a server. + * + * @deprecated Use {@link importBulk} instead. + * @abstract + * @param {Array} records The records to load. + * @return {Promise} + */ + async loadDump(records) { + return this.importBulk(records); + } + /** + * Load records in bulk that were exported from a server. + * + * @abstract + * @param {Array} records The records to load. + * @return {Promise} + */ + async importBulk(records) { + try { + await this.execute(transaction => { + // Since the put operations are asynchronous, we chain + // them together. The last one will be waited for the + // `transaction.oncomplete` callback. (see #execute()) + let i = 0; + putNext(); + function putNext() { + if (i == records.length) { + return; + } + // On error, `transaction.onerror` is called. + transaction.update(records[i]).onsuccess = putNext; + ++i; + } + }); + const previousLastModified = await this.getLastModified(); + const lastModified = Math.max(...records.map(record => record.last_modified)); + if (lastModified > previousLastModified) { + await this.saveLastModified(lastModified); + } + return records; + } + catch (e) { + this._handleError("importBulk", e); + } + } + async saveMetadata(metadata) { + try { + await this.prepare("collections", store => store.put({ cid: this.cid, metadata }), { mode: "readwrite" }); + return metadata; + } + catch (e) { + this._handleError("saveMetadata", e); + } + } + async getMetadata() { + try { + let entry = null; + await this.prepare("collections", store => { + store.get(this.cid).onsuccess = e => (entry = e.target.result); + }); + return entry ? entry.metadata : null; + } + catch (e) { + this._handleError("getMetadata", e); + } + } + } + /** + * IDB transaction proxy. + * + * @param {IDB} adapter The call IDB adapter + * @param {IDBStore} store The IndexedDB database store. + * @param {Array} preloaded The list of records to make available to + * get() (default: []). + * @return {Object} + */ + function transactionProxy(adapter, store, preloaded = []) { + const _cid = adapter.cid; + return { + create(record) { + store.add(Object.assign(Object.assign({}, record), { _cid })); + }, + update(record) { + return store.put(Object.assign(Object.assign({}, record), { _cid })); + }, + delete(id) { + store.delete([_cid, id]); + }, + get(id) { + return preloaded[id]; + }, + }; + } + /** + * Up to version 10.X of kinto.js, each collection had its own collection. + * The database name was `${bid}/${cid}` (eg. `"blocklists/certificates"`) + * and contained only one store with the same name. + */ + async function migrationRequired(dbName) { + let exists = true; + const db = await open(dbName, { + version: 1, + onupgradeneeded: event => { + exists = false; + }, + }); + // Check that the DB we're looking at is really a legacy one, + // and not some remainder of the open() operation above. + exists &= + db.objectStoreNames.contains("__meta__") && + db.objectStoreNames.contains(dbName); + if (!exists) { + db.close(); + // Testing the existence creates it, so delete it :) + await deleteDatabase(dbName); + return null; + } + console.warn(`${dbName}: old IndexedDB database found.`); + try { + // Scan all records. + let records; + await execute(db, dbName, store => { + store.openCursor().onsuccess = cursorHandlers.all({}, res => (records = res)); + }); + console.log(`${dbName}: found ${records.length} records.`); + // Check if there's a entry for this. + let timestamp = null; + await execute(db, "__meta__", store => { + store.get(`${dbName}-lastModified`).onsuccess = e => { + timestamp = e.target.result ? e.target.result.value : null; + }; + }); + // Some previous versions, also used to store the timestamps without prefix. + if (!timestamp) { + await execute(db, "__meta__", store => { + store.get("lastModified").onsuccess = e => { + timestamp = e.target.result ? e.target.result.value : null; + }; + }); + } + console.log(`${dbName}: ${timestamp ? "found" : "no"} timestamp.`); + // Those will be inserted in the new database/schema. + return { records, timestamp }; + } + catch (e) { + console.error("Error occured during migration", e); + return null; + } + finally { + db.close(); + } + } + + var uuid4 = {}; + + const RECORD_FIELDS_TO_CLEAN = ["_status"]; + const AVAILABLE_HOOKS = ["incoming-changes"]; + const IMPORT_CHUNK_SIZE = 200; + /** + * Compare two records omitting local fields and synchronization + * attributes (like _status and last_modified) + * @param {Object} a A record to compare. + * @param {Object} b A record to compare. + * @param {Array} localFields Additional fields to ignore during the comparison + * @return {boolean} + */ + function recordsEqual(a, b, localFields = []) { + const fieldsToClean = RECORD_FIELDS_TO_CLEAN.concat(["last_modified"]).concat(localFields); + const cleanLocal = r => omitKeys(r, fieldsToClean); + return deepEqual(cleanLocal(a), cleanLocal(b)); + } + /** + * Synchronization result object. + */ + class SyncResultObject { + /** + * Public constructor. + */ + constructor() { + /** + * Current synchronization result status; becomes `false` when conflicts or + * errors are registered. + * @type {Boolean} + */ + this.lastModified = null; + this._lists = {}; + [ + "errors", + "created", + "updated", + "deleted", + "published", + "conflicts", + "skipped", + "resolved", + "void", + ].forEach(l => (this._lists[l] = [])); + this._cached = {}; + } + /** + * Adds entries for a given result type. + * + * @param {String} type The result type. + * @param {Array} entries The result entries. + * @return {SyncResultObject} + */ + add(type, entries) { + if (!Array.isArray(this._lists[type])) { + console.warn(`Unknown type "${type}"`); + return; + } + if (!Array.isArray(entries)) { + entries = [entries]; + } + this._lists[type] = this._lists[type].concat(entries); + delete this._cached[type]; + return this; + } + get ok() { + return this.errors.length + this.conflicts.length === 0; + } + get errors() { + return this._lists["errors"]; + } + get conflicts() { + return this._lists["conflicts"]; + } + get skipped() { + return this._deduplicate("skipped"); + } + get resolved() { + return this._deduplicate("resolved"); + } + get created() { + return this._deduplicate("created"); + } + get updated() { + return this._deduplicate("updated"); + } + get deleted() { + return this._deduplicate("deleted"); + } + get published() { + return this._deduplicate("published"); + } + _deduplicate(list) { + if (!(list in this._cached)) { + // Deduplicate entries by id. If the values don't have `id` attribute, just + // keep all. + const recordsWithoutId = new Set(); + const recordsById = new Map(); + this._lists[list].forEach(record => { + if (!record.id) { + recordsWithoutId.add(record); + } + else { + recordsById.set(record.id, record); + } + }); + this._cached[list] = Array.from(recordsById.values()).concat(Array.from(recordsWithoutId)); + } + return this._cached[list]; + } + /** + * Reinitializes result entries for a given result type. + * + * @param {String} type The result type. + * @return {SyncResultObject} + */ + reset(type) { + this._lists[type] = []; + delete this._cached[type]; + return this; + } + toObject() { + // Only used in tests. + return { + ok: this.ok, + lastModified: this.lastModified, + errors: this.errors, + created: this.created, + updated: this.updated, + deleted: this.deleted, + skipped: this.skipped, + published: this.published, + conflicts: this.conflicts, + resolved: this.resolved, + }; + } + } + class ServerWasFlushedError extends Error { + constructor(clientTimestamp, serverTimestamp, message) { + super(message); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ServerWasFlushedError); + } + this.clientTimestamp = clientTimestamp; + this.serverTimestamp = serverTimestamp; + } + } + function createUUIDSchema() { + return { + generate() { + return uuid4(); + }, + validate(id) { + return typeof id == "string" && RE_RECORD_ID.test(id); + }, + }; + } + function markStatus(record, status) { + return Object.assign(Object.assign({}, record), { _status: status }); + } + function markDeleted(record) { + return markStatus(record, "deleted"); + } + function markSynced(record) { + return markStatus(record, "synced"); + } + /** + * Import a remote change into the local database. + * + * @param {IDBTransactionProxy} transaction The transaction handler. + * @param {Object} remote The remote change object to import. + * @param {Array} localFields The list of fields that remain local. + * @param {String} strategy The {@link Collection.strategy}. + * @return {Object} + */ + function importChange(transaction, remote, localFields, strategy) { + const local = transaction.get(remote.id); + if (!local) { + // Not found locally but remote change is marked as deleted; skip to + // avoid recreation. + if (remote.deleted) { + return { type: "skipped", data: remote }; + } + const synced = markSynced(remote); + transaction.create(synced); + return { type: "created", data: synced }; + } + // Apply remote changes on local record. + const synced = Object.assign(Object.assign({}, local), markSynced(remote)); + // With pull only, we don't need to compare records since we override them. + if (strategy === Collection.strategy.PULL_ONLY) { + if (remote.deleted) { + transaction.delete(remote.id); + return { type: "deleted", data: local }; + } + transaction.update(synced); + return { type: "updated", data: { old: local, new: synced } }; + } + // With other sync strategies, we detect conflicts, + // by comparing local and remote, ignoring local fields. + const isIdentical = recordsEqual(local, remote, localFields); + // Detect or ignore conflicts if record has also been modified locally. + if (local._status !== "synced") { + // Locally deleted, unsynced: scheduled for remote deletion. + if (local._status === "deleted") { + return { type: "skipped", data: local }; + } + if (isIdentical) { + // If records are identical, import anyway, so we bump the + // local last_modified value from the server and set record + // status to "synced". + transaction.update(synced); + return { type: "updated", data: { old: local, new: synced } }; + } + if (local.last_modified !== undefined && + local.last_modified === remote.last_modified) { + // If our local version has the same last_modified as the remote + // one, this represents an object that corresponds to a resolved + // conflict. Our local version represents the final output, so + // we keep that one. (No transaction operation to do.) + // But if our last_modified is undefined, + // that means we've created the same object locally as one on + // the server, which *must* be a conflict. + return { type: "void" }; + } + return { + type: "conflicts", + data: { type: "incoming", local: local, remote: remote }, + }; + } + // Local record was synced. + if (remote.deleted) { + transaction.delete(remote.id); + return { type: "deleted", data: local }; + } + // Import locally. + transaction.update(synced); + // if identical, simply exclude it from all SyncResultObject lists + const type = isIdentical ? "void" : "updated"; + return { type, data: { old: local, new: synced } }; + } + /** + * Abstracts a collection of records stored in the local database, providing + * CRUD operations and synchronization helpers. + */ + class Collection { + /** + * Constructor. + * + * Options: + * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`) + * + * @param {String} bucket The bucket identifier. + * @param {String} name The collection name. + * @param {KintoBase} kinto The Kinto instance. + * @param {Object} options The options object. + */ + constructor(bucket, name, kinto, options = {}) { + this._bucket = bucket; + this._name = name; + this._lastModified = null; + const DBAdapter = options.adapter || IDB; + if (!DBAdapter) { + throw new Error("No adapter provided"); + } + const db = new DBAdapter(`${bucket}/${name}`, options.adapterOptions); + if (!(db instanceof BaseAdapter)) { + throw new Error("Unsupported adapter."); + } + // public properties + /** + * The db adapter instance + * @type {BaseAdapter} + */ + this.db = db; + /** + * The KintoBase instance. + * @type {KintoBase} + */ + this.kinto = kinto; + /** + * The event emitter instance. + * @type {EventEmitter} + */ + this.events = options.events; + /** + * The IdSchema instance. + * @type {Object} + */ + this.idSchema = this._validateIdSchema(options.idSchema); + /** + * The list of remote transformers. + * @type {Array} + */ + this.remoteTransformers = this._validateRemoteTransformers(options.remoteTransformers); + /** + * The list of hooks. + * @type {Object} + */ + this.hooks = this._validateHooks(options.hooks); + /** + * The list of fields names that will remain local. + * @type {Array} + */ + this.localFields = options.localFields || []; + } + /** + * The HTTP client. + * @type {KintoClient} + */ + get api() { + return this.kinto.api; + } + /** + * The collection name. + * @type {String} + */ + get name() { + return this._name; + } + /** + * The bucket name. + * @type {String} + */ + get bucket() { + return this._bucket; + } + /** + * The last modified timestamp. + * @type {Number} + */ + get lastModified() { + return this._lastModified; + } + /** + * Synchronization strategies. Available strategies are: + * + * - `MANUAL`: Conflicts will be reported in a dedicated array. + * - `SERVER_WINS`: Conflicts are resolved using remote data. + * - `CLIENT_WINS`: Conflicts are resolved using local data. + * + * @type {Object} + */ + static get strategy() { + return { + CLIENT_WINS: "client_wins", + SERVER_WINS: "server_wins", + PULL_ONLY: "pull_only", + MANUAL: "manual", + }; + } + /** + * Validates an idSchema. + * + * @param {Object|undefined} idSchema + * @return {Object} + */ + _validateIdSchema(idSchema) { + if (typeof idSchema === "undefined") { + return createUUIDSchema(); + } + if (typeof idSchema !== "object") { + throw new Error("idSchema must be an object."); + } + else if (typeof idSchema.generate !== "function") { + throw new Error("idSchema must provide a generate function."); + } + else if (typeof idSchema.validate !== "function") { + throw new Error("idSchema must provide a validate function."); + } + return idSchema; + } + /** + * Validates a list of remote transformers. + * + * @param {Array|undefined} remoteTransformers + * @return {Array} + */ + _validateRemoteTransformers(remoteTransformers) { + if (typeof remoteTransformers === "undefined") { + return []; + } + if (!Array.isArray(remoteTransformers)) { + throw new Error("remoteTransformers should be an array."); + } + return remoteTransformers.map(transformer => { + if (typeof transformer !== "object") { + throw new Error("A transformer must be an object."); + } + else if (typeof transformer.encode !== "function") { + throw new Error("A transformer must provide an encode function."); + } + else if (typeof transformer.decode !== "function") { + throw new Error("A transformer must provide a decode function."); + } + return transformer; + }); + } + /** + * Validate the passed hook is correct. + * + * @param {Array|undefined} hook. + * @return {Array} + **/ + _validateHook(hook) { + if (!Array.isArray(hook)) { + throw new Error("A hook definition should be an array of functions."); + } + return hook.map(fn => { + if (typeof fn !== "function") { + throw new Error("A hook definition should be an array of functions."); + } + return fn; + }); + } + /** + * Validates a list of hooks. + * + * @param {Object|undefined} hooks + * @return {Object} + */ + _validateHooks(hooks) { + if (typeof hooks === "undefined") { + return {}; + } + if (Array.isArray(hooks)) { + throw new Error("hooks should be an object, not an array."); + } + if (typeof hooks !== "object") { + throw new Error("hooks should be an object."); + } + const validatedHooks = {}; + for (const hook in hooks) { + if (!AVAILABLE_HOOKS.includes(hook)) { + throw new Error("The hook should be one of " + AVAILABLE_HOOKS.join(", ")); + } + validatedHooks[hook] = this._validateHook(hooks[hook]); + } + return validatedHooks; + } + /** + * Deletes every records in the current collection and marks the collection as + * never synced. + * + * @return {Promise} + */ + async clear() { + await this.db.clear(); + await this.db.saveMetadata(null); + await this.db.saveLastModified(null); + return { data: [], permissions: {} }; + } + /** + * Encodes a record. + * + * @param {String} type Either "remote" or "local". + * @param {Object} record The record object to encode. + * @return {Promise} + */ + _encodeRecord(type, record) { + if (!this[`${type}Transformers`].length) { + return Promise.resolve(record); + } + return waterfall(this[`${type}Transformers`].map(transformer => { + return record => transformer.encode(record); + }), record); + } + /** + * Decodes a record. + * + * @param {String} type Either "remote" or "local". + * @param {Object} record The record object to decode. + * @return {Promise} + */ + _decodeRecord(type, record) { + if (!this[`${type}Transformers`].length) { + return Promise.resolve(record); + } + return waterfall(this[`${type}Transformers`].reverse().map(transformer => { + return record => transformer.decode(record); + }), record); + } + /** + * Adds a record to the local database, asserting that none + * already exist with this ID. + * + * Note: If either the `useRecordId` or `synced` options are true, then the + * record object must contain the id field to be validated. If none of these + * options are true, an id is generated using the current IdSchema; in this + * case, the record passed must not have an id. + * + * Options: + * - {Boolean} synced Sets record status to "synced" (default: `false`). + * - {Boolean} useRecordId Forces the `id` field from the record to be used, + * instead of one that is generated automatically + * (default: `false`). + * + * @param {Object} record + * @param {Object} options + * @return {Promise} + */ + create(record, options = { useRecordId: false, synced: false }) { + // Validate the record and its ID (if any), even though this + // validation is also done in the CollectionTransaction method, + // because we need to pass the ID to preloadIds. + const reject = msg => Promise.reject(new Error(msg)); + if (typeof record !== "object") { + return reject("Record is not an object."); + } + if ((options.synced || options.useRecordId) && + !Object.prototype.hasOwnProperty.call(record, "id")) { + return reject("Missing required Id; synced and useRecordId options require one"); + } + if (!options.synced && + !options.useRecordId && + Object.prototype.hasOwnProperty.call(record, "id")) { + return reject("Extraneous Id; can't create a record having one set."); + } + const newRecord = Object.assign(Object.assign({}, record), { id: options.synced || options.useRecordId + ? record.id + : this.idSchema.generate(record), _status: options.synced ? "synced" : "created" }); + if (!this.idSchema.validate(newRecord.id)) { + return reject(`Invalid Id: ${newRecord.id}`); + } + return this.execute(txn => txn.create(newRecord), { + preloadIds: [newRecord.id], + }).catch(err => { + if (options.useRecordId) { + throw new Error("Couldn't create record. It may have been virtually deleted."); + } + throw err; + }); + } + /** + * Like {@link CollectionTransaction#update}, but wrapped in its own transaction. + * + * Options: + * - {Boolean} synced: Sets record status to "synced" (default: false) + * - {Boolean} patch: Extends the existing record instead of overwriting it + * (default: false) + * + * @param {Object} record + * @param {Object} options + * @return {Promise} + */ + update(record, options = { synced: false, patch: false }) { + // Validate the record and its ID, even though this validation is + // also done in the CollectionTransaction method, because we need + // to pass the ID to preloadIds. + if (typeof record !== "object") { + return Promise.reject(new Error("Record is not an object.")); + } + if (!Object.prototype.hasOwnProperty.call(record, "id")) { + return Promise.reject(new Error("Cannot update a record missing id.")); + } + if (!this.idSchema.validate(record.id)) { + return Promise.reject(new Error(`Invalid Id: ${record.id}`)); + } + return this.execute(txn => txn.update(record, options), { + preloadIds: [record.id], + }); + } + /** + * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction. + * + * @param {Object} record + * @return {Promise} + */ + upsert(record) { + // Validate the record and its ID, even though this validation is + // also done in the CollectionTransaction method, because we need + // to pass the ID to preloadIds. + if (typeof record !== "object") { + return Promise.reject(new Error("Record is not an object.")); + } + if (!Object.prototype.hasOwnProperty.call(record, "id")) { + return Promise.reject(new Error("Cannot update a record missing id.")); + } + if (!this.idSchema.validate(record.id)) { + return Promise.reject(new Error(`Invalid Id: ${record.id}`)); + } + return this.execute(txn => txn.upsert(record), { preloadIds: [record.id] }); + } + /** + * Like {@link CollectionTransaction#get}, but wrapped in its own transaction. + * + * Options: + * - {Boolean} includeDeleted: Include virtually deleted records. + * + * @param {String} id + * @param {Object} options + * @return {Promise} + */ + get(id, options = { includeDeleted: false }) { + return this.execute(txn => txn.get(id, options), { preloadIds: [id] }); + } + /** + * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction. + * + * @param {String} id + * @return {Promise} + */ + getAny(id) { + return this.execute(txn => txn.getAny(id), { preloadIds: [id] }); + } + /** + * Same as {@link Collection#delete}, but wrapped in its own transaction. + * + * Options: + * - {Boolean} virtual: When set to `true`, doesn't actually delete the record, + * update its `_status` attribute to `deleted` instead (default: true) + * + * @param {String} id The record's Id. + * @param {Object} options The options object. + * @return {Promise} + */ + delete(id, options = { virtual: true }) { + return this.execute(transaction => { + return transaction.delete(id, options); + }, { preloadIds: [id] }); + } + /** + * Same as {@link Collection#deleteAll}, but wrapped in its own transaction, execulding the parameter. + * + * @return {Promise} + */ + async deleteAll() { + const { data } = await this.list({}, { includeDeleted: false }); + const recordIds = data.map(record => record.id); + return this.execute(transaction => { + return transaction.deleteAll(recordIds); + }, { preloadIds: recordIds }); + } + /** + * The same as {@link CollectionTransaction#deleteAny}, but wrapped + * in its own transaction. + * + * @param {String} id The record's Id. + * @return {Promise} + */ + deleteAny(id) { + return this.execute(txn => txn.deleteAny(id), { preloadIds: [id] }); + } + /** + * Lists records from the local database. + * + * Params: + * - {Object} filters Filter the results (default: `{}`). + * - {String} order The order to apply (default: `-last_modified`). + * + * Options: + * - {Boolean} includeDeleted: Include virtually deleted records. + * + * @param {Object} params The filters and order to apply to the results. + * @param {Object} options The options object. + * @return {Promise} + */ + async list(params = {}, options = { includeDeleted: false }) { + params = Object.assign({ order: "-last_modified", filters: {} }, params); + const results = await this.db.list(params); + let data = results; + if (!options.includeDeleted) { + data = results.filter(record => record._status !== "deleted"); + } + return { data, permissions: {} }; + } + /** + * Imports remote changes into the local database. + * This method is in charge of detecting the conflicts, and resolve them + * according to the specified strategy. + * @param {SyncResultObject} syncResultObject The sync result object. + * @param {Array} decodedChanges The list of changes to import in the local database. + * @param {String} strategy The {@link Collection.strategy} (default: MANUAL) + * @return {Promise} + */ + async importChanges(syncResultObject, decodedChanges, strategy = Collection.strategy.MANUAL) { + // Retrieve records matching change ids. + try { + for (let i = 0; i < decodedChanges.length; i += IMPORT_CHUNK_SIZE) { + const slice = decodedChanges.slice(i, i + IMPORT_CHUNK_SIZE); + const { imports, resolved } = await this.db.execute(transaction => { + const imports = slice.map(remote => { + // Store remote change into local database. + return importChange(transaction, remote, this.localFields, strategy); + }); + const conflicts = imports + .filter(i => i.type === "conflicts") + .map(i => i.data); + const resolved = this._handleConflicts(transaction, conflicts, strategy); + return { imports, resolved }; + }, { preload: slice.map(record => record.id) }); + // Lists of created/updated/deleted records + imports.forEach(({ type, data }) => syncResultObject.add(type, data)); + // Automatically resolved conflicts (if not manual) + if (resolved.length > 0) { + syncResultObject.reset("conflicts").add("resolved", resolved); + } + } + } + catch (err) { + const data = { + type: "incoming", + message: err.message, + stack: err.stack, + }; + // XXX one error of the whole transaction instead of per atomic op + syncResultObject.add("errors", data); + } + return syncResultObject; + } + /** + * Imports the responses of pushed changes into the local database. + * Basically it stores the timestamp assigned by the server into the local + * database. + * @param {SyncResultObject} syncResultObject The sync result object. + * @param {Array} toApplyLocally The list of changes to import in the local database. + * @param {Array} conflicts The list of conflicts that have to be resolved. + * @param {String} strategy The {@link Collection.strategy}. + * @return {Promise} + */ + async _applyPushedResults(syncResultObject, toApplyLocally, conflicts, strategy = Collection.strategy.MANUAL) { + const toDeleteLocally = toApplyLocally.filter(r => r.deleted); + const toUpdateLocally = toApplyLocally.filter(r => !r.deleted); + const { published, resolved } = await this.db.execute(transaction => { + const updated = toUpdateLocally.map(record => { + const synced = markSynced(record); + transaction.update(synced); + return synced; + }); + const deleted = toDeleteLocally.map(record => { + transaction.delete(record.id); + // Amend result data with the deleted attribute set + return { id: record.id, deleted: true }; + }); + const published = updated.concat(deleted); + // Handle conflicts, if any + const resolved = this._handleConflicts(transaction, conflicts, strategy); + return { published, resolved }; + }); + syncResultObject.add("published", published); + if (resolved.length > 0) { + syncResultObject + .reset("conflicts") + .reset("resolved") + .add("resolved", resolved); + } + return syncResultObject; + } + /** + * Handles synchronization conflicts according to specified strategy. + * + * @param {SyncResultObject} result The sync result object. + * @param {String} strategy The {@link Collection.strategy}. + * @return {Promise>} The resolved conflicts, as an + * array of {accepted, rejected} objects + */ + _handleConflicts(transaction, conflicts, strategy) { + if (strategy === Collection.strategy.MANUAL) { + return []; + } + return conflicts.map(conflict => { + const resolution = strategy === Collection.strategy.CLIENT_WINS + ? conflict.local + : conflict.remote; + const rejected = strategy === Collection.strategy.CLIENT_WINS + ? conflict.remote + : conflict.local; + let accepted, status, id; + if (resolution === null) { + // We "resolved" with the server-side deletion. Delete locally. + // This only happens during SERVER_WINS because the local + // version of a record can never be null. + // We can get "null" from the remote side if we got a conflict + // and there is no remote version available; see kinto-http.js + // batch.js:aggregate. + transaction.delete(conflict.local.id); + accepted = null; + // The record was deleted, but that status is "synced" with + // the server, so we don't need to push the change. + status = "synced"; + id = conflict.local.id; + } + else { + const updated = this._resolveRaw(conflict, resolution); + transaction.update(updated); + accepted = updated; + status = updated._status; + id = updated.id; + } + return { rejected, accepted, id, _status: status }; + }); + } + /** + * Execute a bunch of operations in a transaction. + * + * This transaction should be atomic -- either all of its operations + * will succeed, or none will. + * + * The argument to this function is itself a function which will be + * called with a {@link CollectionTransaction}. Collection methods + * are available on this transaction, but instead of returning + * promises, they are synchronous. execute() returns a Promise whose + * value will be the return value of the provided function. + * + * Most operations will require access to the record itself, which + * must be preloaded by passing its ID in the preloadIds option. + * + * Options: + * - {Array} preloadIds: list of IDs to fetch at the beginning of + * the transaction + * + * @return {Promise} Resolves with the result of the given function + * when the transaction commits. + */ + execute(doOperations, { preloadIds = [] } = {}) { + for (const id of preloadIds) { + if (!this.idSchema.validate(id)) { + return Promise.reject(Error(`Invalid Id: ${id}`)); + } + } + return this.db.execute(transaction => { + const txn = new CollectionTransaction(this, transaction); + const result = doOperations(txn); + txn.emitEvents(); + return result; + }, { preload: preloadIds }); + } + /** + * Resets the local records as if they were never synced; existing records are + * marked as newly created, deleted records are dropped. + * + * A next call to {@link Collection.sync} will thus republish the whole + * content of the local collection to the server. + * + * @return {Promise} Resolves with the number of processed records. + */ + async resetSyncStatus() { + const unsynced = await this.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true }); + await this.db.execute(transaction => { + unsynced.data.forEach(record => { + if (record._status === "deleted") { + // Garbage collect deleted records. + transaction.delete(record.id); + } + else { + // Records that were synced become «created». + transaction.update(Object.assign(Object.assign({}, record), { last_modified: undefined, _status: "created" })); + } + }); + }); + this._lastModified = null; + await this.db.saveLastModified(null); + return unsynced.data.length; + } + /** + * Returns an object containing two lists: + * + * - `toDelete`: unsynced deleted records we can safely delete; + * - `toSync`: local updates to send to the server. + * + * @return {Promise} + */ + async gatherLocalChanges() { + const unsynced = await this.list({ + filters: { _status: ["created", "updated"] }, + order: "", + }); + const deleted = await this.list({ filters: { _status: "deleted" }, order: "" }, { includeDeleted: true }); + return await Promise.all(unsynced.data + .concat(deleted.data) + .map(this._encodeRecord.bind(this, "remote"))); + } + /** + * Fetch remote changes, import them to the local database, and handle + * conflicts according to `options.strategy`. Then, updates the passed + * {@link SyncResultObject} with import results. + * + * Options: + * - {String} strategy: The selected sync strategy. + * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter. + * - {Array} exclude: A list of record ids to exclude from pull. + * - {Object} headers: The HTTP headers to use in the request. + * - {int} retry: The number of retries to do if the HTTP request fails. + * - {int} lastModified: The timestamp to use in `?_since` query. + * + * @param {KintoClient.Collection} client Kinto client Collection instance. + * @param {SyncResultObject} syncResultObject The sync result object. + * @param {Object} options The options object. + * @return {Promise} + */ + async pullChanges(client, syncResultObject, options = {}) { + if (!syncResultObject.ok) { + return syncResultObject; + } + const since = this.lastModified + ? this.lastModified + : await this.db.getLastModified(); + options = Object.assign({ strategy: Collection.strategy.MANUAL, lastModified: since, headers: {} }, options); + // Optionally ignore some records when pulling for changes. + // (avoid redownloading our own changes on last step of #sync()) + let filters; + if (options.exclude) { + // Limit the list of excluded records to the first 50 records in order + // to remain under de-facto URL size limit (~2000 chars). + // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184 + const exclude_id = options.exclude + .slice(0, 50) + .map(r => r.id) + .join(","); + filters = { exclude_id }; + } + if (options.expectedTimestamp) { + filters = Object.assign(Object.assign({}, filters), { _expected: options.expectedTimestamp }); + } + // First fetch remote changes from the server + const { data, last_modified } = await client.listRecords({ + // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356) + since: options.lastModified ? `${options.lastModified}` : undefined, + headers: options.headers, + retry: options.retry, + // Fetch every page by default (FIXME: option to limit pages, see #277) + pages: Infinity, + filters, + }); + // last_modified is the ETag header value (string). + // For retro-compatibility with first kinto.js versions + // parse it to integer. + const unquoted = last_modified ? parseInt(last_modified, 10) : undefined; + // Check if server was flushed. + // This is relevant for the Kinto demo server + // (and thus for many new comers). + const localSynced = options.lastModified; + const serverChanged = unquoted > options.lastModified; + const emptyCollection = data.length === 0; + if (!options.exclude && localSynced && serverChanged && emptyCollection) { + const e = new ServerWasFlushedError(localSynced, unquoted, "Server has been flushed. Client Side Timestamp: " + + localSynced + + " Server Side Timestamp: " + + unquoted); + throw e; + } + // Atomic updates are not sensible here because unquoted is not + // computed as a function of syncResultObject.lastModified. + // eslint-disable-next-line require-atomic-updates + syncResultObject.lastModified = unquoted; + // Decode incoming changes. + const decodedChanges = await Promise.all(data.map(change => { + return this._decodeRecord("remote", change); + })); + // Hook receives decoded records. + const payload = { lastModified: unquoted, changes: decodedChanges }; + const afterHooks = await this.applyHook("incoming-changes", payload); + // No change, nothing to import. + if (afterHooks.changes.length > 0) { + // Reflect these changes locally + await this.importChanges(syncResultObject, afterHooks.changes, options.strategy); + } + return syncResultObject; + } + applyHook(hookName, payload) { + if (typeof this.hooks[hookName] == "undefined") { + return Promise.resolve(payload); + } + return waterfall(this.hooks[hookName].map(hook => { + return record => { + const result = hook(payload, this); + const resultThenable = result && typeof result.then === "function"; + const resultChanges = result && Object.prototype.hasOwnProperty.call(result, "changes"); + if (!(resultThenable || resultChanges)) { + throw new Error(`Invalid return value for hook: ${JSON.stringify(result)} has no 'then()' or 'changes' properties`); + } + return result; + }; + }), payload); + } + /** + * Publish local changes to the remote server and updates the passed + * {@link SyncResultObject} with publication results. + * + * Options: + * - {String} strategy: The selected sync strategy. + * - {Object} headers: The HTTP headers to use in the request. + * - {int} retry: The number of retries to do if the HTTP request fails. + * + * @param {KintoClient.Collection} client Kinto client Collection instance. + * @param {SyncResultObject} syncResultObject The sync result object. + * @param {Object} changes The change object. + * @param {Array} changes.toDelete The list of records to delete. + * @param {Array} changes.toSync The list of records to create/update. + * @param {Object} options The options object. + * @return {Promise} + */ + async pushChanges(client, changes, syncResultObject, options = {}) { + if (!syncResultObject.ok) { + return syncResultObject; + } + const safe = !options.strategy || options.strategy !== Collection.CLIENT_WINS; + const toDelete = changes.filter(r => r._status == "deleted"); + const toSync = changes.filter(r => r._status != "deleted"); + // Perform a batch request with every changes. + const synced = await client.batch(batch => { + toDelete.forEach(r => { + // never published locally deleted records should not be pusblished + if (r.last_modified) { + batch.deleteRecord(r); + } + }); + toSync.forEach(r => { + // Clean local fields (like _status) before sending to server. + const published = this.cleanLocalFields(r); + if (r._status === "created") { + batch.createRecord(published); + } + else { + batch.updateRecord(published); + } + }); + }, { + headers: options.headers, + retry: options.retry, + safe, + aggregate: true, + }); + // Store outgoing errors into sync result object + syncResultObject.add("errors", synced.errors.map(e => (Object.assign(Object.assign({}, e), { type: "outgoing" })))); + // Store outgoing conflicts into sync result object + const conflicts = []; + for (const { type, local, remote } of synced.conflicts) { + // Note: we ensure that local data are actually available, as they may + // be missing in the case of a published deletion. + const safeLocal = (local && local.data) || { id: remote.id }; + const realLocal = await this._decodeRecord("remote", safeLocal); + // We can get "null" from the remote side if we got a conflict + // and there is no remote version available; see kinto-http.js + // batch.js:aggregate. + const realRemote = remote && (await this._decodeRecord("remote", remote)); + const conflict = { type, local: realLocal, remote: realRemote }; + conflicts.push(conflict); + } + syncResultObject.add("conflicts", conflicts); + // Records that must be deleted are either deletions that were pushed + // to server (published) or deleted records that were never pushed (skipped). + const missingRemotely = synced.skipped.map(r => (Object.assign(Object.assign({}, r), { deleted: true }))); + // For created and updated records, the last_modified coming from server + // will be stored locally. + // Reflect publication results locally using the response from + // the batch request. + const published = synced.published.map(c => c.data); + const toApplyLocally = published.concat(missingRemotely); + // Apply the decode transformers, if any + const decoded = await Promise.all(toApplyLocally.map(record => { + return this._decodeRecord("remote", record); + })); + // We have to update the local records with the responses of the server + // (eg. last_modified values etc.). + if (decoded.length > 0 || conflicts.length > 0) { + await this._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy); + } + return syncResultObject; + } + /** + * Return a copy of the specified record without the local fields. + * + * @param {Object} record A record with potential local fields. + * @return {Object} + */ + cleanLocalFields(record) { + const localKeys = RECORD_FIELDS_TO_CLEAN.concat(this.localFields); + return omitKeys(record, localKeys); + } + /** + * Resolves a conflict, updating local record according to proposed + * resolution — keeping remote record `last_modified` value as a reference for + * further batch sending. + * + * @param {Object} conflict The conflict object. + * @param {Object} resolution The proposed record. + * @return {Promise} + */ + resolve(conflict, resolution) { + return this.db.execute(transaction => { + const updated = this._resolveRaw(conflict, resolution); + transaction.update(updated); + return { data: updated, permissions: {} }; + }); + } + /** + * @private + */ + _resolveRaw(conflict, resolution) { + const resolved = Object.assign(Object.assign({}, resolution), { + // Ensure local record has the latest authoritative timestamp + last_modified: conflict.remote && conflict.remote.last_modified }); + // If the resolution object is strictly equal to the + // remote record, then we can mark it as synced locally. + // Otherwise, mark it as updated (so that the resolution is pushed). + const synced = deepEqual(resolved, conflict.remote); + return markStatus(resolved, synced ? "synced" : "updated"); + } + /** + * Synchronize remote and local data. The promise will resolve with a + * {@link SyncResultObject}, though will reject: + * + * - if the server is currently backed off; + * - if the server has been detected flushed. + * + * Options: + * - {Object} headers: HTTP headers to attach to outgoing requests. + * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter. + * - {Number} retry: Number of retries when server fails to process the request (default: 1). + * - {Collection.strategy} strategy: See {@link Collection.strategy}. + * - {Boolean} ignoreBackoff: Force synchronization even if server is currently + * backed off. + * - {String} bucket: The remove bucket id to use (default: null) + * - {String} collection: The remove collection id to use (default: null) + * - {String} remote The remote Kinto server endpoint to use (default: null). + * + * @param {Object} options Options. + * @return {Promise} + * @throws {Error} If an invalid remote option is passed. + */ + async sync(options = { + strategy: Collection.strategy.MANUAL, + headers: {}, + retry: 1, + ignoreBackoff: false, + bucket: null, + collection: null, + remote: null, + expectedTimestamp: null, + }) { + options = Object.assign(Object.assign({}, options), { bucket: options.bucket || this.bucket, collection: options.collection || this.name }); + const previousRemote = this.api.remote; + if (options.remote) { + // Note: setting the remote ensures it's valid, throws when invalid. + this.api.remote = options.remote; + } + if (!options.ignoreBackoff && this.api.backoff > 0) { + const seconds = Math.ceil(this.api.backoff / 1000); + return Promise.reject(new Error(`Server is asking clients to back off; retry in ${seconds}s or use the ignoreBackoff option.`)); + } + const client = this.api + .bucket(options.bucket) + .collection(options.collection); + const result = new SyncResultObject(); + try { + // Fetch collection metadata. + await this.pullMetadata(client, options); + // Fetch last changes from the server. + await this.pullChanges(client, result, options); + const { lastModified } = result; + if (options.strategy != Collection.strategy.PULL_ONLY) { + // Fetch local changes + const toSync = await this.gatherLocalChanges(); + // Publish local changes and pull local resolutions + await this.pushChanges(client, toSync, result, options); + // Publish local resolution of push conflicts to server (on CLIENT_WINS) + const resolvedUnsynced = result.resolved.filter(r => r._status !== "synced"); + if (resolvedUnsynced.length > 0) { + const resolvedEncoded = await Promise.all(resolvedUnsynced.map(resolution => { + let record = resolution.accepted; + if (record === null) { + record = { id: resolution.id, _status: resolution._status }; + } + return this._encodeRecord("remote", record); + })); + await this.pushChanges(client, resolvedEncoded, result, options); + } + // Perform a last pull to catch changes that occured after the last pull, + // while local changes were pushed. Do not do it nothing was pushed. + if (result.published.length > 0) { + // Avoid redownloading our own changes during the last pull. + const pullOpts = Object.assign(Object.assign({}, options), { lastModified, exclude: result.published }); + await this.pullChanges(client, result, pullOpts); + } + } + // Don't persist lastModified value if any conflict or error occured + if (result.ok) { + // No conflict occured, persist collection's lastModified value + this._lastModified = await this.db.saveLastModified(result.lastModified); + } + } + catch (e) { + this.events.emit("sync:error", Object.assign(Object.assign({}, options), { error: e })); + throw e; + } + finally { + // Ensure API default remote is reverted if a custom one's been used + this.api.remote = previousRemote; + } + this.events.emit("sync:success", Object.assign(Object.assign({}, options), { result })); + return result; + } + /** + * Load a list of records already synced with the remote server. + * + * The local records which are unsynced or whose timestamp is either missing + * or superior to those being loaded will be ignored. + * + * @deprecated Use {@link importBulk} instead. + * @param {Array} records The previously exported list of records to load. + * @return {Promise} with the effectively imported records. + */ + async loadDump(records) { + return this.importBulk(records); + } + /** + * Load a list of records already synced with the remote server. + * + * The local records which are unsynced or whose timestamp is either missing + * or superior to those being loaded will be ignored. + * + * @param {Array} records The previously exported list of records to load. + * @return {Promise} with the effectively imported records. + */ + async importBulk(records) { + if (!Array.isArray(records)) { + throw new Error("Records is not an array."); + } + for (const record of records) { + if (!Object.prototype.hasOwnProperty.call(record, "id") || + !this.idSchema.validate(record.id)) { + throw new Error("Record has invalid ID: " + JSON.stringify(record)); + } + if (!record.last_modified) { + throw new Error("Record has no last_modified value: " + JSON.stringify(record)); + } + } + // Fetch all existing records from local database, + // and skip those who are newer or not marked as synced. + // XXX filter by status / ids in records + const { data } = await this.list({}, { includeDeleted: true }); + const existingById = data.reduce((acc, record) => { + acc[record.id] = record; + return acc; + }, {}); + const newRecords = records.filter(record => { + const localRecord = existingById[record.id]; + const shouldKeep = + // No local record with this id. + localRecord === undefined || + // Or local record is synced + (localRecord._status === "synced" && + // And was synced from server + localRecord.last_modified !== undefined && + // And is older than imported one. + record.last_modified > localRecord.last_modified); + return shouldKeep; + }); + return await this.db.importBulk(newRecords.map(markSynced)); + } + async pullMetadata(client, options = {}) { + const { expectedTimestamp, headers } = options; + const query = expectedTimestamp + ? { query: { _expected: expectedTimestamp } } + : undefined; + const metadata = await client.getData(Object.assign(Object.assign({}, query), { headers })); + return this.db.saveMetadata(metadata); + } + async metadata() { + return this.db.getMetadata(); + } + } + /** + * A Collection-oriented wrapper for an adapter's transaction. + * + * This defines the high-level functions available on a collection. + * The collection itself offers functions of the same name. These will + * perform just one operation in its own transaction. + */ + class CollectionTransaction { + constructor(collection, adapterTransaction) { + this.collection = collection; + this.adapterTransaction = adapterTransaction; + this._events = []; + } + _queueEvent(action, payload) { + this._events.push({ action, payload }); + } + /** + * Emit queued events, to be called once every transaction operations have + * been executed successfully. + */ + emitEvents() { + for (const { action, payload } of this._events) { + this.collection.events.emit(action, payload); + } + if (this._events.length > 0) { + const targets = this._events.map(({ action, payload }) => (Object.assign({ action }, payload))); + this.collection.events.emit("change", { targets }); + } + this._events = []; + } + /** + * Retrieve a record by its id from the local database, or + * undefined if none exists. + * + * This will also return virtually deleted records. + * + * @param {String} id + * @return {Object} + */ + getAny(id) { + const record = this.adapterTransaction.get(id); + return { data: record, permissions: {} }; + } + /** + * Retrieve a record by its id from the local database. + * + * Options: + * - {Boolean} includeDeleted: Include virtually deleted records. + * + * @param {String} id + * @param {Object} options + * @return {Object} + */ + get(id, options = { includeDeleted: false }) { + const res = this.getAny(id); + if (!res.data || + (!options.includeDeleted && res.data._status === "deleted")) { + throw new Error(`Record with id=${id} not found.`); + } + return res; + } + /** + * Deletes a record from the local database. + * + * Options: + * - {Boolean} virtual: When set to `true`, doesn't actually delete the record, + * update its `_status` attribute to `deleted` instead (default: true) + * + * @param {String} id The record's Id. + * @param {Object} options The options object. + * @return {Object} + */ + delete(id, options = { virtual: true }) { + // Ensure the record actually exists. + const existing = this.adapterTransaction.get(id); + const alreadyDeleted = existing && existing._status == "deleted"; + if (!existing || (alreadyDeleted && options.virtual)) { + throw new Error(`Record with id=${id} not found.`); + } + // Virtual updates status. + if (options.virtual) { + this.adapterTransaction.update(markDeleted(existing)); + } + else { + // Delete for real. + this.adapterTransaction.delete(id); + } + this._queueEvent("delete", { data: existing }); + return { data: existing, permissions: {} }; + } + /** + * Soft delete all records from the local database. + * + * @param {Array} ids Array of non-deleted Record Ids. + * @return {Object} + */ + deleteAll(ids) { + const existingRecords = []; + ids.forEach(id => { + existingRecords.push(this.adapterTransaction.get(id)); + this.delete(id); + }); + this._queueEvent("deleteAll", { data: existingRecords }); + return { data: existingRecords, permissions: {} }; + } + /** + * Deletes a record from the local database, if any exists. + * Otherwise, do nothing. + * + * @param {String} id The record's Id. + * @return {Object} + */ + deleteAny(id) { + const existing = this.adapterTransaction.get(id); + if (existing) { + this.adapterTransaction.update(markDeleted(existing)); + this._queueEvent("delete", { data: existing }); + } + return { data: Object.assign({ id }, existing), deleted: !!existing, permissions: {} }; + } + /** + * Adds a record to the local database, asserting that none + * already exist with this ID. + * + * @param {Object} record, which must contain an ID + * @return {Object} + */ + create(record) { + if (typeof record !== "object") { + throw new Error("Record is not an object."); + } + if (!Object.prototype.hasOwnProperty.call(record, "id")) { + throw new Error("Cannot create a record missing id"); + } + if (!this.collection.idSchema.validate(record.id)) { + throw new Error(`Invalid Id: ${record.id}`); + } + this.adapterTransaction.create(record); + this._queueEvent("create", { data: record }); + return { data: record, permissions: {} }; + } + /** + * Updates a record from the local database. + * + * Options: + * - {Boolean} synced: Sets record status to "synced" (default: false) + * - {Boolean} patch: Extends the existing record instead of overwriting it + * (default: false) + * + * @param {Object} record + * @param {Object} options + * @return {Object} + */ + update(record, options = { synced: false, patch: false }) { + if (typeof record !== "object") { + throw new Error("Record is not an object."); + } + if (!Object.prototype.hasOwnProperty.call(record, "id")) { + throw new Error("Cannot update a record missing id."); + } + if (!this.collection.idSchema.validate(record.id)) { + throw new Error(`Invalid Id: ${record.id}`); + } + const oldRecord = this.adapterTransaction.get(record.id); + if (!oldRecord) { + throw new Error(`Record with id=${record.id} not found.`); + } + const newRecord = options.patch ? Object.assign(Object.assign({}, oldRecord), record) : record; + const updated = this._updateRaw(oldRecord, newRecord, options); + this.adapterTransaction.update(updated); + this._queueEvent("update", { data: updated, oldRecord }); + return { data: updated, oldRecord, permissions: {} }; + } + /** + * Lower-level primitive for updating a record while respecting + * _status and last_modified. + * + * @param {Object} oldRecord: the record retrieved from the DB + * @param {Object} newRecord: the record to replace it with + * @return {Object} + */ + _updateRaw(oldRecord, newRecord, { synced = false } = {}) { + const updated = Object.assign({}, newRecord); + // Make sure to never loose the existing timestamp. + if (oldRecord && oldRecord.last_modified && !updated.last_modified) { + updated.last_modified = oldRecord.last_modified; + } + // If only local fields have changed, then keep record as synced. + // If status is created, keep record as created. + // If status is deleted, mark as updated. + const isIdentical = oldRecord && + recordsEqual(oldRecord, updated, this.collection.localFields); + const keepSynced = isIdentical && oldRecord._status == "synced"; + const neverSynced = !oldRecord || (oldRecord && oldRecord._status == "created"); + const newStatus = keepSynced || synced ? "synced" : neverSynced ? "created" : "updated"; + return markStatus(updated, newStatus); + } + /** + * Upsert a record into the local database. + * + * This record must have an ID. + * + * If a record with this ID already exists, it will be replaced. + * Otherwise, this record will be inserted. + * + * @param {Object} record + * @return {Object} + */ + upsert(record) { + if (typeof record !== "object") { + throw new Error("Record is not an object."); + } + if (!Object.prototype.hasOwnProperty.call(record, "id")) { + throw new Error("Cannot update a record missing id."); + } + if (!this.collection.idSchema.validate(record.id)) { + throw new Error(`Invalid Id: ${record.id}`); + } + let oldRecord = this.adapterTransaction.get(record.id); + const updated = this._updateRaw(oldRecord, record); + this.adapterTransaction.update(updated); + // Don't return deleted records -- pretend they are gone + if (oldRecord && oldRecord._status == "deleted") { + oldRecord = undefined; + } + if (oldRecord) { + this._queueEvent("update", { data: updated, oldRecord }); + } + else { + this._queueEvent("create", { data: updated }); + } + return { data: updated, oldRecord, permissions: {} }; + } + } + + const DEFAULT_BUCKET_NAME = "default"; + const DEFAULT_REMOTE = "http://localhost:8888/v1"; + const DEFAULT_RETRY = 1; + /** + * KintoBase class. + */ + class KintoBase { + /** + * Provides a public access to the base adapter class. Users can create a + * custom DB adapter by extending {@link BaseAdapter}. + * + * @type {Object} + */ + static get adapters() { + return { + BaseAdapter: BaseAdapter, + }; + } + /** + * Synchronization strategies. Available strategies are: + * + * - `MANUAL`: Conflicts will be reported in a dedicated array. + * - `SERVER_WINS`: Conflicts are resolved using remote data. + * - `CLIENT_WINS`: Conflicts are resolved using local data. + * + * @type {Object} + */ + static get syncStrategy() { + return Collection.strategy; + } + /** + * Constructor. + * + * Options: + * - `{String}` `remote` The server URL to use. + * - `{String}` `bucket` The collection bucket name. + * - `{EventEmitter}` `events` Events handler. + * - `{BaseAdapter}` `adapter` The base DB adapter class. + * - `{Object}` `adapterOptions` Options given to the adapter. + * - `{Object}` `headers` The HTTP headers to use. + * - `{Object}` `retry` Number of retries when the server fails to process the request (default: `1`) + * - `{String}` `requestMode` The HTTP CORS mode to use. + * - `{Number}` `timeout` The requests timeout in ms (default: `5000`). + * + * @param {Object} options The options object. + */ + constructor(options = {}) { + const defaults = { + bucket: DEFAULT_BUCKET_NAME, + remote: DEFAULT_REMOTE, + retry: DEFAULT_RETRY, + }; + this._options = Object.assign(Object.assign({}, defaults), options); + if (!this._options.adapter) { + throw new Error("No adapter provided"); + } + this._api = null; + /** + * The event emitter instance. + * @type {EventEmitter} + */ + this.events = this._options.events; + } + /** + * The kinto HTTP client instance. + * @type {KintoClient} + */ + get api() { + const { events, headers, remote, requestMode, retry, timeout, } = this._options; + if (!this._api) { + this._api = new this.ApiClass(remote, { + events, + headers, + requestMode, + retry, + timeout, + }); + } + return this._api; + } + /** + * Creates a {@link Collection} instance. The second (optional) parameter + * will set collection-level options like e.g. `remoteTransformers`. + * + * @param {String} collName The collection name. + * @param {Object} [options={}] Extra options or override client's options. + * @param {Object} [options.idSchema] IdSchema instance (default: UUID) + * @param {Object} [options.remoteTransformers] Array (default: `[]`]) + * @param {Object} [options.hooks] Array (default: `[]`]) + * @param {Object} [options.localFields] Array (default: `[]`]) + * @return {Collection} + */ + collection(collName, options = {}) { + if (!collName) { + throw new Error("missing collection name"); + } + const { bucket, events, adapter, adapterOptions } = Object.assign(Object.assign({}, this._options), options); + const { idSchema, remoteTransformers, hooks, localFields } = options; + return new Collection(bucket, collName, this, { + events, + adapter, + adapterOptions, + idSchema, + remoteTransformers, + hooks, + localFields, + }); + } + } + + /* + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + const { setTimeout, clearTimeout } = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs"); + const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs"); + XPCOMUtils.defineLazyGlobalGetters(global, ["fetch", "indexedDB"]); + ChromeUtils.defineESModuleGetters(global, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + // Use standalone kinto-http module landed in FFx. + KintoHttpClient: "resource://services-common/kinto-http-client.sys.mjs" + }); + ChromeUtils.defineLazyGetter(global, "generateUUID", () => { + const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + return generateUUID; + }); + class Kinto extends KintoBase { + static get adapters() { + return { + BaseAdapter, + IDB, + }; + } + get ApiClass() { + return KintoHttpClient; + } + constructor(options = {}) { + const events = {}; + EventEmitter.decorate(events); + const defaults = { + adapter: IDB, + events, + }; + super(Object.assign(Object.assign({}, defaults), options)); + } + collection(collName, options = {}) { + const idSchema = { + validate(id) { + return typeof id == "string" && RE_RECORD_ID.test(id); + }, + generate() { + return generateUUID() + .toString() + .replace(/[{}]/g, ""); + }, + }; + return super.collection(collName, Object.assign({ idSchema }, options)); + } + } + + return Kinto; + +}))); diff --git a/services/common/kinto-storage-adapter.sys.mjs b/services/common/kinto-storage-adapter.sys.mjs new file mode 100644 index 0000000000..75c6c06a26 --- /dev/null +++ b/services/common/kinto-storage-adapter.sys.mjs @@ -0,0 +1,553 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs"; + +const { Kinto } = ChromeUtils.import( + "resource://services-common/kinto-offline-client.js" +); + +/** + * Filter and sort list against provided filters and order. + * + * @param {Object} filters The filters to apply. + * @param {String} order The order to apply. + * @param {Array} list The list to reduce. + * @return {Array} + */ +function reduceRecords(filters, order, list) { + const filtered = filters ? filterObjects(filters, list) : list; + return order ? sortObjects(order, filtered) : filtered; +} + +/** + * Checks if a value is undefined. + * + * This is a copy of `_isUndefined` from kinto.js/src/utils.js. + * @param {Any} value + * @return {Boolean} + */ +function _isUndefined(value) { + return typeof value === "undefined"; +} + +/** + * Sorts records in a list according to a given ordering. + * + * This is a copy of `sortObjects` from kinto.js/src/utils.js. + * + * @param {String} order The ordering, eg. `-last_modified`. + * @param {Array} list The collection to order. + * @return {Array} + */ +function sortObjects(order, list) { + const hasDash = order[0] === "-"; + const field = hasDash ? order.slice(1) : order; + const direction = hasDash ? -1 : 1; + return list.slice().sort((a, b) => { + if (a[field] && _isUndefined(b[field])) { + return direction; + } + if (b[field] && _isUndefined(a[field])) { + return -direction; + } + if (_isUndefined(a[field]) && _isUndefined(b[field])) { + return 0; + } + return a[field] > b[field] ? direction : -direction; + }); +} + +/** + * Test if a single object matches all given filters. + * + * This is a copy of `filterObject` from kinto.js/src/utils.js. + * + * @param {Object} filters The filters object. + * @param {Object} entry The object to filter. + * @return {Function} + */ +function filterObject(filters, entry) { + return Object.keys(filters).every(filter => { + const value = filters[filter]; + if (Array.isArray(value)) { + return value.some(candidate => candidate === entry[filter]); + } + return entry[filter] === value; + }); +} + +/** + * Filters records in a list matching all given filters. + * + * This is a copy of `filterObjects` from kinto.js/src/utils.js. + * + * @param {Object} filters The filters object. + * @param {Array} list The collection to filter. + * @return {Array} + */ +function filterObjects(filters, list) { + return list.filter(entry => { + return filterObject(filters, entry); + }); +} + +const statements = { + createCollectionData: ` + CREATE TABLE collection_data ( + collection_name TEXT, + record_id TEXT, + record TEXT + );`, + + createCollectionMetadata: ` + CREATE TABLE collection_metadata ( + collection_name TEXT PRIMARY KEY, + last_modified INTEGER, + metadata TEXT + ) WITHOUT ROWID;`, + + createCollectionDataRecordIdIndex: ` + CREATE UNIQUE INDEX unique_collection_record + ON collection_data(collection_name, record_id);`, + + clearData: ` + DELETE FROM collection_data + WHERE collection_name = :collection_name;`, + + createData: ` + INSERT INTO collection_data (collection_name, record_id, record) + VALUES (:collection_name, :record_id, :record);`, + + updateData: ` + INSERT OR REPLACE INTO collection_data (collection_name, record_id, record) + VALUES (:collection_name, :record_id, :record);`, + + deleteData: ` + DELETE FROM collection_data + WHERE collection_name = :collection_name + AND record_id = :record_id;`, + + saveLastModified: ` + INSERT INTO collection_metadata(collection_name, last_modified) + VALUES(:collection_name, :last_modified) + ON CONFLICT(collection_name) DO UPDATE SET last_modified = :last_modified`, + + getLastModified: ` + SELECT last_modified + FROM collection_metadata + WHERE collection_name = :collection_name;`, + + saveMetadata: ` + INSERT INTO collection_metadata(collection_name, metadata) + VALUES(:collection_name, :metadata) + ON CONFLICT(collection_name) DO UPDATE SET metadata = :metadata`, + + getMetadata: ` + SELECT metadata + FROM collection_metadata + WHERE collection_name = :collection_name;`, + + getRecord: ` + SELECT record + FROM collection_data + WHERE collection_name = :collection_name + AND record_id = :record_id;`, + + listRecords: ` + SELECT record + FROM collection_data + WHERE collection_name = :collection_name;`, + + // N.B. we have to have a dynamic number of placeholders, which you + // can't do without building your own statement. See `execute` for details + listRecordsById: ` + SELECT record_id, record + FROM collection_data + WHERE collection_name = ? + AND record_id IN `, + + importData: ` + REPLACE INTO collection_data (collection_name, record_id, record) + VALUES (:collection_name, :record_id, :record);`, + + scanAllRecords: `SELECT * FROM collection_data;`, + + clearCollectionMetadata: `DELETE FROM collection_metadata;`, + + calculateStorage: ` + SELECT collection_name, SUM(LENGTH(record)) as size, COUNT(record) as num_records + FROM collection_data + GROUP BY collection_name;`, + + addMetadataColumn: ` + ALTER TABLE collection_metadata + ADD COLUMN metadata TEXT;`, +}; + +const createStatements = [ + "createCollectionData", + "createCollectionMetadata", + "createCollectionDataRecordIdIndex", +]; + +const currentSchemaVersion = 2; + +/** + * Firefox adapter. + * + * Uses Sqlite as a backing store. + * + * Options: + * - sqliteHandle: a handle to the Sqlite database this adapter will + * use as its backing store. To open such a handle, use the + * static openConnection() method. + */ +export class FirefoxAdapter extends Kinto.adapters.BaseAdapter { + constructor(collection, options = {}) { + super(); + const { sqliteHandle = null } = options; + this.collection = collection; + this._connection = sqliteHandle; + this._options = options; + } + + /** + * Initialize a Sqlite connection to be suitable for use with Kinto. + * + * This will be called automatically by open(). + */ + static async _init(connection) { + await connection.executeTransaction(async function doSetup() { + const schema = await connection.getSchemaVersion(); + + if (schema == 0) { + for (let statementName of createStatements) { + await connection.execute(statements[statementName]); + } + await connection.setSchemaVersion(currentSchemaVersion); + } else if (schema == 1) { + await connection.execute(statements.addMetadataColumn); + await connection.setSchemaVersion(currentSchemaVersion); + } else if (schema != 2) { + throw new Error("Unknown database schema: " + schema); + } + }); + return connection; + } + + _executeStatement(statement, params) { + return this._connection.executeCached(statement, params); + } + + /** + * Open and initialize a Sqlite connection to a database that Kinto + * can use. When you are done with this connection, close it by + * calling close(). + * + * Options: + * - path: The path for the Sqlite database + * + * @returns SqliteConnection + */ + static async openConnection(options) { + const opts = Object.assign({}, { sharedMemoryCache: false }, options); + const conn = await Sqlite.openConnection(opts).then(this._init); + try { + Sqlite.shutdown.addBlocker( + "Kinto storage adapter connection closing", + () => conn.close() + ); + } catch (e) { + // It's too late to block shutdown, just close the connection. + await conn.close(); + throw e; + } + return conn; + } + + clear() { + const params = { collection_name: this.collection }; + return this._executeStatement(statements.clearData, params); + } + + execute(callback, options = { preload: [] }) { + let result; + const conn = this._connection; + const collection = this.collection; + + return conn + .executeTransaction(async function doExecuteTransaction() { + // Preload specified records from DB, within transaction. + + // if options.preload has more elements than the sqlite variable + // limit, split it up. + const limit = 100; + let preloaded = {}; + let preload; + let more = options.preload; + + while (more.length) { + preload = more.slice(0, limit); + more = more.slice(limit, more.length); + + const parameters = [collection, ...preload]; + const placeholders = preload.map(_ => "?"); + const stmt = + statements.listRecordsById + "(" + placeholders.join(",") + ");"; + const rows = await conn.execute(stmt, parameters); + + rows.reduce((acc, row) => { + const record = JSON.parse(row.getResultByName("record")); + acc[row.getResultByName("record_id")] = record; + return acc; + }, preloaded); + } + const proxy = transactionProxy(collection, preloaded); + result = callback(proxy); + + for (let { statement, params } of proxy.operations) { + await conn.executeCached(statement, params); + } + }, conn.TRANSACTION_EXCLUSIVE) + .then(_ => result); + } + + get(id) { + const params = { + collection_name: this.collection, + record_id: id, + }; + return this._executeStatement(statements.getRecord, params).then(result => { + if (!result.length) { + return null; + } + return JSON.parse(result[0].getResultByName("record")); + }); + } + + list(params = { filters: {}, order: "" }) { + const parameters = { + collection_name: this.collection, + }; + return this._executeStatement(statements.listRecords, parameters) + .then(result => { + const records = []; + for (let k = 0; k < result.length; k++) { + const row = result[k]; + records.push(JSON.parse(row.getResultByName("record"))); + } + return records; + }) + .then(results => { + // The resulting list of records is filtered and sorted. + // XXX: with some efforts, this could be implemented using SQL. + return reduceRecords(params.filters, params.order, results); + }); + } + + async loadDump(records) { + return this.importBulk(records); + } + + /** + * Load a list of records into the local database. + * + * Note: The adapter is not in charge of filtering the already imported + * records. This is done in `Collection#loadDump()`, as a common behaviour + * between every adapters. + * + * @param {Array} records. + * @return {Array} imported records. + */ + async importBulk(records) { + const connection = this._connection; + const collection_name = this.collection; + await connection.executeTransaction(async function doImport() { + for (let record of records) { + const params = { + collection_name, + record_id: record.id, + record: JSON.stringify(record), + }; + await connection.execute(statements.importData, params); + } + const lastModified = Math.max( + ...records.map(record => record.last_modified) + ); + const params = { + collection_name, + }; + const previousLastModified = await connection + .execute(statements.getLastModified, params) + .then(result => { + return result.length + ? result[0].getResultByName("last_modified") + : -1; + }); + if (lastModified > previousLastModified) { + const params = { + collection_name, + last_modified: lastModified, + }; + await connection.execute(statements.saveLastModified, params); + } + }); + return records; + } + + saveLastModified(lastModified) { + const parsedLastModified = parseInt(lastModified, 10) || null; + const params = { + collection_name: this.collection, + last_modified: parsedLastModified, + }; + return this._executeStatement(statements.saveLastModified, params).then( + () => parsedLastModified + ); + } + + getLastModified() { + const params = { + collection_name: this.collection, + }; + return this._executeStatement(statements.getLastModified, params).then( + result => { + if (!result.length) { + return 0; + } + return result[0].getResultByName("last_modified"); + } + ); + } + + async saveMetadata(metadata) { + const params = { + collection_name: this.collection, + metadata: JSON.stringify(metadata), + }; + await this._executeStatement(statements.saveMetadata, params); + return metadata; + } + + async getMetadata() { + const params = { + collection_name: this.collection, + }; + const result = await this._executeStatement(statements.getMetadata, params); + if (!result.length) { + return null; + } + return JSON.parse(result[0].getResultByName("metadata")); + } + + calculateStorage() { + return this._executeStatement(statements.calculateStorage, {}).then( + result => { + return Array.from(result, row => ({ + collectionName: row.getResultByName("collection_name"), + size: row.getResultByName("size"), + numRecords: row.getResultByName("num_records"), + })); + } + ); + } + + /** + * Reset the sync status of every record and collection we have + * access to. + */ + resetSyncStatus() { + // We're going to use execute instead of executeCached, so build + // in our own sanity check + if (!this._connection) { + throw new Error("The storage adapter is not open"); + } + + return this._connection.executeTransaction(async function (conn) { + const promises = []; + await conn.execute(statements.scanAllRecords, null, function (row) { + const record = JSON.parse(row.getResultByName("record")); + const record_id = row.getResultByName("record_id"); + const collection_name = row.getResultByName("collection_name"); + if (record._status === "deleted") { + // Garbage collect deleted records. + promises.push( + conn.execute(statements.deleteData, { collection_name, record_id }) + ); + } else { + const newRecord = Object.assign({}, record, { + _status: "created", + last_modified: undefined, + }); + promises.push( + conn.execute(statements.updateData, { + record: JSON.stringify(newRecord), + record_id, + collection_name, + }) + ); + } + }); + await Promise.all(promises); + await conn.execute(statements.clearCollectionMetadata); + }); + } +} + +function transactionProxy(collection, preloaded) { + const _operations = []; + + return { + get operations() { + return _operations; + }, + + create(record) { + _operations.push({ + statement: statements.createData, + params: { + collection_name: collection, + record_id: record.id, + record: JSON.stringify(record), + }, + }); + }, + + update(record) { + _operations.push({ + statement: statements.updateData, + params: { + collection_name: collection, + record_id: record.id, + record: JSON.stringify(record), + }, + }); + }, + + delete(id) { + _operations.push({ + statement: statements.deleteData, + params: { + collection_name: collection, + record_id: id, + }, + }); + }, + + get(id) { + // Gecko JS engine outputs undesired warnings if id is not in preloaded. + return id in preloaded ? preloaded[id] : undefined; + }, + }; +} diff --git a/services/common/logmanager.sys.mjs b/services/common/logmanager.sys.mjs new file mode 100644 index 0000000000..724cfde38b --- /dev/null +++ b/services/common/logmanager.sys.mjs @@ -0,0 +1,447 @@ +/* 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;"; + +import { Log } from "resource://gre/modules/Log.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", +}); + +const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days + +// "shared" logs (ie, where the same log name is used by multiple LogManager +// instances) are a fact of life here - eg, FirefoxAccounts logs are used by +// both Sync and Reading List. +// However, different instances have different pref branches, so we need to +// handle when one pref branch says "Debug" and the other says "Error" +// So we (a) keep singleton console and dump appenders and (b) keep track +// of the minimum (ie, most verbose) level and use that. +// This avoids (a) the most recent setter winning (as that is indeterminate) +// and (b) multiple dump/console appenders being added to the same log multiple +// times, which would cause messages to appear twice. + +// Singletons used by each instance. +var formatter; +var dumpAppender; +var consoleAppender; + +// A set of all preference roots used by all instances. +var allBranches = new Set(); + +const STREAM_SEGMENT_SIZE = 4096; +const PR_UINT32_MAX = 0xffffffff; + +/** + * Append to an nsIStorageStream + * + * This writes logging output to an in-memory stream which can later be read + * back as an nsIInputStream. It can be used to avoid expensive I/O operations + * during logging. Instead, one can periodically consume the input stream and + * e.g. write it to disk asynchronously. + */ +class StorageStreamAppender extends Log.Appender { + constructor(formatter) { + super(formatter); + this._name = "StorageStreamAppender"; + + this._converterStream = null; // holds the nsIConverterOutputStream + this._outputStream = null; // holds the underlying nsIOutputStream + + this._ss = null; + } + + get outputStream() { + if (!this._outputStream) { + // First create a raw stream. We can bail out early if that fails. + this._outputStream = this.newOutputStream(); + if (!this._outputStream) { + return null; + } + + // Wrap the raw stream in an nsIConverterOutputStream. We can reuse + // the instance if we already have one. + if (!this._converterStream) { + this._converterStream = Cc[ + "@mozilla.org/intl/converter-output-stream;1" + ].createInstance(Ci.nsIConverterOutputStream); + } + this._converterStream.init(this._outputStream, "UTF-8"); + } + return this._converterStream; + } + + newOutputStream() { + let ss = (this._ss = Cc["@mozilla.org/storagestream;1"].createInstance( + Ci.nsIStorageStream + )); + ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null); + return ss.getOutputStream(0); + } + + getInputStream() { + if (!this._ss) { + return null; + } + return this._ss.newInputStream(0); + } + + reset() { + if (!this._outputStream) { + return; + } + this.outputStream.close(); + this._outputStream = null; + this._ss = null; + } + + doAppend(formatted) { + if (!formatted) { + return; + } + try { + this.outputStream.writeString(formatted + "\n"); + } catch (ex) { + if (ex.result == Cr.NS_BASE_STREAM_CLOSED) { + // The underlying output stream is closed, so let's open a new one + // and try again. + this._outputStream = null; + } + try { + this.outputStream.writeString(formatted + "\n"); + } catch (ex) { + // Ah well, we tried, but something seems to be hosed permanently. + } + } + } +} + +// A storage appender that is flushable to a file on disk. Policies for +// when to flush, to what file, log rotation etc are up to the consumer +// (although it does maintain a .sawError property to help the consumer decide +// based on its policies) +class FlushableStorageAppender extends StorageStreamAppender { + constructor(formatter) { + super(formatter); + this.sawError = false; + } + + append(message) { + if (message.level >= Log.Level.Error) { + this.sawError = true; + } + StorageStreamAppender.prototype.append.call(this, message); + } + + reset() { + super.reset(); + this.sawError = false; + } + + // Flush the current stream to a file. Somewhat counter-intuitively, you + // must pass a log which will be written to with details of the operation. + async flushToFile(subdirArray, filename, log) { + let inStream = this.getInputStream(); + this.reset(); + if (!inStream) { + log.debug("Failed to flush log to a file - no input stream"); + return; + } + log.debug("Flushing file log"); + log.trace("Beginning stream copy to " + filename + ": " + Date.now()); + try { + await this._copyStreamToFile(inStream, subdirArray, filename, log); + log.trace("onCopyComplete", Date.now()); + } catch (ex) { + log.error("Failed to copy log stream to file", ex); + } + } + + /** + * Copy an input stream to the named file, doing everything off the main + * thread. + * subDirArray is an array of path components, relative to the profile + * directory, where the file will be created. + * outputFileName is the filename to create. + * Returns a promise that is resolved on completion or rejected with an error. + */ + async _copyStreamToFile(inputStream, subdirArray, outputFileName, log) { + let outputDirectory = PathUtils.join(PathUtils.profileDir, ...subdirArray); + await IOUtils.makeDirectory(outputDirectory); + let fullOutputFileName = PathUtils.join(outputDirectory, outputFileName); + + let outputStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + + outputStream.init( + new lazy.FileUtils.File(fullOutputFileName), + -1, + -1, + Ci.nsIFileOutputStream.DEFER_OPEN + ); + + await new Promise(resolve => + lazy.NetUtil.asyncCopy(inputStream, outputStream, () => resolve()) + ); + + outputStream.close(); + log.trace("finished copy to", fullOutputFileName); + } +} + +// The public LogManager object. +export function LogManager(prefRoot, logNames, logFilePrefix) { + this._prefObservers = []; + this.init(prefRoot, logNames, logFilePrefix); +} + +LogManager.StorageStreamAppender = StorageStreamAppender; + +LogManager.prototype = { + _cleaningUpFileLogs: false, + + init(prefRoot, logNames, logFilePrefix) { + this._prefs = Services.prefs.getBranch(prefRoot); + this._prefsBranch = prefRoot; + + this.logFilePrefix = logFilePrefix; + if (!formatter) { + // Create a formatter and various appenders to attach to the logs. + formatter = new Log.BasicFormatter(); + consoleAppender = new Log.ConsoleAppender(formatter); + dumpAppender = new Log.DumpAppender(formatter); + } + + allBranches.add(this._prefsBranch); + // We create a preference observer for all our prefs so they are magically + // reflected if the pref changes after creation. + let setupAppender = ( + appender, + prefName, + defaultLevel, + findSmallest = false + ) => { + let observer = newVal => { + let level = Log.Level[newVal] || defaultLevel; + if (findSmallest) { + // As some of our appenders have global impact (ie, there is only one + // place 'dump' goes to), we need to find the smallest value from all + // prefs controlling this appender. + // For example, if consumerA has dump=Debug then consumerB sets + // dump=Error, we need to keep dump=Debug so consumerA is respected. + for (let branch of allBranches) { + let lookPrefBranch = Services.prefs.getBranch(branch); + let lookVal = + Log.Level[lookPrefBranch.getStringPref(prefName, null)]; + if (lookVal && lookVal < level) { + level = lookVal; + } + } + } + appender.level = level; + }; + this._prefs.addObserver(prefName, observer); + this._prefObservers.push([prefName, observer]); + // and call the observer now with the current pref value. + observer(this._prefs.getStringPref(prefName, null)); + return observer; + }; + + this._observeConsolePref = setupAppender( + consoleAppender, + "log.appender.console", + Log.Level.Fatal, + true + ); + this._observeDumpPref = setupAppender( + dumpAppender, + "log.appender.dump", + Log.Level.Error, + true + ); + + // The file appender doesn't get the special singleton behaviour. + let fapp = (this._fileAppender = new FlushableStorageAppender(formatter)); + // the stream gets a default of Debug as the user must go out of their way + // to see the stuff spewed to it. + this._observeStreamPref = setupAppender( + fapp, + "log.appender.file.level", + Log.Level.Debug + ); + + // now attach the appenders to all our logs. + for (let logName of logNames) { + let log = Log.repository.getLogger(logName); + for (let appender of [fapp, dumpAppender, consoleAppender]) { + log.addAppender(appender); + } + } + // and use the first specified log as a "root" for our log. + this._log = Log.repository.getLogger(logNames[0] + ".LogManager"); + }, + + /** + * Cleanup this instance + */ + finalize() { + for (let [prefName, observer] of this._prefObservers) { + this._prefs.removeObserver(prefName, observer); + } + this._prefObservers = []; + try { + allBranches.delete(this._prefsBranch); + } catch (e) {} + this._prefs = null; + }, + + get _logFileSubDirectoryEntries() { + // At this point we don't allow a custom directory for the logs, nor allow + // it to be outside the profile directory. + // This returns an array of the the relative directory entries below the + // profile dir, and is the directory about:sync-log uses. + return ["weave", "logs"]; + }, + + get sawError() { + return this._fileAppender.sawError; + }, + + // Result values for resetFileLog. + SUCCESS_LOG_WRITTEN: "success-log-written", + ERROR_LOG_WRITTEN: "error-log-written", + + /** + * Possibly generate a log file for all accumulated log messages and refresh + * the input & output streams. + * Whether a "success" or "error" log is written is determined based on + * whether an "Error" log entry was written to any of the logs. + * Returns a promise that resolves on completion with either null (for no + * file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was + * written, or ERROR_LOG_WRITTEN if an "error" log was written. + */ + async resetFileLog() { + try { + let flushToFile; + let reasonPrefix; + let reason; + if (this._fileAppender.sawError) { + reason = this.ERROR_LOG_WRITTEN; + flushToFile = this._prefs.getBoolPref( + "log.appender.file.logOnError", + true + ); + reasonPrefix = "error"; + } else { + reason = this.SUCCESS_LOG_WRITTEN; + flushToFile = this._prefs.getBoolPref( + "log.appender.file.logOnSuccess", + false + ); + reasonPrefix = "success"; + } + + // might as well avoid creating an input stream if we aren't going to use it. + if (!flushToFile) { + this._fileAppender.reset(); + return null; + } + + // We have reasonPrefix at the start of the filename so all "error" + // logs are grouped in about:sync-log. + let filename = + reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt"; + await this._fileAppender.flushToFile( + this._logFileSubDirectoryEntries, + filename, + this._log + ); + // It's not completely clear to markh why we only do log cleanups + // for errors, but for now the Sync semantics have been copied... + // (one theory is that only cleaning up on error makes it less + // likely old error logs would be removed, but that's not true if + // there are occasional errors - let's address this later!) + if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) { + this._log.trace("Running cleanup."); + try { + await this.cleanupLogs(); + } catch (err) { + this._log.error("Failed to cleanup logs", err); + } + } + return reason; + } catch (ex) { + this._log.error("Failed to resetFileLog", ex); + return null; + } + }, + + /** + * Finds all logs older than maxErrorAge and deletes them using async I/O. + */ + cleanupLogs() { + let maxAge = this._prefs.getIntPref( + "log.appender.file.maxErrorAge", + DEFAULT_MAX_ERROR_AGE + ); + let threshold = Date.now() - 1000 * maxAge; + this._log.debug("Log cleanup threshold time: " + threshold); + + let shouldDelete = fileInfo => { + return fileInfo.lastModified < threshold; + }; + return this._deleteLogFiles(shouldDelete); + }, + + /** + * Finds all logs and removes them. + */ + removeAllLogs() { + return this._deleteLogFiles(() => true); + }, + + // Delete some log files. A callback is invoked for each found log file to + // determine if that file should be removed. + async _deleteLogFiles(cbShouldDelete) { + this._cleaningUpFileLogs = true; + let logDir = lazy.FileUtils.getDir( + "ProfD", + this._logFileSubDirectoryEntries + ); + for (const path of await IOUtils.getChildren(logDir.path)) { + const name = PathUtils.filename(path); + + if (!name.startsWith("error-") && !name.startsWith("success-")) { + continue; + } + + try { + const info = await IOUtils.stat(path); + if (!cbShouldDelete(info)) { + continue; + } + + this._log.trace(` > Cleanup removing ${name} (${info.lastModified})`); + await IOUtils.remove(path); + this._log.trace(`Deleted ${name}`); + } catch (ex) { + this._log.debug( + `Encountered error trying to clean up old log file ${name}`, + ex + ); + } + } + this._cleaningUpFileLogs = false; + this._log.debug("Done deleting files."); + // This notification is used only for tests. + Services.obs.notifyObservers( + null, + "services-tests:common:log-manager:cleanup-logs" + ); + }, +}; diff --git a/services/common/modules-testing/logging.sys.mjs b/services/common/modules-testing/logging.sys.mjs new file mode 100644 index 0000000000..63bd306b67 --- /dev/null +++ b/services/common/modules-testing/logging.sys.mjs @@ -0,0 +1,56 @@ +/* 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 { Log } from "resource://gre/modules/Log.sys.mjs"; + +export function initTestLogging(level) { + function LogStats() { + this.errorsLogged = 0; + } + LogStats.prototype = { + format: function format(message) { + if (message.level == Log.Level.Error) { + this.errorsLogged += 1; + } + + return ( + message.time + + "\t" + + message.loggerName + + "\t" + + message.levelDesc + + "\t" + + this.formatText(message) + + "\n" + ); + }, + }; + Object.setPrototypeOf(LogStats.prototype, new Log.BasicFormatter()); + + let log = Log.repository.rootLogger; + let logStats = new LogStats(); + let appender = new Log.DumpAppender(logStats); + + if (typeof level == "undefined") { + level = "Debug"; + } + getTestLogger().level = Log.Level[level]; + Log.repository.getLogger("Services").level = Log.Level[level]; + + log.level = Log.Level.Trace; + appender.level = Log.Level.Trace; + // Overwrite any other appenders (e.g. from previous incarnations) + log.ownAppenders = [appender]; + log.updateAppenders(); + + // SQLite logging is noisy in these tests - we make it quiet by default + // (although individual tests are free to bump it later) + Log.repository.getLogger("Sqlite").level = Log.Level.Info; + + return logStats; +} + +export function getTestLogger(component) { + return Log.repository.getLogger("Testing"); +} diff --git a/services/common/moz.build b/services/common/moz.build new file mode 100644 index 0000000000..3585dc5426 --- /dev/null +++ b/services/common/moz.build @@ -0,0 +1,47 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Cloud Services", "Firefox: Common") + +TEST_DIRS += ["tests"] + +EXPORTS.mozilla.appservices += [ + "app_services_logger/AppServicesLoggerComponents.h", +] + +EXTRA_COMPONENTS += [ + "servicesComponents.manifest", +] + +EXTRA_JS_MODULES["services-common"] += [ + "async.sys.mjs", + "kinto-http-client.sys.mjs", + "kinto-offline-client.js", + "kinto-storage-adapter.sys.mjs", + "logmanager.sys.mjs", + "observers.sys.mjs", + "rest.sys.mjs", + "uptake-telemetry.sys.mjs", + "utils.sys.mjs", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": + EXTRA_JS_MODULES["services-common"] += [ + "hawkclient.sys.mjs", + "hawkrequest.sys.mjs", + "tokenserverclient.sys.mjs", + ] + +TESTING_JS_MODULES.services.common += [ + "modules-testing/logging.sys.mjs", +] + +SPHINX_TREES["/services/common"] = "docs" + +XPCOM_MANIFESTS += [ + "app_services_logger/components.conf", +] diff --git a/services/common/observers.sys.mjs b/services/common/observers.sys.mjs new file mode 100644 index 0000000000..c79b2b1bf2 --- /dev/null +++ b/services/common/observers.sys.mjs @@ -0,0 +1,148 @@ +/* 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/. */ + +/** + * A service for adding, removing and notifying observers of notifications. + * Wraps the nsIObserverService interface. + * + * @version 0.2 + */ +export var Observers = { + /** + * Register the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic to observe + * + * @param callback {Object} + * the callback; an Object that implements nsIObserver or a Function + * that gets called when the notification occurs + * + * @param thisObject {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ + add(topic, callback, thisObject) { + let observer = new Observer(topic, callback, thisObject); + this._cache.push(observer); + Services.obs.addObserver(observer, topic, true); + + return observer; + }, + + /** + * Unregister the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic being observed + * + * @param callback {Object} + * the callback doing the observing + * + * @param thisObject {Object} [optional] + * the object being used as |this| when calling a Function callback + */ + remove(topic, callback, thisObject) { + // This seems fairly inefficient, but I'm not sure how much better + // we can make it. We could index by topic, but we can't index by callback + // or thisObject, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + let [observer] = this._cache.filter( + v => + v.topic == topic && v.callback == callback && v.thisObject == thisObject + ); + if (observer) { + Services.obs.removeObserver(observer, topic); + this._cache.splice(this._cache.indexOf(observer), 1); + } else { + throw new Error("Attempt to remove non-existing observer"); + } + }, + + /** + * Notify observers about something. + * + * @param topic {String} + * the topic to notify observers about + * + * @param subject {Object} [optional] + * some information about the topic; can be any JS object or primitive + * + * @param data {String} [optional] [deprecated] + * some more information about the topic; deprecated as the subject + * is sufficient to pass all needed information to the JS observers + * that this module targets; if you have multiple values to pass to + * the observer, wrap them in an object and pass them via the subject + * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) + */ + notify(topic, subject, data) { + subject = typeof subject == "undefined" ? null : new Subject(subject); + data = typeof data == "undefined" ? null : data; + Services.obs.notifyObservers(subject, topic, data); + }, + + /** + * A cache of observers that have been added. + * + * We use this to remove observers when a caller calls |remove|. + * + * XXX This might result in reference cycles, causing memory leaks, + * if we hold a reference to an observer that holds a reference to us. + * Could we fix that by making this an independent top-level object + * rather than a property of this object? + */ + _cache: [], +}; + +function Observer(topic, callback, thisObject) { + this.topic = topic; + this.callback = callback; + this.thisObject = thisObject; +} + +Observer.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + observe(subject, topic, data) { + // Extract the wrapped object for subjects that are one of our wrappers + // around a JS object. This way we support both wrapped subjects created + // using this module and those that are real XPCOM components. + if ( + subject && + typeof subject == "object" && + "wrappedJSObject" in subject && + "observersModuleSubjectWrapper" in subject.wrappedJSObject + ) { + subject = subject.wrappedJSObject.object; + } + + if (typeof this.callback == "function") { + if (this.thisObject) { + this.callback.call(this.thisObject, subject, data); + } else { + this.callback(subject, data); + } + } else { + // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); + } + }, +}; + +function Subject(object) { + // Double-wrap the object and set a property identifying the wrappedJSObject + // as one of our wrappers to distinguish between subjects that are one of our + // wrappers (which we should unwrap when notifying our observers) and those + // that are real JS XPCOM components (which we should pass through unaltered). + this.wrappedJSObject = { observersModuleSubjectWrapper: true, object }; +} + +Subject.prototype = { + QueryInterface: ChromeUtils.generateQI([]), + getScriptableHelper() {}, + getInterfaces() {}, +}; diff --git a/services/common/rest.sys.mjs b/services/common/rest.sys.mjs new file mode 100644 index 0000000000..ed32047bcd --- /dev/null +++ b/services/common/rest.sys.mjs @@ -0,0 +1,720 @@ +/* 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 { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs"; + +import { Log } from "resource://gre/modules/Log.sys.mjs"; + +import { CommonUtils } from "resource://services-common/utils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CryptoUtils: "resource://services-crypto/utils.sys.mjs", +}); + +function decodeString(data, charset) { + if (!data || !charset) { + return data; + } + + // This could be simpler if we assumed the charset is only ever UTF-8. + // It's unclear to me how willing we are to assume this, though... + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stringStream.setData(data, data.length); + + let converterStream = Cc[ + "@mozilla.org/intl/converter-input-stream;1" + ].createInstance(Ci.nsIConverterInputStream); + + converterStream.init( + stringStream, + charset, + 0, + converterStream.DEFAULT_REPLACEMENT_CHARACTER + ); + + let remaining = data.length; + let body = ""; + while (remaining > 0) { + let str = {}; + let num = converterStream.readString(remaining, str); + if (!num) { + break; + } + remaining -= num; + body += str.value; + } + return body; +} + +/** + * Single use HTTP requests to RESTish resources. + * + * @param uri + * URI for the request. This can be an nsIURI object or a string + * that can be used to create one. An exception will be thrown if + * the string is not a valid URI. + * + * Examples: + * + * (1) Quick GET request: + * + * let response = await new RESTRequest("http://server/rest/resource").get(); + * if (!response.success) { + * // Bail out if we're not getting an HTTP 2xx code. + * processHTTPError(response.status); + * return; + * } + * processData(response.body); + * + * (2) Quick PUT request (non-string data is automatically JSONified) + * + * let response = await new RESTRequest("http://server/rest/resource").put(data); + */ +export function RESTRequest(uri) { + this.status = this.NOT_SENT; + + // If we don't have an nsIURI object yet, make one. This will throw if + // 'uri' isn't a valid URI string. + if (!(uri instanceof Ci.nsIURI)) { + uri = Services.io.newURI(uri); + } + this.uri = uri; + + this._headers = {}; + this._deferred = Promise.withResolvers(); + this._log = Log.repository.getLogger(this._logName); + this._log.manageLevelFromPref("services.common.log.logger.rest.request"); +} + +RESTRequest.prototype = { + _logName: "Services.Common.RESTRequest", + + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIChannelEventSink", + ]), + + /** Public API: **/ + + /** + * URI for the request (an nsIURI object). + */ + uri: null, + + /** + * HTTP method (e.g. "GET") + */ + method: null, + + /** + * RESTResponse object + */ + response: null, + + /** + * nsIRequest load flags. Don't do any caching by default. Don't send user + * cookies and such over the wire (Bug 644734). + */ + loadFlags: + Ci.nsIRequest.LOAD_BYPASS_CACHE | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_ANONYMOUS, + + /** + * nsIHttpChannel + */ + channel: null, + + /** + * Flag to indicate the status of the request. + * + * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED. + */ + status: null, + + NOT_SENT: 0, + SENT: 1, + IN_PROGRESS: 2, + COMPLETED: 4, + ABORTED: 8, + + /** + * HTTP status text of response + */ + statusText: null, + + /** + * Request timeout (in seconds, though decimal values can be used for + * up to millisecond granularity.) + * + * 0 for no timeout. Default is 300 seconds (5 minutes), the same as Sync uses + * in resource.js. + */ + timeout: 300, + + /** + * The encoding with which the response to this request must be treated. + * If a charset parameter is available in the HTTP Content-Type header for + * this response, that will always be used, and this value is ignored. We + * default to UTF-8 because that is a reasonable default. + */ + charset: "utf-8", + + /** + * Set a request header. + */ + setHeader(name, value) { + this._headers[name.toLowerCase()] = value; + }, + + /** + * Perform an HTTP GET. + * + * @return Promise + */ + async get() { + return this.dispatch("GET", null); + }, + + /** + * Perform an HTTP PATCH. + * + * @param data + * Data to be used as the request body. If this isn't a string + * it will be JSONified automatically. + * + * @return Promise + */ + async patch(data) { + return this.dispatch("PATCH", data); + }, + + /** + * Perform an HTTP PUT. + * + * @param data + * Data to be used as the request body. If this isn't a string + * it will be JSONified automatically. + * + * @return Promise + */ + async put(data) { + return this.dispatch("PUT", data); + }, + + /** + * Perform an HTTP POST. + * + * @param data + * Data to be used as the request body. If this isn't a string + * it will be JSONified automatically. + * + * @return Promise + */ + async post(data) { + return this.dispatch("POST", data); + }, + + /** + * Perform an HTTP DELETE. + * + * @return Promise + */ + async delete() { + return this.dispatch("DELETE", null); + }, + + /** + * Abort an active request. + */ + abort(rejectWithError = null) { + if (this.status != this.SENT && this.status != this.IN_PROGRESS) { + throw new Error("Can only abort a request that has been sent."); + } + + this.status = this.ABORTED; + this.channel.cancel(Cr.NS_BINDING_ABORTED); + + if (this.timeoutTimer) { + // Clear the abort timer now that the channel is done. + this.timeoutTimer.clear(); + } + if (rejectWithError) { + this._deferred.reject(rejectWithError); + } + }, + + /** Implementation stuff **/ + + async dispatch(method, data) { + if (this.status != this.NOT_SENT) { + throw new Error("Request has already been sent!"); + } + + this.method = method; + + // Create and initialize HTTP channel. + let channel = NetUtil.newChannel({ + uri: this.uri, + loadUsingSystemPrincipal: true, + }) + .QueryInterface(Ci.nsIRequest) + .QueryInterface(Ci.nsIHttpChannel); + this.channel = channel; + channel.loadFlags |= this.loadFlags; + channel.notificationCallbacks = this; + + this._log.debug(`${method} request to ${this.uri.spec}`); + // Set request headers. + let headers = this._headers; + for (let key in headers) { + if (key == "authorization" || key == "x-client-state") { + this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); + } else { + this._log.trace("HTTP Header " + key + ": " + headers[key]); + } + channel.setRequestHeader(key, headers[key], false); + } + + // REST requests accept JSON by default + if (!headers.accept) { + channel.setRequestHeader( + "accept", + "application/json;q=0.9,*/*;q=0.2", + false + ); + } + + // Set HTTP request body. + if (method == "PUT" || method == "POST" || method == "PATCH") { + // Convert non-string bodies into JSON with utf-8 encoding. If a string + // is passed we assume they've already encoded it. + let contentType = headers["content-type"]; + if (typeof data != "string") { + data = JSON.stringify(data); + if (!contentType) { + contentType = "application/json"; + } + if (!contentType.includes("charset")) { + data = CommonUtils.encodeUTF8(data); + contentType += "; charset=utf-8"; + } else { + // If someone handed us an object but also a custom content-type + // it's probably confused. We could go to even further lengths to + // respect it, but this shouldn't happen in practice. + console.error( + "rest.js found an object to JSON.stringify but also a " + + "content-type header with a charset specification. " + + "This probably isn't going to do what you expect" + ); + } + } + if (!contentType) { + contentType = "text/plain"; + } + + this._log.debug(method + " Length: " + data.length); + if (this._log.level <= Log.Level.Trace) { + this._log.trace(method + " Body: " + data); + } + + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(data, data.length); + + channel.QueryInterface(Ci.nsIUploadChannel); + channel.setUploadStream(stream, contentType, data.length); + } + // We must set this after setting the upload stream, otherwise it + // will always be 'PUT'. Yeah, I know. + channel.requestMethod = method; + + // Before opening the channel, set the charset that serves as a hint + // as to what the response might be encoded as. + channel.contentCharset = this.charset; + + // Blast off! + try { + channel.asyncOpen(this); + } catch (ex) { + // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port. + this._log.warn("Caught an error in asyncOpen", ex); + this._deferred.reject(ex); + } + this.status = this.SENT; + this.delayTimeout(); + return this._deferred.promise; + }, + + /** + * Create or push back the abort timer that kills this request. + */ + delayTimeout() { + if (this.timeout) { + CommonUtils.namedTimer( + this.abortTimeout, + this.timeout * 1000, + this, + "timeoutTimer" + ); + } + }, + + /** + * Abort the request based on a timeout. + */ + abortTimeout() { + this.abort( + Components.Exception( + "Aborting due to channel inactivity.", + Cr.NS_ERROR_NET_TIMEOUT + ) + ); + }, + + /** nsIStreamListener **/ + + onStartRequest(channel) { + if (this.status == this.ABORTED) { + this._log.trace( + "Not proceeding with onStartRequest, request was aborted." + ); + // We might have already rejected, but just in case. + this._deferred.reject( + Components.Exception("Request aborted", Cr.NS_BINDING_ABORTED) + ); + return; + } + + try { + channel.QueryInterface(Ci.nsIHttpChannel); + } catch (ex) { + this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); + this.status = this.ABORTED; + channel.cancel(Cr.NS_BINDING_ABORTED); + this._deferred.reject(ex); + return; + } + + this.status = this.IN_PROGRESS; + + this._log.trace( + "onStartRequest: " + channel.requestMethod + " " + channel.URI.spec + ); + + // Create a new response object. + this.response = new RESTResponse(this); + + this.delayTimeout(); + }, + + onStopRequest(channel, statusCode) { + if (this.timeoutTimer) { + // Clear the abort timer now that the channel is done. + this.timeoutTimer.clear(); + } + + // We don't want to do anything for a request that's already been aborted. + if (this.status == this.ABORTED) { + this._log.trace( + "Not proceeding with onStopRequest, request was aborted." + ); + // We might not have already rejected if the user called reject() manually. + // If we have already rejected, then this is a no-op + this._deferred.reject( + Components.Exception("Request aborted", Cr.NS_BINDING_ABORTED) + ); + return; + } + + try { + channel.QueryInterface(Ci.nsIHttpChannel); + } catch (ex) { + this._log.error("Unexpected error: channel not nsIHttpChannel!"); + this.status = this.ABORTED; + this._deferred.reject(ex); + return; + } + + this.status = this.COMPLETED; + + try { + this.response.body = decodeString( + this.response._rawBody, + this.response.charset + ); + this.response._rawBody = null; + } catch (ex) { + this._log.warn( + `Exception decoding response - ${this.method} ${channel.URI.spec}`, + ex + ); + this._deferred.reject(ex); + return; + } + + let statusSuccess = Components.isSuccessCode(statusCode); + let uri = (channel && channel.URI && channel.URI.spec) || ""; + this._log.trace( + "Channel for " + + channel.requestMethod + + " " + + uri + + " returned status code " + + statusCode + ); + + // Throw the failure code and stop execution. Use Components.Exception() + // instead of Error() so the exception is QI-able and can be passed across + // XPCOM borders while preserving the status code. + if (!statusSuccess) { + let message = Components.Exception("", statusCode).name; + let error = Components.Exception(message, statusCode); + this._log.debug( + this.method + " " + uri + " failed: " + statusCode + " - " + message + ); + // Additionally give the full response body when Trace logging. + if (this._log.level <= Log.Level.Trace) { + this._log.trace(this.method + " body", this.response.body); + } + this._deferred.reject(error); + return; + } + + this._log.debug(this.method + " " + uri + " " + this.response.status); + + // Note that for privacy/security reasons we don't log this response body + + delete this._inputStream; + + this._deferred.resolve(this.response); + }, + + onDataAvailable(channel, stream, off, count) { + // We get an nsIRequest, which doesn't have contentCharset. + try { + channel.QueryInterface(Ci.nsIHttpChannel); + } catch (ex) { + this._log.error("Unexpected error: channel not nsIHttpChannel!"); + this.abort(ex); + return; + } + + if (channel.contentCharset) { + this.response.charset = channel.contentCharset; + } else { + this.response.charset = null; + } + + if (!this._inputStream) { + this._inputStream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].createInstance(Ci.nsIScriptableInputStream); + } + this._inputStream.init(stream); + + this.response._rawBody += this._inputStream.read(count); + + this.delayTimeout(); + }, + + /** nsIInterfaceRequestor **/ + + getInterface(aIID) { + return this.QueryInterface(aIID); + }, + + /** + * Returns true if headers from the old channel should be + * copied to the new channel. Invoked when a channel redirect + * is in progress. + */ + shouldCopyOnRedirect(oldChannel, newChannel, flags) { + let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL); + let isSameURI = newChannel.URI.equals(oldChannel.URI); + this._log.debug( + "Channel redirect: " + + oldChannel.URI.spec + + ", " + + newChannel.URI.spec + + ", internal = " + + isInternal + ); + return isInternal && isSameURI; + }, + + /** nsIChannelEventSink **/ + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + let oldSpec = + oldChannel && oldChannel.URI ? oldChannel.URI.spec : ""; + let newSpec = + newChannel && newChannel.URI ? newChannel.URI.spec : ""; + this._log.debug( + "Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags + ); + + try { + newChannel.QueryInterface(Ci.nsIHttpChannel); + } catch (ex) { + this._log.error("Unexpected error: channel not nsIHttpChannel!"); + callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); + return; + } + + // For internal redirects, copy the headers that our caller set. + try { + if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) { + this._log.trace("Copying headers for safe internal redirect."); + for (let key in this._headers) { + newChannel.setRequestHeader(key, this._headers[key], false); + } + } + } catch (ex) { + this._log.error("Error copying headers", ex); + } + + this.channel = newChannel; + + // We let all redirects proceed. + callback.onRedirectVerifyCallback(Cr.NS_OK); + }, +}; + +/** + * Response object for a RESTRequest. This will be created automatically by + * the RESTRequest. + */ +export function RESTResponse(request = null) { + this.body = ""; + this._rawBody = ""; + this.request = request; + this._log = Log.repository.getLogger(this._logName); + this._log.manageLevelFromPref("services.common.log.logger.rest.response"); +} + +RESTResponse.prototype = { + _logName: "Services.Common.RESTResponse", + + /** + * Corresponding REST request + */ + request: null, + + /** + * HTTP status code + */ + get status() { + let status; + try { + status = this.request.channel.responseStatus; + } catch (ex) { + this._log.debug("Caught exception fetching HTTP status code", ex); + return null; + } + Object.defineProperty(this, "status", { value: status }); + return status; + }, + + /** + * HTTP status text + */ + get statusText() { + let statusText; + try { + statusText = this.request.channel.responseStatusText; + } catch (ex) { + this._log.debug("Caught exception fetching HTTP status text", ex); + return null; + } + Object.defineProperty(this, "statusText", { value: statusText }); + return statusText; + }, + + /** + * Boolean flag that indicates whether the HTTP status code is 2xx or not. + */ + get success() { + let success; + try { + success = this.request.channel.requestSucceeded; + } catch (ex) { + this._log.debug("Caught exception fetching HTTP success flag", ex); + return null; + } + Object.defineProperty(this, "success", { value: success }); + return success; + }, + + /** + * Object containing HTTP headers (keyed as lower case) + */ + get headers() { + let headers = {}; + try { + this._log.trace("Processing response headers."); + let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel); + channel.visitResponseHeaders(function (header, value) { + headers[header.toLowerCase()] = value; + }); + } catch (ex) { + this._log.debug("Caught exception processing response headers", ex); + return null; + } + + Object.defineProperty(this, "headers", { value: headers }); + return headers; + }, + + /** + * HTTP body (string) + */ + body: null, +}; + +/** + * Single use MAC authenticated HTTP requests to RESTish resources. + * + * @param uri + * URI going to the RESTRequest constructor. + * @param authToken + * (Object) An auth token of the form {id: (string), key: (string)} + * from which the MAC Authentication header for this request will be + * derived. A token as obtained from + * TokenServerClient.getTokenUsingOAuth is accepted. + * @param extra + * (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts, + * nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on + * the purpose of these values. + */ +export function TokenAuthenticatedRESTRequest(uri, authToken, extra) { + RESTRequest.call(this, uri); + this.authToken = authToken; + this.extra = extra || {}; +} + +TokenAuthenticatedRESTRequest.prototype = { + async dispatch(method, data) { + let sig = await lazy.CryptoUtils.computeHTTPMACSHA1( + this.authToken.id, + this.authToken.key, + method, + this.uri, + this.extra + ); + + this.setHeader("Authorization", sig.getHeader()); + + return super.dispatch(method, data); + }, +}; + +Object.setPrototypeOf( + TokenAuthenticatedRESTRequest.prototype, + RESTRequest.prototype +); diff --git a/services/common/servicesComponents.manifest b/services/common/servicesComponents.manifest new file mode 100644 index 0000000000..fe2a52fab6 --- /dev/null +++ b/services/common/servicesComponents.manifest @@ -0,0 +1,2 @@ +# Register resource aliases +resource services-common resource://gre/modules/services-common/ diff --git a/services/common/tests/moz.build b/services/common/tests/moz.build new file mode 100644 index 0000000000..77d3622318 --- /dev/null +++ b/services/common/tests/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"] + +TEST_DIRS += ["unit"] diff --git a/services/common/tests/unit/head_global.js b/services/common/tests/unit/head_global.js new file mode 100644 index 0000000000..bcb1c063a4 --- /dev/null +++ b/services/common/tests/unit/head_global.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var Cm = Components.manager; + +// Required to avoid failures. +do_get_profile(); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo({ + name: "XPCShell", + ID: "xpcshell@tests.mozilla.org", + version: "1", + platformVersion: "", +}); + +function addResourceAlias() { + const handler = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + let modules = ["common", "crypto", "settings"]; + for (let module of modules) { + let uri = Services.io.newURI( + "resource://gre/modules/services-" + module + "/" + ); + handler.setSubstitution("services-" + module, uri); + } +} +addResourceAlias(); diff --git a/services/common/tests/unit/head_helpers.js b/services/common/tests/unit/head_helpers.js new file mode 100644 index 0000000000..bd994ee71a --- /dev/null +++ b/services/common/tests/unit/head_helpers.js @@ -0,0 +1,270 @@ +/* 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-globals-from head_global.js */ + +var { Log } = ChromeUtils.importESModule("resource://gre/modules/Log.sys.mjs"); +var { CommonUtils } = ChromeUtils.importESModule( + "resource://services-common/utils.sys.mjs" +); +var { + HTTP_400, + HTTP_401, + HTTP_402, + HTTP_403, + HTTP_404, + HTTP_405, + HTTP_406, + HTTP_407, + HTTP_408, + HTTP_409, + HTTP_410, + HTTP_411, + HTTP_412, + HTTP_413, + HTTP_414, + HTTP_415, + HTTP_417, + HTTP_500, + HTTP_501, + HTTP_502, + HTTP_503, + HTTP_504, + HTTP_505, + HttpError, + HttpServer, +} = ChromeUtils.importESModule("resource://testing-common/httpd.sys.mjs"); +var { getTestLogger, initTestLogging } = ChromeUtils.importESModule( + "resource://testing-common/services/common/logging.sys.mjs" +); +var { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); +var { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function do_check_empty(obj) { + do_check_attribute_count(obj, 0); +} + +function do_check_attribute_count(obj, c) { + Assert.equal(c, Object.keys(obj).length); +} + +function do_check_throws(aFunc, aResult) { + try { + aFunc(); + } catch (e) { + Assert.equal(e.result, aResult); + return; + } + do_throw("Expected result " + aResult + ", none thrown."); +} + +/** + * Test whether specified function throws exception with expected + * result. + * + * @param func + * Function to be tested. + * @param message + * Message of expected exception. null for no throws. + */ +function do_check_throws_message(aFunc, aResult) { + try { + aFunc(); + } catch (e) { + Assert.equal(e.message, aResult); + return; + } + do_throw("Expected an error, none thrown."); +} + +/** + * 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 (some, debug, text, to) { + print(Array.from(arguments).join(" ")); +}; + +function httpd_setup(handlers, port = -1) { + let server = new HttpServer(); + for (let path in handlers) { + server.registerPathHandler(path, handlers[path]); + } + try { + server.start(port); + } catch (ex) { + _("=========================================="); + _("Got exception starting HTTP server on port " + port); + _("Error: " + Log.exceptionStr(ex)); + _("Is there a process already listening on port " + port + "?"); + _("=========================================="); + do_throw(ex); + } + + // Set the base URI for convenience. + let i = server.identity; + server.baseURI = + i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort; + + return server; +} + +function httpd_handler(statusCode, status, body) { + return function handler(request, response) { + _("Processing request"); + // Allow test functions to inspect the request. + request.body = readBytesFromInputStream(request.bodyInputStream); + handler.request = request; + + response.setStatusLine(request.httpVersion, statusCode, status); + if (body) { + response.bodyOutputStream.write(body, body.length); + } + }; +} + +function promiseStopServer(server) { + return new Promise(resolve => server.stop(resolve)); +} + +/* + * Read bytes string from an nsIInputStream. If 'count' is omitted, + * all available input is read. + */ +function readBytesFromInputStream(inputStream, count) { + if (!count) { + count = inputStream.available(); + } + if (!count) { + return ""; + } + return NetUtil.readInputStreamToString(inputStream, count, { + charset: "UTF-8", + }); +} + +function writeBytesToOutputStream(outputStream, string) { + if (!string) { + return; + } + let converter = Cc[ + "@mozilla.org/intl/converter-output-stream;1" + ].createInstance(Ci.nsIConverterOutputStream); + converter.init(outputStream, "UTF-8"); + converter.writeString(string); + converter.close(); +} + +/* + * Ensure exceptions from inside callbacks leads to test failures. + */ +function ensureThrows(func) { + return function () { + try { + func.apply(this, arguments); + } catch (ex) { + do_throw(ex); + } + }; +} + +/** + * Proxy auth helpers. + */ + +/** + * Fake a PAC to prompt a channel replacement. + */ +var PACSystemSettings = { + QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), + + // Replace this URI for each test to avoid caching. We want to ensure that + // each test gets a completely fresh setup. + mainThreadOnly: true, + PACURI: null, + getProxyForURI: function getProxyForURI(aURI) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +var fakePACCID; +function installFakePAC() { + _("Installing fake PAC."); + fakePACCID = MockRegistrar.register( + "@mozilla.org/system-proxy-settings;1", + PACSystemSettings + ); +} + +function uninstallFakePAC() { + _("Uninstalling fake PAC."); + MockRegistrar.unregister(fakePACCID); +} + +function getUptakeTelemetrySnapshot(component, source) { + const TELEMETRY_CATEGORY_ID = "uptake.remotecontent.result"; + const snapshot = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_ALL_CHANNELS, + true + ); + const parentEvents = snapshot.parent || []; + return ( + parentEvents + // Transform raw event data to objects. + .map(([i, category, method, object, value, extras]) => { + return { category, method, object, value, extras }; + }) + // Keep only for the specified component and source. + .filter( + e => + e.category == TELEMETRY_CATEGORY_ID && + e.object == component && + e.extras.source == source + ) + // Return total number of events received by status, to mimic histograms snapshots. + .reduce((acc, e) => { + acc[e.value] = (acc[e.value] || 0) + 1; + return acc; + }, {}) + ); +} + +function checkUptakeTelemetry(snapshot1, snapshot2, expectedIncrements) { + const { UptakeTelemetry } = ChromeUtils.importESModule( + "resource://services-common/uptake-telemetry.sys.mjs" + ); + const STATUSES = Object.values(UptakeTelemetry.STATUS); + for (const status of STATUSES) { + const expected = expectedIncrements[status] || 0; + const previous = snapshot1[status] || 0; + const current = snapshot2[status] || previous; + Assert.equal(expected, current - previous, `check events for ${status}`); + } +} + +async function withFakeChannel(channel, f) { + const { Policy } = ChromeUtils.importESModule( + "resource://services-common/uptake-telemetry.sys.mjs" + ); + let oldGetChannel = Policy.getChannel; + Policy.getChannel = () => channel; + try { + return await f(); + } finally { + Policy.getChannel = oldGetChannel; + } +} + +function arrayEqual(a, b) { + return JSON.stringify(a) == JSON.stringify(b); +} diff --git a/services/common/tests/unit/head_http.js b/services/common/tests/unit/head_http.js new file mode 100644 index 0000000000..6c2a0a3f0f --- /dev/null +++ b/services/common/tests/unit/head_http.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from head_global.js */ + +var { CommonUtils } = ChromeUtils.importESModule( + "resource://services-common/utils.sys.mjs" +); + +function basic_auth_header(user, password) { + return "Basic " + btoa(user + ":" + CommonUtils.encodeUTF8(password)); +} + +function basic_auth_matches(req, user, password) { + if (!req.hasHeader("Authorization")) { + return false; + } + + let expected = basic_auth_header(user, CommonUtils.encodeUTF8(password)); + return req.getHeader("Authorization") == expected; +} + +function httpd_basic_auth_handler(body, metadata, response) { + if (basic_auth_matches(metadata, "guest", "guest")) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } else { + body = "This path exists and is protected - failed"; + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } + response.bodyOutputStream.write(body, body.length); +} diff --git a/services/common/tests/unit/moz.build b/services/common/tests/unit/moz.build new file mode 100644 index 0000000000..568f361a54 --- /dev/null +++ b/services/common/tests/unit/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. diff --git a/services/common/tests/unit/test_async_chain.js b/services/common/tests/unit/test_async_chain.js new file mode 100644 index 0000000000..3277c92f81 --- /dev/null +++ b/services/common/tests/unit/test_async_chain.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Async } = ChromeUtils.importESModule( + "resource://services-common/async.sys.mjs" +); + +function run_test() { + _("Chain a few async methods, making sure the 'this' object is correct."); + + let methods = { + save(x, callback) { + this.x = x; + callback(x); + }, + addX(x, callback) { + callback(x + this.x); + }, + double(x, callback) { + callback(x * 2); + }, + neg(x, callback) { + callback(-x); + }, + }; + methods.chain = Async.chain; + + // ((1 + 1 + 1) * (-1) + 1) * 2 + 1 = -3 + methods.chain( + methods.save, + methods.addX, + methods.addX, + methods.neg, + methods.addX, + methods.double, + methods.addX, + methods.save + )(1); + Assert.equal(methods.x, -3); +} diff --git a/services/common/tests/unit/test_async_foreach.js b/services/common/tests/unit/test_async_foreach.js new file mode 100644 index 0000000000..14a23e7ff7 --- /dev/null +++ b/services/common/tests/unit/test_async_foreach.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Async } = ChromeUtils.importESModule( + "resource://services-common/async.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +function makeArray(length) { + // Start at 1 so that we can just divide by yieldEvery to get the expected + // call count. (we exp) + return Array.from({ length }, (v, i) => i + 1); +} + +// Adjust if we ever change the default. +const DEFAULT_YIELD_EVERY = 50; + +add_task(async function testYields() { + let spy = sinon.spy(Async, "promiseYield"); + try { + await Async.yieldingForEach(makeArray(DEFAULT_YIELD_EVERY * 2), element => { + // The yield will happen *after* this function is ran. + Assert.equal( + spy.callCount, + Math.floor((element - 1) / DEFAULT_YIELD_EVERY) + ); + }); + } finally { + spy.restore(); + } +}); + +add_task(async function testExistingYieldState() { + const yieldState = Async.yieldState(DEFAULT_YIELD_EVERY); + + for (let i = 0; i < 15; i++) { + Assert.equal(yieldState.shouldYield(), false); + } + + let spy = sinon.spy(Async, "promiseYield"); + + try { + await Async.yieldingForEach( + makeArray(DEFAULT_YIELD_EVERY * 2), + element => { + Assert.equal( + spy.callCount, + Math.floor((element + 15 - 1) / DEFAULT_YIELD_EVERY) + ); + }, + yieldState + ); + } finally { + spy.restore(); + } +}); + +add_task(async function testEarlyReturn() { + let lastElement = 0; + await Async.yieldingForEach(makeArray(DEFAULT_YIELD_EVERY), element => { + lastElement = element; + return element === 10; + }); + + Assert.equal(lastElement, 10); +}); + +add_task(async function testEaryReturnAsync() { + let lastElement = 0; + await Async.yieldingForEach(makeArray(DEFAULT_YIELD_EVERY), async element => { + lastElement = element; + return element === 10; + }); + + Assert.equal(lastElement, 10); +}); + +add_task(async function testEarlyReturnPromise() { + let lastElement = 0; + await Async.yieldingForEach(makeArray(DEFAULT_YIELD_EVERY), element => { + lastElement = element; + return new Promise(resolve => resolve(element === 10)); + }); + + Assert.equal(lastElement, 10); +}); diff --git a/services/common/tests/unit/test_hawkclient.js b/services/common/tests/unit/test_hawkclient.js new file mode 100644 index 0000000000..33d125f42e --- /dev/null +++ b/services/common/tests/unit/test_hawkclient.js @@ -0,0 +1,515 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HawkClient } = ChromeUtils.importESModule( + "resource://services-common/hawkclient.sys.mjs" +); + +const SECOND_MS = 1000; +const MINUTE_MS = SECOND_MS * 60; +const HOUR_MS = MINUTE_MS * 60; + +const TEST_CREDS = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", +}; + +initTestLogging("Trace"); + +add_task(function test_now() { + let client = new HawkClient("https://example.com"); + + Assert.ok(client.now() - Date.now() < SECOND_MS); +}); + +add_task(function test_updateClockOffset() { + let client = new HawkClient("https://example.com"); + + let now = new Date(); + let serverDate = now.toUTCString(); + + // Client's clock is off + client.now = () => { + return now.valueOf() + HOUR_MS; + }; + + client._updateClockOffset(serverDate); + + // Check that they're close; there will likely be a one-second rounding + // error, so checking strict equality will likely fail. + // + // localtimeOffsetMsec is how many milliseconds to add to the local clock so + // that it agrees with the server. We are one hour ahead of the server, so + // our offset should be -1 hour. + Assert.ok(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS); +}); + +add_task(async function test_authenticated_get_request() { + let message = '{"msg": "Great Success!"}'; + let method = "GET"; + + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(request.hasHeader("Authorization")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + let response = await client.request("/foo", method, TEST_CREDS); + let result = JSON.parse(response.body); + + Assert.equal("Great Success!", result.msg); + + await promiseStopServer(server); +}); + +async function check_authenticated_request(method) { + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(request.hasHeader("Authorization")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + response.bodyOutputStream.writeFrom( + request.bodyInputStream, + request.bodyInputStream.available() + ); + }, + }); + + let client = new HawkClient(server.baseURI); + + let response = await client.request("/foo", method, TEST_CREDS, { + foo: "bar", + }); + let result = JSON.parse(response.body); + + Assert.equal("bar", result.foo); + + await promiseStopServer(server); +} + +add_task(async function test_authenticated_post_request() { + await check_authenticated_request("POST"); +}); + +add_task(async function test_authenticated_put_request() { + await check_authenticated_request("PUT"); +}); + +add_task(async function test_authenticated_patch_request() { + await check_authenticated_request("PATCH"); +}); + +add_task(async function test_extra_headers() { + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(request.hasHeader("Authorization")); + Assert.ok(request.hasHeader("myHeader")); + Assert.equal(request.getHeader("myHeader"), "fake"); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + response.bodyOutputStream.writeFrom( + request.bodyInputStream, + request.bodyInputStream.available() + ); + }, + }); + + let client = new HawkClient(server.baseURI); + + let response = await client.request( + "/foo", + "POST", + TEST_CREDS, + { foo: "bar" }, + { myHeader: "fake" } + ); + let result = JSON.parse(response.body); + + Assert.equal("bar", result.foo); + + await promiseStopServer(server); +}); + +add_task(async function test_credentials_optional() { + let method = "GET"; + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(!request.hasHeader("Authorization")); + + let message = JSON.stringify({ msg: "you're in the friend zone" }); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + let result = await client.request("/foo", method); // credentials undefined + Assert.equal(JSON.parse(result.body).msg, "you're in the friend zone"); + + await promiseStopServer(server); +}); + +add_task(async function test_server_error() { + let message = "Ohai!"; + let method = "GET"; + + let server = httpd_setup({ + "/foo": (request, response) => { + response.setStatusLine(request.httpVersion, 418, "I am a Teapot"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + try { + await client.request("/foo", method, TEST_CREDS); + do_throw("Expected an error"); + } catch (err) { + Assert.equal(418, err.code); + Assert.equal("I am a Teapot", err.message); + } + + await promiseStopServer(server); +}); + +add_task(async function test_server_error_json() { + let message = JSON.stringify({ error: "Cannot get ye flask." }); + let method = "GET"; + + let server = httpd_setup({ + "/foo": (request, response) => { + response.setStatusLine( + request.httpVersion, + 400, + "What wouldst thou deau?" + ); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + try { + await client.request("/foo", method, TEST_CREDS); + do_throw("Expected an error"); + } catch (err) { + Assert.equal("Cannot get ye flask.", err.error); + } + + await promiseStopServer(server); +}); + +add_task(async function test_offset_after_request() { + let message = "Ohai!"; + let method = "GET"; + + let server = httpd_setup({ + "/foo": (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + let now = Date.now(); + client.now = () => { + return now + HOUR_MS; + }; + + Assert.equal(client.localtimeOffsetMsec, 0); + + await client.request("/foo", method, TEST_CREDS); + // Should be about an hour off + Assert.ok(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS); + + await promiseStopServer(server); +}); + +add_task(async function test_offset_in_hawk_header() { + let message = "Ohai!"; + let method = "GET"; + + let server = httpd_setup({ + "/first": function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + + "/second": function (request, response) { + // We see a better date now in the ts component of the header + let delta = getTimestampDelta(request.getHeader("Authorization")); + + // We're now within HAWK's one-minute window. + // I hope this isn't a recipe for intermittent oranges ... + if (delta < MINUTE_MS) { + response.setStatusLine(request.httpVersion, 200, "OK"); + } else { + response.setStatusLine(request.httpVersion, 400, "Delta: " + delta); + } + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + client.now = () => { + return Date.now() + 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + await client.request("/first", method, TEST_CREDS); + + // After the first server response, our offset is updated to -12 hours. + // We should be safely in the window, now. + Assert.ok(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS); + await client.request("/second", method, TEST_CREDS); + + await promiseStopServer(server); +}); + +add_task(async function test_2xx_success() { + // Just to ensure that we're not biased toward 200 OK for success + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + let method = "GET"; + + let server = httpd_setup({ + "/foo": (request, response) => { + response.setStatusLine(request.httpVersion, 202, "Accepted"); + }, + }); + + let client = new HawkClient(server.baseURI); + + let response = await client.request("/foo", method, credentials); + + // Shouldn't be any content in a 202 + Assert.equal(response.body, ""); + + await promiseStopServer(server); +}); + +add_task(async function test_retry_request_on_fail() { + let attempts = 0; + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + let method = "GET"; + + let server = httpd_setup({ + "/maybe": function (request, response) { + // This path should be hit exactly twice; once with a bad timestamp, and + // again when the client retries the request with a corrected timestamp. + attempts += 1; + Assert.ok(attempts <= 2); + + let delta = getTimestampDelta(request.getHeader("Authorization")); + + // First time through, we should have a bad timestamp + if (attempts === 1) { + Assert.ok(delta > MINUTE_MS); + let message = "never!!!"; + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.bodyOutputStream.write(message, message.length); + return; + } + + // Second time through, timestamp should be corrected by client + Assert.ok(delta < MINUTE_MS); + let message = "i love you!!!"; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + client.now = () => { + return Date.now() + 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + + // Request will have bad timestamp; client will retry once + let response = await client.request("/maybe", method, credentials); + Assert.equal(response.body, "i love you!!!"); + + await promiseStopServer(server); +}); + +add_task(async function test_multiple_401_retry_once() { + // Like test_retry_request_on_fail, but always return a 401 + // and ensure that the client only retries once. + let attempts = 0; + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + let method = "GET"; + + let server = httpd_setup({ + "/maybe": function (request, response) { + // This path should be hit exactly twice; once with a bad timestamp, and + // again when the client retries the request with a corrected timestamp. + attempts += 1; + + Assert.ok(attempts <= 2); + + let message = "never!!!"; + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + client.now = () => { + return Date.now() - 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + + // Request will have bad timestamp; client will retry once + try { + await client.request("/maybe", method, credentials); + do_throw("Expected an error"); + } catch (err) { + Assert.equal(err.code, 401); + } + Assert.equal(attempts, 2); + + await promiseStopServer(server); +}); + +add_task(async function test_500_no_retry() { + // If we get a 500 error, the client should not retry (as it would with a + // 401) + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + let method = "GET"; + + let server = httpd_setup({ + "/no-shutup": function (request, response) { + let message = "Cannot get ye flask."; + response.setStatusLine(request.httpVersion, 500, "Internal server error"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + // Throw off the clock so the HawkClient would want to retry the request if + // it could + client.now = () => { + return Date.now() - 12 * HOUR_MS; + }; + + // Request will 500; no retries + try { + await client.request("/no-shutup", method, credentials); + do_throw("Expected an error"); + } catch (err) { + Assert.equal(err.code, 500); + } + + await promiseStopServer(server); +}); + +add_task(async function test_401_then_500() { + // Like test_multiple_401_retry_once, but return a 500 to the + // second request, ensuring that the promise is properly rejected + // in client.request. + let attempts = 0; + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + let method = "GET"; + + let server = httpd_setup({ + "/maybe": function (request, response) { + // This path should be hit exactly twice; once with a bad timestamp, and + // again when the client retries the request with a corrected timestamp. + attempts += 1; + Assert.ok(attempts <= 2); + + let delta = getTimestampDelta(request.getHeader("Authorization")); + + // First time through, we should have a bad timestamp + // Client will retry + if (attempts === 1) { + Assert.ok(delta > MINUTE_MS); + let message = "never!!!"; + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.bodyOutputStream.write(message, message.length); + return; + } + + // Second time through, timestamp should be corrected by client + // And fail on the client + Assert.ok(delta < MINUTE_MS); + let message = "Cannot get ye flask."; + response.setStatusLine(request.httpVersion, 500, "Internal server error"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + client.now = () => { + return Date.now() - 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + + // Request will have bad timestamp; client will retry once + try { + await client.request("/maybe", method, credentials); + } catch (err) { + Assert.equal(err.code, 500); + } + Assert.equal(attempts, 2); + + await promiseStopServer(server); +}); + +// End of tests. +// Utility functions follow + +function getTimestampDelta(authHeader, now = Date.now()) { + let tsMS = new Date( + parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS + ); + return Math.abs(tsMS - now); +} + +function run_test() { + initTestLogging("Trace"); + run_next_test(); +} diff --git a/services/common/tests/unit/test_hawkrequest.js b/services/common/tests/unit/test_hawkrequest.js new file mode 100644 index 0000000000..13ab6737de --- /dev/null +++ b/services/common/tests/unit/test_hawkrequest.js @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HAWKAuthenticatedRESTRequest, deriveHawkCredentials } = + ChromeUtils.importESModule("resource://services-common/hawkrequest.sys.mjs"); +const { Async } = ChromeUtils.importESModule( + "resource://services-common/async.sys.mjs" +); + +// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-use-session-certificatesign-etc +var SESSION_KEYS = { + sessionToken: h( + // eslint-disable-next-line no-useless-concat + "a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf" + "b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf" + ), + + tokenID: h( + // eslint-disable-next-line no-useless-concat + "c0a29dcf46174973 da1378696e4c82ae" + "10f723cf4f4d9f75 e39f4ae3851595ab" + ), + + reqHMACkey: h( + // eslint-disable-next-line no-useless-concat + "9d8f22998ee7f579 8b887042466b72d5" + "3e56ab0c094388bf 65831f702d2febc0" + ), +}; + +function do_register_cleanup() { + Services.prefs.clearUserPref("intl.accept_languages"); + Services.prefs.clearUserPref("services.common.log.logger.rest.request"); + + // remove the pref change listener + let hawk = new HAWKAuthenticatedRESTRequest("https://example.com"); + hawk._intl.uninit(); +} + +function run_test() { + registerCleanupFunction(do_register_cleanup); + + Services.prefs.setStringPref( + "services.common.log.logger.rest.request", + "Trace" + ); + initTestLogging("Trace"); + + run_next_test(); +} + +add_test(function test_intl_accept_language() { + let testCount = 0; + let languages = [ + "zu-NP;vo", // Nepalese dialect of Zulu, defaulting to Volapük + "fa-CG;ik", // Congolese dialect of Farsei, defaulting to Inupiaq + ]; + + function setLanguagePref(lang) { + Services.prefs.setStringPref("intl.accept_languages", lang); + } + + let hawk = new HAWKAuthenticatedRESTRequest("https://example.com"); + + Services.prefs.addObserver("intl.accept_languages", checkLanguagePref); + setLanguagePref(languages[testCount]); + + function checkLanguagePref() { + CommonUtils.nextTick(function () { + // Ensure we're only called for the number of entries in languages[]. + Assert.ok(testCount < languages.length); + + Assert.equal(hawk._intl.accept_languages, languages[testCount]); + + testCount++; + if (testCount < languages.length) { + // Set next language in prefs; Pref service will call checkNextLanguage. + setLanguagePref(languages[testCount]); + return; + } + + // We've checked all the entries in languages[]. Cleanup and move on. + info( + "Checked " + + testCount + + " languages. Removing checkLanguagePref as pref observer." + ); + Services.prefs.removeObserver("intl.accept_languages", checkLanguagePref); + run_next_test(); + }); + } +}); + +add_task(async function test_hawk_authenticated_request() { + let postData = { your: "data" }; + + // An arbitrary date - Feb 2, 1971. It ends in a bunch of zeroes to make our + // computation with the hawk timestamp easier, since hawk throws away the + // millisecond values. + let then = 34329600000; + + let clockSkew = 120000; + let timeOffset = -1 * clockSkew; + let localTime = then + clockSkew; + + // Set the accept-languages pref to the Nepalese dialect of Zulu. + let acceptLanguage = "zu-NP"; // omit trailing ';', which our HTTP libs snip + Services.prefs.setStringPref("intl.accept_languages", acceptLanguage); + + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + + let server = httpd_setup({ + "/elysium": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + + // check that the header timestamp is our arbitrary system date, not + // today's date. Note that hawk header timestamps are in seconds, not + // milliseconds. + let authorization = request.getHeader("Authorization"); + let tsMS = parseInt(/ts="(\d+)"/.exec(authorization)[1], 10) * 1000; + Assert.equal(tsMS, then); + + // This testing can be a little wonky. In an environment where + // pref("intl.accept_languages") === 'en-US, en' + // the header is sent as: + // 'en-US,en;q=0.5' + // hence our fake value for acceptLanguage. + let lang = request.getHeader("Accept-Language"); + Assert.equal(lang, acceptLanguage); + + let message = "yay"; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let url = server.baseURI + "/elysium"; + let extra = { + now: localTime, + localtimeOffsetMsec: timeOffset, + }; + + let request = new HAWKAuthenticatedRESTRequest(url, credentials, extra); + + // Allow hawk._intl to respond to the language pref change + await Async.promiseYield(); + + await request.post(postData); + Assert.equal(200, request.response.status); + Assert.equal(request.response.body, "yay"); + + Services.prefs.clearUserPref("intl.accept_languages"); + let pref = Services.prefs.getComplexValue( + "intl.accept_languages", + Ci.nsIPrefLocalizedString + ); + Assert.notEqual(acceptLanguage, pref.data); + + await promiseStopServer(server); +}); + +add_task(async function test_hawk_language_pref_changed() { + let languages = [ + "zu-NP", // Nepalese dialect of Zulu + "fa-CG", // Congolese dialect of Farsi + ]; + + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + + function setLanguage(lang) { + Services.prefs.setStringPref("intl.accept_languages", lang); + } + + let server = httpd_setup({ + "/foo": function (request, response) { + Assert.equal(languages[1], request.getHeader("Accept-Language")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + }, + }); + + let url = server.baseURI + "/foo"; + let request; + + setLanguage(languages[0]); + + // A new request should create the stateful object for tracking the current + // language. + request = new HAWKAuthenticatedRESTRequest(url, credentials); + + // Wait for change to propagate + await Async.promiseYield(); + Assert.equal(languages[0], request._intl.accept_languages); + + // Change the language pref ... + setLanguage(languages[1]); + + await Async.promiseYield(); + + request = new HAWKAuthenticatedRESTRequest(url, credentials); + let response = await request.post({}); + + Assert.equal(200, response.status); + Services.prefs.clearUserPref("intl.accept_languages"); + + await promiseStopServer(server); +}); + +add_task(async function test_deriveHawkCredentials() { + let credentials = await deriveHawkCredentials( + SESSION_KEYS.sessionToken, + "sessionToken" + ); + Assert.equal(credentials.id, SESSION_KEYS.tokenID); + Assert.equal( + CommonUtils.bytesAsHex(credentials.key), + SESSION_KEYS.reqHMACkey + ); +}); + +// turn formatted test vectors into normal hex strings +function h(hexStr) { + return hexStr.replace(/\s+/g, ""); +} diff --git a/services/common/tests/unit/test_kinto.js b/services/common/tests/unit/test_kinto.js new file mode 100644 index 0000000000..4b5e8471b1 --- /dev/null +++ b/services/common/tests/unit/test_kinto.js @@ -0,0 +1,512 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Kinto } = ChromeUtils.import( + "resource://services-common/kinto-offline-client.js" +); +const { FirefoxAdapter } = ChromeUtils.importESModule( + "resource://services-common/kinto-storage-adapter.sys.mjs" +); + +var server; + +// set up what we need to make storage adapters +const kintoFilename = "kinto.sqlite"; + +function do_get_kinto_sqliteHandle() { + return FirefoxAdapter.openConnection({ path: kintoFilename }); +} + +function do_get_kinto_collection(sqliteHandle, collection = "test_collection") { + let config = { + remote: `http://localhost:${server.identity.primaryPort}/v1/`, + headers: { Authorization: "Basic " + btoa("user:pass") }, + adapter: FirefoxAdapter, + adapterOptions: { sqliteHandle }, + }; + return new Kinto(config).collection(collection); +} + +async function clear_collection() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + await collection.clear(); + } finally { + await sqliteHandle.close(); + } +} + +// test some operations on a local collection +add_task(async function test_kinto_add_get() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + + let newRecord = { foo: "bar" }; + // check a record is created + let createResult = await collection.create(newRecord); + Assert.equal(createResult.data.foo, newRecord.foo); + // check getting the record gets the same info + let getResult = await collection.get(createResult.data.id); + deepEqual(createResult.data, getResult.data); + // check what happens if we create the same item again (it should throw + // since you can't create with id) + try { + await collection.create(createResult.data); + do_throw("Creation of a record with an id should fail"); + } catch (err) {} + // try a few creates without waiting for the first few to resolve + let promises = []; + promises.push(collection.create(newRecord)); + promises.push(collection.create(newRecord)); + promises.push(collection.create(newRecord)); + await collection.create(newRecord); + await Promise.all(promises); + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +// test some operations on multiple connections +add_task(async function test_kinto_add_get() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection1 = do_get_kinto_collection(sqliteHandle); + const collection2 = do_get_kinto_collection( + sqliteHandle, + "test_collection_2" + ); + + let newRecord = { foo: "bar" }; + + // perform several write operations alternately without waiting for promises + // to resolve + let promises = []; + for (let i = 0; i < 10; i++) { + promises.push(collection1.create(newRecord)); + promises.push(collection2.create(newRecord)); + } + + // ensure subsequent operations still work + await Promise.all([ + collection1.create(newRecord), + collection2.create(newRecord), + ]); + await Promise.all(promises); + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +add_task(async function test_kinto_update() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + const newRecord = { foo: "bar" }; + // check a record is created + let createResult = await collection.create(newRecord); + Assert.equal(createResult.data.foo, newRecord.foo); + Assert.equal(createResult.data._status, "created"); + // check we can update this OK + let copiedRecord = Object.assign(createResult.data, {}); + deepEqual(createResult.data, copiedRecord); + copiedRecord.foo = "wibble"; + let updateResult = await collection.update(copiedRecord); + // check the field was updated + Assert.equal(updateResult.data.foo, copiedRecord.foo); + // check the status is still "created", since we haven't synced + // the record + Assert.equal(updateResult.data._status, "created"); + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +add_task(async function test_kinto_clear() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + + // create an expected number of records + const expected = 10; + const newRecord = { foo: "bar" }; + for (let i = 0; i < expected; i++) { + await collection.create(newRecord); + } + // check the collection contains the correct number + let list = await collection.list(); + Assert.equal(list.data.length, expected); + // clear the collection and check again - should be 0 + await collection.clear(); + list = await collection.list(); + Assert.equal(list.data.length, 0); + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +add_task(async function test_kinto_delete() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + const newRecord = { foo: "bar" }; + // check a record is created + let createResult = await collection.create(newRecord); + Assert.equal(createResult.data.foo, newRecord.foo); + // check getting the record gets the same info + let getResult = await collection.get(createResult.data.id); + deepEqual(createResult.data, getResult.data); + // delete that record + let deleteResult = await collection.delete(createResult.data.id); + // check the ID is set on the result + Assert.equal(getResult.data.id, deleteResult.data.id); + // and check that get no longer returns the record + try { + getResult = await collection.get(createResult.data.id); + do_throw("there should not be a result"); + } catch (e) {} + } finally { + await sqliteHandle.close(); + } +}); + +add_task(async function test_kinto_list() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + const expected = 10; + const created = []; + for (let i = 0; i < expected; i++) { + let newRecord = { foo: "test " + i }; + let createResult = await collection.create(newRecord); + created.push(createResult.data); + } + // check the collection contains the correct number + let list = await collection.list(); + Assert.equal(list.data.length, expected); + + // check that all created records exist in the retrieved list + for (let createdRecord of created) { + let found = false; + for (let retrievedRecord of list.data) { + if (createdRecord.id == retrievedRecord.id) { + deepEqual(createdRecord, retrievedRecord); + found = true; + } + } + Assert.ok(found); + } + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +add_task(async function test_importBulk_ignores_already_imported_records() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + const record = { + id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", + title: "foo", + last_modified: 1457896541, + }; + await collection.importBulk([record]); + let impactedRecords = await collection.importBulk([record]); + Assert.equal(impactedRecords.length, 0); + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +add_task(async function test_loadDump_should_overwrite_old_records() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + const record = { + id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", + title: "foo", + last_modified: 1457896541, + }; + await collection.loadDump([record]); + const updated = Object.assign({}, record, { last_modified: 1457896543 }); + let impactedRecords = await collection.loadDump([updated]); + Assert.equal(impactedRecords.length, 1); + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +add_task(async function test_loadDump_should_not_overwrite_unsynced_records() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f"; + await collection.create( + { id: recordId, title: "foo" }, + { useRecordId: true } + ); + const record = { id: recordId, title: "bar", last_modified: 1457896541 }; + let impactedRecords = await collection.loadDump([record]); + Assert.equal(impactedRecords.length, 0); + } finally { + await sqliteHandle.close(); + } +}); + +add_task(clear_collection); + +add_task( + async function test_loadDump_should_not_overwrite_records_without_last_modified() { + let sqliteHandle; + try { + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f"; + await collection.create({ id: recordId, title: "foo" }, { synced: true }); + const record = { id: recordId, title: "bar", last_modified: 1457896541 }; + let impactedRecords = await collection.loadDump([record]); + Assert.equal(impactedRecords.length, 0); + } finally { + await sqliteHandle.close(); + } + } +); + +add_task(clear_collection); + +// Now do some sanity checks against a server - we're not looking to test +// core kinto.js functionality here (there is excellent test coverage in +// kinto.js), more making sure things are basically working as expected. +add_task(async function test_kinto_sync() { + const configPath = "/v1/"; + const metadataPath = "/v1/buckets/default/collections/test_collection"; + const recordsPath = "/v1/buckets/default/collections/test_collection/records"; + // register a handler + function handleResponse(request, response) { + try { + const sampled = getSampleResponse(request, server.identity.primaryPort); + 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()); + } + response.setHeader("Date", new Date().toUTCString()); + + response.write(sampled.responseBody); + } catch (e) { + dump(`${e}\n`); + } + } + server.registerPathHandler(configPath, handleResponse); + server.registerPathHandler(metadataPath, handleResponse); + server.registerPathHandler(recordsPath, handleResponse); + + // create an empty collection, sync to populate + let sqliteHandle; + try { + let result; + sqliteHandle = await do_get_kinto_sqliteHandle(); + const collection = do_get_kinto_collection(sqliteHandle); + + result = await collection.sync(); + Assert.ok(result.ok); + + // our test data has a single record; it should be in the local collection + let list = await collection.list(); + Assert.equal(list.data.length, 1); + + // now sync again; we should now have 2 records + result = await collection.sync(); + Assert.ok(result.ok); + list = await collection.list(); + Assert.equal(list.data.length, 2); + + // sync again; the second records should have been modified + const before = list.data[0].title; + result = await collection.sync(); + Assert.ok(result.ok); + list = await collection.list(); + const after = list.data[1].title; + Assert.notEqual(before, after); + + const manualID = list.data[0].id; + Assert.equal(list.data.length, 3); + Assert.equal(manualID, "some-manually-chosen-id"); + } finally { + await sqliteHandle.close(); + } +}); + +function run_test() { + // Set up an HTTP Server + server = new HttpServer(); + server.start(-1); + + run_next_test(); + + registerCleanupFunction(function () { + server.stop(function () {}); + }); +} + +// get a response for a given request from sample data +function getSampleResponse(req, port) { + const responses = { + OPTIONS: { + sampleHeaders: [ + "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page", + "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS", + "Access-Control-Allow-Origin: *", + "Content-Type: application/json; charset=UTF-8", + "Server: waitress", + ], + status: { status: 200, statusText: "OK" }, + responseBody: "null", + }, + "GET:/v1/?": { + 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", + }), + }, + "GET:/v1/buckets/default/collections/test_collection": { + sampleHeaders: [ + "Access-Control-Allow-Origin: *", + "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", + "Content-Type: application/json; charset=UTF-8", + "Server: waitress", + 'Etag: "1234"', + ], + status: { status: 200, statusText: "OK" }, + responseBody: JSON.stringify({ + data: { + id: "test_collection", + last_modified: 1234, + }, + }), + }, + "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": + { + sampleHeaders: [ + "Access-Control-Allow-Origin: *", + "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", + "Content-Type: application/json; charset=UTF-8", + "Server: waitress", + 'Etag: "1445606341071"', + ], + status: { status: 200, statusText: "OK" }, + responseBody: JSON.stringify({ + data: [ + { + last_modified: 1445606341071, + done: false, + id: "68db8313-686e-4fff-835e-07d78ad6f2af", + title: "New test", + }, + ], + }), + }, + "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": + { + sampleHeaders: [ + "Access-Control-Allow-Origin: *", + "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", + "Content-Type: application/json; charset=UTF-8", + "Server: waitress", + 'Etag: "1445607941223"', + ], + status: { status: 200, statusText: "OK" }, + responseBody: JSON.stringify({ + data: [ + { + last_modified: 1445607941223, + done: false, + id: "901967b0-f729-4b30-8d8d-499cba7f4b1d", + title: "Another new test", + }, + ], + }), + }, + "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223": + { + sampleHeaders: [ + "Access-Control-Allow-Origin: *", + "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", + "Content-Type: application/json; charset=UTF-8", + "Server: waitress", + 'Etag: "1445607541267"', + ], + status: { status: 200, statusText: "OK" }, + responseBody: JSON.stringify({ + data: [ + { + last_modified: 1445607541265, + done: false, + id: "901967b0-f729-4b30-8d8d-499cba7f4b1d", + title: "Modified title", + }, + { + last_modified: 1445607541267, + done: true, + id: "some-manually-chosen-id", + title: "New record with custom ID", + }, + ], + }), + }, + }; + return ( + responses[`${req.method}:${req.path}?${req.queryString}`] || + responses[`${req.method}:${req.path}`] || + responses[req.method] + ); +} diff --git a/services/common/tests/unit/test_load_modules.js b/services/common/tests/unit/test_load_modules.js new file mode 100644 index 0000000000..d86165266f --- /dev/null +++ b/services/common/tests/unit/test_load_modules.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const MODULE_BASE = "resource://services-common/"; +const shared_modules = [ + "async.sys.mjs", + "logmanager.sys.mjs", + "rest.sys.mjs", + "utils.sys.mjs", +]; + +const non_android_modules = ["tokenserverclient.sys.mjs"]; + +const TEST_BASE = "resource://testing-common/services/common/"; +const shared_test_modules = ["logging.sys.mjs"]; + +function expectImportsToSucceed(mm, base = MODULE_BASE) { + for (let m of mm) { + let resource = base + m; + let succeeded = false; + try { + ChromeUtils.importESModule(resource); + succeeded = true; + } catch (e) {} + + if (!succeeded) { + throw new Error(`Importing ${resource} should have succeeded!`); + } + } +} + +function expectImportsToFail(mm, base = MODULE_BASE) { + for (let m of mm) { + let resource = base + m; + let succeeded = false; + try { + ChromeUtils.importESModule(resource); + succeeded = true; + } catch (e) {} + + if (succeeded) { + throw new Error(`Importing ${resource} should have failed!`); + } + } +} + +function run_test() { + expectImportsToSucceed(shared_modules); + expectImportsToSucceed(shared_test_modules, TEST_BASE); + + if (AppConstants.platform != "android") { + expectImportsToSucceed(non_android_modules); + } else { + expectImportsToFail(non_android_modules); + } +} diff --git a/services/common/tests/unit/test_logmanager.js b/services/common/tests/unit/test_logmanager.js new file mode 100644 index 0000000000..89ac274e61 --- /dev/null +++ b/services/common/tests/unit/test_logmanager.js @@ -0,0 +1,330 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// NOTE: The sync test_errorhandler_* tests have quite good coverage for +// other aspects of this. + +const { LogManager } = ChromeUtils.importESModule( + "resource://services-common/logmanager.sys.mjs" +); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); + +// Returns an array of [consoleAppender, dumpAppender, [fileAppenders]] for +// the specified log. Note that fileAppenders will usually have length=1 +function getAppenders(log) { + let capps = log.appenders.filter(app => app instanceof Log.ConsoleAppender); + equal(capps.length, 1, "should only have one console appender"); + let dapps = log.appenders.filter(app => app instanceof Log.DumpAppender); + equal(dapps.length, 1, "should only have one dump appender"); + let fapps = log.appenders.filter( + app => app instanceof LogManager.StorageStreamAppender + ); + return [capps[0], dapps[0], fapps]; +} + +// Test that the correct thing happens when no prefs exist for the log manager. +add_task(async function test_noPrefs() { + // tell the log manager to init with a pref branch that doesn't exist. + let lm = new LogManager("no-such-branch.", ["TestLog"], "test"); + + let log = Log.repository.getLogger("TestLog"); + let [capp, dapp, fapps] = getAppenders(log); + // The console appender gets "Fatal" while the "dump" appender gets "Error" levels + equal(capp.level, Log.Level.Fatal); + equal(dapp.level, Log.Level.Error); + // and the file (stream) appender gets Debug by default + equal(fapps.length, 1, "only 1 file appender"); + equal(fapps[0].level, Log.Level.Debug); + lm.finalize(); +}); + +// Test that changes to the prefs used by the log manager are updated dynamically. +add_task(async function test_PrefChanges() { + Services.prefs.setStringPref( + "log-manager.test.log.appender.console", + "Trace" + ); + Services.prefs.setStringPref("log-manager.test.log.appender.dump", "Trace"); + Services.prefs.setStringPref( + "log-manager.test.log.appender.file.level", + "Trace" + ); + let lm = new LogManager("log-manager.test.", ["TestLog2"], "test"); + + let log = Log.repository.getLogger("TestLog2"); + let [capp, dapp, [fapp]] = getAppenders(log); + equal(capp.level, Log.Level.Trace); + equal(dapp.level, Log.Level.Trace); + equal(fapp.level, Log.Level.Trace); + // adjust the prefs and they should magically be reflected in the appenders. + Services.prefs.setStringPref( + "log-manager.test.log.appender.console", + "Debug" + ); + Services.prefs.setStringPref("log-manager.test.log.appender.dump", "Debug"); + Services.prefs.setStringPref( + "log-manager.test.log.appender.file.level", + "Debug" + ); + equal(capp.level, Log.Level.Debug); + equal(dapp.level, Log.Level.Debug); + equal(fapp.level, Log.Level.Debug); + // and invalid values should cause them to fallback to their defaults. + Services.prefs.setStringPref("log-manager.test.log.appender.console", "xxx"); + Services.prefs.setStringPref("log-manager.test.log.appender.dump", "xxx"); + Services.prefs.setStringPref( + "log-manager.test.log.appender.file.level", + "xxx" + ); + equal(capp.level, Log.Level.Fatal); + equal(dapp.level, Log.Level.Error); + equal(fapp.level, Log.Level.Debug); + lm.finalize(); +}); + +// Test that the same log used by multiple log managers does the right thing. +add_task(async function test_SharedLogs() { + // create the prefs for the first instance. + Services.prefs.setStringPref( + "log-manager-1.test.log.appender.console", + "Trace" + ); + Services.prefs.setStringPref("log-manager-1.test.log.appender.dump", "Trace"); + Services.prefs.setStringPref( + "log-manager-1.test.log.appender.file.level", + "Trace" + ); + let lm1 = new LogManager("log-manager-1.test.", ["TestLog3"], "test"); + + // and the second. + Services.prefs.setStringPref( + "log-manager-2.test.log.appender.console", + "Debug" + ); + Services.prefs.setStringPref("log-manager-2.test.log.appender.dump", "Debug"); + Services.prefs.setStringPref( + "log-manager-2.test.log.appender.file.level", + "Debug" + ); + let lm2 = new LogManager("log-manager-2.test.", ["TestLog3"], "test"); + + let log = Log.repository.getLogger("TestLog3"); + let [capp, dapp] = getAppenders(log); + + // console and dump appenders should be "trace" as it is more verbose than + // "debug" + equal(capp.level, Log.Level.Trace); + equal(dapp.level, Log.Level.Trace); + + // Set the prefs on the -1 branch to "Error" - it should then end up with + // "Debug" from the -2 branch. + Services.prefs.setStringPref( + "log-manager-1.test.log.appender.console", + "Error" + ); + Services.prefs.setStringPref("log-manager-1.test.log.appender.dump", "Error"); + Services.prefs.setStringPref( + "log-manager-1.test.log.appender.file.level", + "Error" + ); + + equal(capp.level, Log.Level.Debug); + equal(dapp.level, Log.Level.Debug); + + lm1.finalize(); + lm2.finalize(); +}); + +// A little helper to test what log files exist. We expect exactly zero (if +// prefix is null) or exactly one with the specified prefix. +function checkLogFile(prefix) { + let logsdir = FileUtils.getDir("ProfD", ["weave", "logs"]); + let entries = logsdir.directoryEntries; + if (!prefix) { + // expecting no files. + ok(!entries.hasMoreElements()); + } else { + // expecting 1 file. + ok(entries.hasMoreElements()); + let logfile = entries.getNext().QueryInterface(Ci.nsIFile); + equal(logfile.leafName.slice(-4), ".txt"); + ok(logfile.leafName.startsWith(prefix + "-test-"), logfile.leafName); + // and remove it ready for the next check. + logfile.remove(false); + } +} + +// Test that we correctly write error logs by default +add_task(async function test_logFileErrorDefault() { + let lm = new LogManager("log-manager.test.", ["TestLog2"], "test"); + + let log = Log.repository.getLogger("TestLog2"); + log.error("an error message"); + await lm.resetFileLog(lm.REASON_ERROR); + // One error log file exists. + checkLogFile("error"); + + lm.finalize(); +}); + +// Test that we correctly write success logs. +add_task(async function test_logFileSuccess() { + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnError", + false + ); + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnSuccess", + false + ); + + let lm = new LogManager("log-manager.test.", ["TestLog2"], "test"); + + let log = Log.repository.getLogger("TestLog2"); + log.info("an info message"); + await lm.resetFileLog(); + // Zero log files exist. + checkLogFile(null); + + // Reset logOnSuccess and do it again - log should appear. + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnSuccess", + true + ); + log.info("an info message"); + await lm.resetFileLog(); + + checkLogFile("success"); + + // Now test with no "reason" specified and no "error" record. + log.info("an info message"); + await lm.resetFileLog(); + // should get a "success" entry. + checkLogFile("success"); + + // With no "reason" and an error record - should get no success log. + log.error("an error message"); + await lm.resetFileLog(); + // should get no entry + checkLogFile(null); + + // And finally now with no error, to ensure that the fact we had an error + // previously doesn't persist after the .resetFileLog call. + log.info("an info message"); + await lm.resetFileLog(); + checkLogFile("success"); + + lm.finalize(); +}); + +// Test that we correctly write error logs. +add_task(async function test_logFileError() { + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnError", + false + ); + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnSuccess", + false + ); + + let lm = new LogManager("log-manager.test.", ["TestLog2"], "test"); + + let log = Log.repository.getLogger("TestLog2"); + log.info("an info message"); + let reason = await lm.resetFileLog(); + Assert.equal(reason, null, "null returned when no file created."); + // Zero log files exist. + checkLogFile(null); + + // Reset logOnSuccess - success logs should appear if no error records. + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnSuccess", + true + ); + log.info("an info message"); + reason = await lm.resetFileLog(); + Assert.equal(reason, lm.SUCCESS_LOG_WRITTEN); + checkLogFile("success"); + + // Set logOnError and unset logOnSuccess - error logs should appear. + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnSuccess", + false + ); + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnError", + true + ); + log.error("an error message"); + reason = await lm.resetFileLog(); + Assert.equal(reason, lm.ERROR_LOG_WRITTEN); + checkLogFile("error"); + + // Now test with no "error" record. + log.info("an info message"); + reason = await lm.resetFileLog(); + // should get no file + Assert.equal(reason, null); + checkLogFile(null); + + // With an error record we should get an error log. + log.error("an error message"); + reason = await lm.resetFileLog(); + // should get en error log + Assert.equal(reason, lm.ERROR_LOG_WRITTEN); + checkLogFile("error"); + + // And finally now with success, to ensure that the fact we had an error + // previously doesn't persist after the .resetFileLog call. + log.info("an info message"); + await lm.resetFileLog(); + checkLogFile(null); + + lm.finalize(); +}); + +function countLogFiles() { + let logsdir = FileUtils.getDir("ProfD", ["weave", "logs"]); + let count = 0; + for (let entry of logsdir.directoryEntries) { + void entry; + count += 1; + } + return count; +} + +// Test that removeAllLogs removes all log files. +add_task(async function test_logFileError() { + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnError", + true + ); + Services.prefs.setBoolPref( + "log-manager.test.log.appender.file.logOnSuccess", + true + ); + + let lm = new LogManager("log-manager.test.", ["TestLog2"], "test"); + + let log = Log.repository.getLogger("TestLog2"); + log.info("an info message"); + let reason = await lm.resetFileLog(); + Assert.equal(reason, lm.SUCCESS_LOG_WRITTEN, "success log was written."); + + log.error("an error message"); + reason = await lm.resetFileLog(); + Assert.equal(reason, lm.ERROR_LOG_WRITTEN); + + Assert.equal(countLogFiles(), 2, "expect 2 log files"); + await lm.removeAllLogs(); + Assert.equal( + countLogFiles(), + 0, + "should be no log files after removing them" + ); + + lm.finalize(); +}); diff --git a/services/common/tests/unit/test_observers.js b/services/common/tests/unit/test_observers.js new file mode 100644 index 0000000000..b0fce95e0b --- /dev/null +++ b/services/common/tests/unit/test_observers.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Observers } = ChromeUtils.importESModule( + "resource://services-common/observers.sys.mjs" +); + +var gSubject = {}; + +add_test(function test_function_observer() { + let foo = false; + + let onFoo = function (subject, data) { + foo = !foo; + Assert.equal(subject, gSubject); + Assert.equal(data, "some data"); + }; + + Observers.add("foo", onFoo); + Observers.notify("foo", gSubject, "some data"); + + // The observer was notified after being added. + Assert.ok(foo); + + Observers.remove("foo", onFoo); + Observers.notify("foo"); + + // The observer was not notified after being removed. + Assert.ok(foo); + + run_next_test(); +}); + +add_test(function test_method_observer() { + let obj = { + foo: false, + onFoo(subject, data) { + this.foo = !this.foo; + Assert.equal(subject, gSubject); + Assert.equal(data, "some data"); + }, + }; + + // The observer is notified after being added. + Observers.add("foo", obj.onFoo, obj); + Observers.notify("foo", gSubject, "some data"); + Assert.ok(obj.foo); + + // The observer is not notified after being removed. + Observers.remove("foo", obj.onFoo, obj); + Observers.notify("foo"); + Assert.ok(obj.foo); + + run_next_test(); +}); + +add_test(function test_object_observer() { + let obj = { + foo: false, + observe(subject, topic, data) { + this.foo = !this.foo; + + Assert.equal(subject, gSubject); + Assert.equal(topic, "foo"); + Assert.equal(data, "some data"); + }, + }; + + Observers.add("foo", obj); + Observers.notify("foo", gSubject, "some data"); + + // The observer is notified after being added. + Assert.ok(obj.foo); + + Observers.remove("foo", obj); + Observers.notify("foo"); + + // The observer is not notified after being removed. + Assert.ok(obj.foo); + + run_next_test(); +}); diff --git a/services/common/tests/unit/test_restrequest.js b/services/common/tests/unit/test_restrequest.js new file mode 100644 index 0000000000..9ae5e9429a --- /dev/null +++ b/services/common/tests/unit/test_restrequest.js @@ -0,0 +1,860 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { RESTRequest } = ChromeUtils.importESModule( + "resource://services-common/rest.sys.mjs" +); + +function run_test() { + Log.repository.getLogger("Services.Common.RESTRequest").level = + Log.Level.Trace; + initTestLogging("Trace"); + + run_next_test(); +} + +/** + * Initializing a RESTRequest with an invalid URI throws + * NS_ERROR_MALFORMED_URI. + */ +add_test(function test_invalid_uri() { + do_check_throws(function () { + new RESTRequest("an invalid URI"); + }, Cr.NS_ERROR_MALFORMED_URI); + run_next_test(); +}); + +/** + * Verify initial values for attributes. + */ +add_test(function test_attributes() { + let uri = "http://foo.com/bar/baz"; + let request = new RESTRequest(uri); + + Assert.ok(request.uri instanceof Ci.nsIURI); + Assert.equal(request.uri.spec, uri); + Assert.equal(request.response, null); + Assert.equal(request.status, request.NOT_SENT); + let expectedLoadFlags = + Ci.nsIRequest.LOAD_BYPASS_CACHE | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_ANONYMOUS; + Assert.equal(request.loadFlags, expectedLoadFlags); + + run_next_test(); +}); + +/** + * Verify that a proxy auth redirect doesn't break us. This has to be the first + * request made in the file! + */ +add_task(async function test_proxy_auth_redirect() { + let pacFetched = false; + function pacHandler(metadata, response) { + pacFetched = true; + let body = 'function FindProxyForURL(url, host) { return "DIRECT"; }'; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader( + "Content-Type", + "application/x-ns-proxy-autoconfig", + false + ); + response.bodyOutputStream.write(body, body.length); + } + + let fetched = false; + function original(metadata, response) { + fetched = true; + let body = "TADA!"; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); + } + + let server = httpd_setup({ + "/original": original, + "/pac3": pacHandler, + }); + PACSystemSettings.PACURI = server.baseURI + "/pac3"; + installFakePAC(); + + let req = new RESTRequest(server.baseURI + "/original"); + await req.get(); + + Assert.ok(pacFetched); + Assert.ok(fetched); + + Assert.ok(req.response.success); + Assert.equal("TADA!", req.response.body); + uninstallFakePAC(); + await promiseStopServer(server); +}); + +/** + * Ensure that failures that cause asyncOpen to throw + * result in callbacks being invoked. + * Bug 826086. + */ +add_task(async function test_forbidden_port() { + let request = new RESTRequest("http://localhost:6000/"); + + await Assert.rejects( + request.get(), + error => error.result == Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED + ); +}); + +/** + * Demonstrate API short-hand: create a request and dispatch it immediately. + */ +add_task(async function test_simple_get() { + let handler = httpd_handler(200, "OK", "Huzzah!"); + let server = httpd_setup({ "/resource": handler }); + let request = new RESTRequest(server.baseURI + "/resource"); + let promiseResponse = request.get(); + + Assert.equal(request.status, request.SENT); + Assert.equal(request.method, "GET"); + + let response = await promiseResponse; + Assert.equal(response, request.response); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(response.success); + Assert.equal(response.status, 200); + Assert.equal(response.body, "Huzzah!"); + await promiseStopServer(server); +}); + +/** + * Test HTTP GET with all bells and whistles. + */ +add_task(async function test_get() { + let handler = httpd_handler(200, "OK", "Huzzah!"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + Assert.equal(request.status, request.NOT_SENT); + + let promiseResponse = request.get(); + + Assert.equal(request.status, request.SENT); + Assert.equal(request.method, "GET"); + + Assert.ok(!!(request.channel.loadFlags & Ci.nsIRequest.LOAD_BYPASS_CACHE)); + Assert.ok(!!(request.channel.loadFlags & Ci.nsIRequest.INHIBIT_CACHING)); + + let response = await promiseResponse; + + Assert.equal(response, request.response); + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, "Huzzah!"); + Assert.equal(handler.request.method, "GET"); + + await Assert.rejects(request.get(), /Request has already been sent/); + + await promiseStopServer(server); +}); + +/** + * Test HTTP GET with UTF-8 content, and custom Content-Type. + */ +add_task(async function test_get_utf8() { + let response = "Hello World or Καλημέρα κόσμε or こんにちは 世界 😺"; + + let contentType = "text/plain"; + let charset = true; + let charsetSuffix = "; charset=UTF-8"; + + let server = httpd_setup({ + "/resource": function (req, res) { + res.setStatusLine(req.httpVersion, 200, "OK"); + res.setHeader( + "Content-Type", + contentType + (charset ? charsetSuffix : "") + ); + + let converter = Cc[ + "@mozilla.org/intl/converter-output-stream;1" + ].createInstance(Ci.nsIConverterOutputStream); + converter.init(res.bodyOutputStream, "UTF-8"); + converter.writeString(response); + converter.close(); + }, + }); + + // Check if charset in Content-Type is propertly interpreted. + let request1 = new RESTRequest(server.baseURI + "/resource"); + await request1.get(); + + Assert.equal(request1.response.status, 200); + Assert.equal(request1.response.body, response); + Assert.equal( + request1.response.headers["content-type"], + contentType + charsetSuffix + ); + + // Check that we default to UTF-8 if Content-Type doesn't have a charset + charset = false; + let request2 = new RESTRequest(server.baseURI + "/resource"); + await request2.get(); + Assert.equal(request2.response.status, 200); + Assert.equal(request2.response.body, response); + Assert.equal(request2.response.headers["content-type"], contentType); + Assert.equal(request2.response.charset, "utf-8"); + + let request3 = new RESTRequest(server.baseURI + "/resource"); + + // With the test server we tend to get onDataAvailable in chunks of 8192 (in + // real network requests there doesn't appear to be any pattern to the size of + // the data `onDataAvailable` is called with), the smiling cat emoji encodes as + // 4 bytes, and so when utf8 encoded, the `"a" + "😺".repeat(2048)` will not be + // aligned onto a codepoint. + // + // Since 8192 isn't guaranteed and could easily change, the following string is + // a) very long, and b) misaligned on roughly 3/4 of the bytes, as a safety + // measure. + response = ("a" + "😺".repeat(2048)).repeat(10); + + await request3.get(); + + Assert.equal(request3.response.status, 200); + + // Make sure it came through ok, despite the misalignment. + Assert.equal(request3.response.body, response); + + await promiseStopServer(server); +}); + +/** + * Test HTTP POST data is encoded as UTF-8 by default. + */ +add_task(async function test_post_utf8() { + // We setup a handler that responds with exactly what it received. + // Given we've already tested above that responses are correctly utf-8 + // decoded we can surmise that the correct response coming back means the + // input must also have been encoded. + let server = httpd_setup({ + "/echo": function (req, res) { + res.setStatusLine(req.httpVersion, 200, "OK"); + res.setHeader("Content-Type", req.getHeader("content-type")); + // Get the body as bytes and write them back without touching them + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(req.bodyInputStream); + let body = sis.read(sis.available()); + sis.close(); + res.write(body); + }, + }); + + let data = { + copyright: "©", + // See the comment in test_get_utf8 about this string. + long: ("a" + "😺".repeat(2048)).repeat(10), + }; + let request1 = new RESTRequest(server.baseURI + "/echo"); + await request1.post(data); + + Assert.equal(request1.response.status, 200); + deepEqual(JSON.parse(request1.response.body), data); + Assert.equal( + request1.response.headers["content-type"], + "application/json; charset=utf-8" + ); + + await promiseStopServer(server); +}); + +/** + * Test more variations of charset handling. + */ +add_task(async function test_charsets() { + let response = "Hello World, I can't speak Russian"; + + let contentType = "text/plain"; + let charset = true; + let charsetSuffix = "; charset=us-ascii"; + + let server = httpd_setup({ + "/resource": function (req, res) { + res.setStatusLine(req.httpVersion, 200, "OK"); + res.setHeader( + "Content-Type", + contentType + (charset ? charsetSuffix : "") + ); + + let converter = Cc[ + "@mozilla.org/intl/converter-output-stream;1" + ].createInstance(Ci.nsIConverterOutputStream); + converter.init(res.bodyOutputStream, "us-ascii"); + converter.writeString(response); + converter.close(); + }, + }); + + // Check that provided charset overrides hint. + let request1 = new RESTRequest(server.baseURI + "/resource"); + request1.charset = "not-a-charset"; + await request1.get(); + Assert.equal(request1.response.status, 200); + Assert.equal(request1.response.body, response); + Assert.equal( + request1.response.headers["content-type"], + contentType + charsetSuffix + ); + Assert.equal(request1.response.charset, "us-ascii"); + + // Check that hint is used if Content-Type doesn't have a charset. + charset = false; + let request2 = new RESTRequest(server.baseURI + "/resource"); + request2.charset = "us-ascii"; + await request2.get(); + + Assert.equal(request2.response.status, 200); + Assert.equal(request2.response.body, response); + Assert.equal(request2.response.headers["content-type"], contentType); + Assert.equal(request2.response.charset, "us-ascii"); + + await promiseStopServer(server); +}); + +/** + * Used for testing PATCH/PUT/POST methods. + */ +async function check_posting_data(method) { + let funcName = method.toLowerCase(); + let handler = httpd_handler(200, "OK", "Got it!"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + Assert.equal(request.status, request.NOT_SENT); + let responsePromise = request[funcName]("Hullo?"); + Assert.equal(request.status, request.SENT); + Assert.equal(request.method, method); + + let response = await responsePromise; + + Assert.equal(response, request.response); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, "Got it!"); + + Assert.equal(handler.request.method, method); + Assert.equal(handler.request.body, "Hullo?"); + Assert.equal(handler.request.getHeader("Content-Type"), "text/plain"); + + await Assert.rejects( + request[funcName]("Hai!"), + /Request has already been sent/ + ); + + await promiseStopServer(server); +} + +/** + * Test HTTP PATCH with a simple string argument and default Content-Type. + */ +add_task(async function test_patch() { + await check_posting_data("PATCH"); +}); + +/** + * Test HTTP PUT with a simple string argument and default Content-Type. + */ +add_task(async function test_put() { + await check_posting_data("PUT"); +}); + +/** + * Test HTTP POST with a simple string argument and default Content-Type. + */ +add_task(async function test_post() { + await check_posting_data("POST"); +}); + +/** + * Test HTTP DELETE. + */ +add_task(async function test_delete() { + let handler = httpd_handler(200, "OK", "Got it!"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + Assert.equal(request.status, request.NOT_SENT); + let responsePromise = request.delete(); + Assert.equal(request.status, request.SENT); + Assert.equal(request.method, "DELETE"); + + let response = await responsePromise; + Assert.equal(response, request.response); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, "Got it!"); + Assert.equal(handler.request.method, "DELETE"); + + await Assert.rejects(request.delete(), /Request has already been sent/); + + await promiseStopServer(server); +}); + +/** + * Test an HTTP response with a non-200 status code. + */ +add_task(async function test_get_404() { + let handler = httpd_handler(404, "Not Found", "Cannae find it!"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + await request.get(); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(!request.response.success); + Assert.equal(request.response.status, 404); + Assert.equal(request.response.body, "Cannae find it!"); + + await promiseStopServer(server); +}); + +/** + * The 'data' argument to PUT, if not a string already, is automatically + * stringified as JSON. + */ +add_task(async function test_put_json() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let sample_data = { + some: "sample_data", + injson: "format", + number: 42, + }; + let request = new RESTRequest(server.baseURI + "/resource"); + await request.put(sample_data); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + Assert.equal(handler.request.method, "PUT"); + Assert.equal(handler.request.body, JSON.stringify(sample_data)); + Assert.equal( + handler.request.getHeader("Content-Type"), + "application/json; charset=utf-8" + ); + + await promiseStopServer(server); +}); + +/** + * The 'data' argument to POST, if not a string already, is automatically + * stringified as JSON. + */ +add_task(async function test_post_json() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let sample_data = { + some: "sample_data", + injson: "format", + number: 42, + }; + let request = new RESTRequest(server.baseURI + "/resource"); + await request.post(sample_data); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + Assert.equal(handler.request.method, "POST"); + Assert.equal(handler.request.body, JSON.stringify(sample_data)); + Assert.equal( + handler.request.getHeader("Content-Type"), + "application/json; charset=utf-8" + ); + + await promiseStopServer(server); +}); + +/** + * The content-type will be text/plain without a charset if the 'data' argument + * to POST is already a string. + */ +add_task(async function test_post_json() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let sample_data = "hello"; + let request = new RESTRequest(server.baseURI + "/resource"); + await request.post(sample_data); + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + Assert.equal(handler.request.method, "POST"); + Assert.equal(handler.request.body, sample_data); + Assert.equal(handler.request.getHeader("Content-Type"), "text/plain"); + + await promiseStopServer(server); +}); + +/** + * HTTP PUT with a custom Content-Type header. + */ +add_task(async function test_put_override_content_type() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + request.setHeader("Content-Type", "application/lolcat"); + await request.put("O HAI!!1!"); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + Assert.equal(handler.request.method, "PUT"); + Assert.equal(handler.request.body, "O HAI!!1!"); + Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat"); + + await promiseStopServer(server); +}); + +/** + * HTTP POST with a custom Content-Type header. + */ +add_task(async function test_post_override_content_type() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + request.setHeader("Content-Type", "application/lolcat"); + await request.post("O HAI!!1!"); + + Assert.equal(request.status, request.COMPLETED); + Assert.ok(request.response.success); + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + Assert.equal(handler.request.method, "POST"); + Assert.equal(handler.request.body, "O HAI!!1!"); + Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat"); + + await promiseStopServer(server); +}); + +/** + * No special headers are sent by default on a GET request. + */ +add_task(async function test_get_no_headers() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let ignore_headers = [ + "host", + "user-agent", + "accept", + "accept-language", + "accept-encoding", + "accept-charset", + "keep-alive", + "connection", + "pragma", + "cache-control", + "content-length", + "sec-fetch-dest", + "sec-fetch-mode", + "sec-fetch-site", + "sec-fetch-user", + ]; + let request = new RESTRequest(server.baseURI + "/resource"); + await request.get(); + + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + let server_headers = handler.request.headers; + while (server_headers.hasMoreElements()) { + let header = server_headers.getNext().toString(); + if (!ignore_headers.includes(header)) { + do_throw("Got unexpected header!"); + } + } + + await promiseStopServer(server); +}); + +/** + * Client includes default Accept header in API requests + */ +add_task(async function test_default_accept_headers() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + await request.get(); + + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + let accept_header = handler.request.getHeader("accept"); + + Assert.ok(!accept_header.includes("text/html")); + Assert.ok(!accept_header.includes("application/xhtml+xml")); + Assert.ok(!accept_header.includes("applcation/xml")); + + Assert.ok( + accept_header.includes("application/json") || + accept_header.includes("application/newlines") + ); + + await promiseStopServer(server); +}); + +/** + * Test changing the URI after having created the request. + */ +add_task(async function test_changing_uri() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest("http://localhost:1234/the-wrong-resource"); + request.uri = CommonUtils.makeURI(server.baseURI + "/resource"); + let response = await request.get(); + Assert.equal(response.status, 200); + await promiseStopServer(server); +}); + +/** + * Test setting HTTP request headers. + */ +add_task(async function test_request_setHeader() { + let handler = httpd_handler(200, "OK"); + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + + request.setHeader("X-What-Is-Weave", "awesome"); + request.setHeader("X-WHAT-is-Weave", "more awesomer"); + request.setHeader("Another-Header", "Hello World"); + await request.get(); + + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + Assert.equal(handler.request.getHeader("X-What-Is-Weave"), "more awesomer"); + Assert.equal(handler.request.getHeader("another-header"), "Hello World"); + + await promiseStopServer(server); +}); + +/** + * Test receiving HTTP response headers. + */ +add_task(async function test_response_headers() { + function handler(request, response) { + response.setHeader("X-What-Is-Weave", "awesome"); + response.setHeader("Another-Header", "Hello World"); + response.setStatusLine(request.httpVersion, 200, "OK"); + } + let server = httpd_setup({ "/resource": handler }); + let request = new RESTRequest(server.baseURI + "/resource"); + await request.get(); + + Assert.equal(request.response.status, 200); + Assert.equal(request.response.body, ""); + + Assert.equal(request.response.headers["x-what-is-weave"], "awesome"); + Assert.equal(request.response.headers["another-header"], "Hello World"); + + await promiseStopServer(server); +}); + +/** + * The onComplete() handler gets called in case of any network errors + * (e.g. NS_ERROR_CONNECTION_REFUSED). + */ +add_task(async function test_connection_refused() { + let request = new RESTRequest("http://localhost:1234/resource"); + + // Fail the test if we resolve, return the error if we reject + await Assert.rejects( + request.get(), + error => + error.result == Cr.NS_ERROR_CONNECTION_REFUSED && + error.message == "NS_ERROR_CONNECTION_REFUSED" + ); + + Assert.equal(request.status, request.COMPLETED); +}); + +/** + * Abort a request that just sent off. + */ +add_task(async function test_abort() { + function handler() { + do_throw("Shouldn't have gotten here!"); + } + let server = httpd_setup({ "/resource": handler }); + + let request = new RESTRequest(server.baseURI + "/resource"); + + // Aborting a request that hasn't been sent yet is pointless and will throw. + do_check_throws(function () { + request.abort(); + }); + + let responsePromise = request.get(); + request.abort(); + + // Aborting an already aborted request is pointless and will throw. + do_check_throws(function () { + request.abort(); + }); + + Assert.equal(request.status, request.ABORTED); + + await Assert.rejects(responsePromise, /NS_BINDING_ABORTED/); + + await promiseStopServer(server); +}); + +/** + * A non-zero 'timeout' property specifies the amount of seconds to wait after + * channel activity until the request is automatically canceled. + */ +add_task(async function test_timeout() { + let server = new HttpServer(); + let server_connection; + server._handler.handleResponse = function (connection) { + // This is a handler that doesn't do anything, just keeps the connection + // open, thereby mimicking a timing out connection. We keep a reference to + // the open connection for later so it can be properly disposed of. That's + // why you really only want to make one HTTP request to this server ever. + server_connection = connection; + }; + server.start(); + let identity = server.identity; + let uri = + identity.primaryScheme + + "://" + + identity.primaryHost + + ":" + + identity.primaryPort; + + let request = new RESTRequest(uri + "/resource"); + request.timeout = 0.1; // 100 milliseconds + + await Assert.rejects( + request.get(), + error => error.result == Cr.NS_ERROR_NET_TIMEOUT + ); + + Assert.equal(request.status, request.ABORTED); + + // server_connection is undefined on the Android emulator for reasons + // unknown. Yet, we still get here. If this test is refactored, we should + // investigate the reason why the above callback is behaving differently. + if (server_connection) { + _("Closing connection."); + server_connection.close(); + } + await promiseStopServer(server); +}); + +add_task(async function test_new_channel() { + _("Ensure a redirect to a new channel is handled properly."); + + function checkUA(metadata) { + let ua = metadata.getHeader("User-Agent"); + _("User-Agent is " + ua); + Assert.equal("foo bar", ua); + } + + let redirectRequested = false; + let redirectURL; + function redirectHandler(metadata, response) { + checkUA(metadata); + redirectRequested = true; + + let body = "Redirecting"; + response.setStatusLine(metadata.httpVersion, 307, "TEMPORARY REDIRECT"); + response.setHeader("Location", redirectURL); + response.bodyOutputStream.write(body, body.length); + } + + let resourceRequested = false; + function resourceHandler(metadata, response) { + checkUA(metadata); + resourceRequested = true; + + let body = "Test"; + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(body, body.length); + } + + let server1 = httpd_setup({ "/redirect": redirectHandler }); + let server2 = httpd_setup({ "/resource": resourceHandler }); + redirectURL = server2.baseURI + "/resource"; + + let request = new RESTRequest(server1.baseURI + "/redirect"); + request.setHeader("User-Agent", "foo bar"); + + // Swizzle in our own fakery, because this redirect is neither + // internal nor URI-preserving. RESTRequest's policy is to only + // copy headers under certain circumstances. + let protoMethod = request.shouldCopyOnRedirect; + request.shouldCopyOnRedirect = function wrapped(o, n, f) { + // Check the default policy. + Assert.ok(!protoMethod.call(this, o, n, f)); + return true; + }; + + let response = await request.get(); + + Assert.equal(200, response.status); + Assert.equal("Test", response.body); + Assert.ok(redirectRequested); + Assert.ok(resourceRequested); + + await promiseStopServer(server1); + await promiseStopServer(server2); +}); + +add_task(async function test_not_sending_cookie() { + function handler(metadata, response) { + let body = "COOKIE!"; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); + Assert.ok(!metadata.hasHeader("Cookie")); + } + let server = httpd_setup({ "/test": handler }); + + let uri = CommonUtils.makeURI(server.baseURI); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.cookies.setCookieStringFromHttp(uri, "test=test; path=/;", channel); + + let res = new RESTRequest(server.baseURI + "/test"); + let response = await res.get(); + + Assert.ok(response.success); + Assert.equal("COOKIE!", response.body); + + await promiseStopServer(server); +}); diff --git a/services/common/tests/unit/test_storage_adapter.js b/services/common/tests/unit/test_storage_adapter.js new file mode 100644 index 0000000000..1dda35ad12 --- /dev/null +++ b/services/common/tests/unit/test_storage_adapter.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); +const { FirefoxAdapter } = ChromeUtils.importESModule( + "resource://services-common/kinto-storage-adapter.sys.mjs" +); + +// set up what we need to make storage adapters +const kintoFilename = "kinto.sqlite"; + +function do_get_kinto_connection() { + return FirefoxAdapter.openConnection({ path: kintoFilename }); +} + +function do_get_kinto_adapter(sqliteHandle) { + return new FirefoxAdapter("test", { sqliteHandle }); +} + +function do_get_kinto_db() { + let profile = do_get_profile(); + let kintoDB = profile.clone(); + kintoDB.append(kintoFilename); + return kintoDB; +} + +function cleanup_kinto() { + add_test(function cleanup_kinto_files() { + let kintoDB = do_get_kinto_db(); + // clean up the db + kintoDB.remove(false); + run_next_test(); + }); +} + +function test_collection_operations() { + add_task(async function test_kinto_clear() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + await adapter.clear(); + await sqliteHandle.close(); + }); + + // test creating new records... and getting them again + add_task(async function test_kinto_create_new_get_existing() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + let record = { id: "test-id", foo: "bar" }; + await adapter.execute(transaction => transaction.create(record)); + let newRecord = await adapter.get("test-id"); + // ensure the record is the same as when it was added + deepEqual(record, newRecord); + await sqliteHandle.close(); + }); + + // test removing records + add_task(async function test_kinto_can_remove_some_records() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + // create a second record + let record = { id: "test-id-2", foo: "baz" }; + await adapter.execute(transaction => transaction.create(record)); + let newRecord = await adapter.get("test-id-2"); + deepEqual(record, newRecord); + // delete the record + await adapter.execute(transaction => transaction.delete(record.id)); + newRecord = await adapter.get(record.id); + // ... and ensure it's no longer there + Assert.equal(newRecord, undefined); + // ensure the other record still exists + newRecord = await adapter.get("test-id"); + Assert.notEqual(newRecord, undefined); + await sqliteHandle.close(); + }); + + // test getting records that don't exist + add_task(async function test_kinto_get_non_existant() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + // Kinto expects adapters to either: + let newRecord = await adapter.get("missing-test-id"); + // resolve with an undefined record + Assert.equal(newRecord, undefined); + await sqliteHandle.close(); + }); + + // test updating records... and getting them again + add_task(async function test_kinto_update_get_existing() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + let originalRecord = { id: "test-id", foo: "bar" }; + let updatedRecord = { id: "test-id", foo: "baz" }; + await adapter.clear(); + await adapter.execute(transaction => transaction.create(originalRecord)); + await adapter.execute(transaction => transaction.update(updatedRecord)); + // ensure the record exists + let newRecord = await adapter.get("test-id"); + // ensure the record is the same as when it was added + deepEqual(updatedRecord, newRecord); + await sqliteHandle.close(); + }); + + // test listing records + add_task(async function test_kinto_list() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + let originalRecord = { id: "test-id-1", foo: "bar" }; + let records = await adapter.list(); + Assert.equal(records.length, 1); + await adapter.execute(transaction => transaction.create(originalRecord)); + records = await adapter.list(); + Assert.equal(records.length, 2); + await sqliteHandle.close(); + }); + + // test aborting transaction + add_task(async function test_kinto_aborting_transaction() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + await adapter.clear(); + let record = { id: 1, foo: "bar" }; + let error = null; + try { + await adapter.execute(transaction => { + transaction.create(record); + throw new Error("unexpected"); + }); + } catch (e) { + error = e; + } + Assert.notEqual(error, null); + let records = await adapter.list(); + Assert.equal(records.length, 0); + await sqliteHandle.close(); + }); + + // test save and get last modified + add_task(async function test_kinto_last_modified() { + const initialValue = 0; + const intendedValue = 12345678; + + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + let lastModified = await adapter.getLastModified(); + Assert.equal(lastModified, initialValue); + let result = await adapter.saveLastModified(intendedValue); + Assert.equal(result, intendedValue); + lastModified = await adapter.getLastModified(); + Assert.equal(lastModified, intendedValue); + + // test saveLastModified parses values correctly + result = await adapter.saveLastModified(" " + intendedValue + " blah"); + // should resolve with the parsed int + Assert.equal(result, intendedValue); + // and should have saved correctly + lastModified = await adapter.getLastModified(); + Assert.equal(lastModified, intendedValue); + await sqliteHandle.close(); + }); + + // test loadDump(records) + add_task(async function test_kinto_import_records() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + let record1 = { id: 1, foo: "bar" }; + let record2 = { id: 2, foo: "baz" }; + let impactedRecords = await adapter.loadDump([record1, record2]); + Assert.equal(impactedRecords.length, 2); + let newRecord1 = await adapter.get("1"); + // ensure the record is the same as when it was added + deepEqual(record1, newRecord1); + let newRecord2 = await adapter.get("2"); + // ensure the record is the same as when it was added + deepEqual(record2, newRecord2); + await sqliteHandle.close(); + }); + + add_task(async function test_kinto_import_records_should_override_existing() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + await adapter.clear(); + let records = await adapter.list(); + Assert.equal(records.length, 0); + let impactedRecords = await adapter.loadDump([ + { id: 1, foo: "bar" }, + { id: 2, foo: "baz" }, + ]); + Assert.equal(impactedRecords.length, 2); + await adapter.loadDump([ + { id: 1, foo: "baz" }, + { id: 3, foo: "bab" }, + ]); + records = await adapter.list(); + Assert.equal(records.length, 3); + let newRecord1 = await adapter.get("1"); + deepEqual(newRecord1.foo, "baz"); + await sqliteHandle.close(); + }); + + add_task(async function test_import_updates_lastModified() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + await adapter.loadDump([ + { id: 1, foo: "bar", last_modified: 1457896541 }, + { id: 2, foo: "baz", last_modified: 1458796542 }, + ]); + let lastModified = await adapter.getLastModified(); + Assert.equal(lastModified, 1458796542); + await sqliteHandle.close(); + }); + + add_task(async function test_import_preserves_older_lastModified() { + let sqliteHandle = await do_get_kinto_connection(); + let adapter = do_get_kinto_adapter(sqliteHandle); + await adapter.saveLastModified(1458796543); + + await adapter.loadDump([ + { id: 1, foo: "bar", last_modified: 1457896541 }, + { id: 2, foo: "baz", last_modified: 1458796542 }, + ]); + let lastModified = await adapter.getLastModified(); + Assert.equal(lastModified, 1458796543); + await sqliteHandle.close(); + }); + + add_task(async function test_save_metadata_preserves_lastModified() { + let sqliteHandle = await do_get_kinto_connection(); + + let adapter = do_get_kinto_adapter(sqliteHandle); + await adapter.saveLastModified(42); + + await adapter.saveMetadata({ id: "col" }); + + let lastModified = await adapter.getLastModified(); + Assert.equal(lastModified, 42); + await sqliteHandle.close(); + }); +} + +// test kinto db setup and operations in various scenarios +// test from scratch - no current existing database +add_test(function test_db_creation() { + add_test(function test_create_from_scratch() { + // ensure the file does not exist in the profile + let kintoDB = do_get_kinto_db(); + Assert.ok(!kintoDB.exists()); + run_next_test(); + }); + + test_collection_operations(); + + cleanup_kinto(); + run_next_test(); +}); + +// this is the closest we can get to a schema version upgrade at v1 - test an +// existing database +add_test(function test_creation_from_empty_db() { + add_test(function test_create_from_empty_db() { + // place an empty kinto db file in the profile + let profile = do_get_profile(); + + let emptyDB = do_get_file("test_storage_adapter/empty.sqlite"); + emptyDB.copyTo(profile, kintoFilename); + + run_next_test(); + }); + + test_collection_operations(); + + cleanup_kinto(); + run_next_test(); +}); + +// test schema version upgrade at v2 +add_test(function test_migration_from_v1_to_v2() { + add_test(function test_migrate_from_v1_to_v2() { + // place an empty kinto db file in the profile + let profile = do_get_profile(); + + let v1DB = do_get_file("test_storage_adapter/v1.sqlite"); + v1DB.copyTo(profile, kintoFilename); + + run_next_test(); + }); + + add_test(async function schema_is_update_from_1_to_2() { + // The `v1.sqlite` has schema version 1. + let sqliteHandle = await Sqlite.openConnection({ path: kintoFilename }); + Assert.equal(await sqliteHandle.getSchemaVersion(), 1); + await sqliteHandle.close(); + + // The `.openConnection()` migrates it to version 2. + sqliteHandle = await FirefoxAdapter.openConnection({ path: kintoFilename }); + Assert.equal(await sqliteHandle.getSchemaVersion(), 2); + await sqliteHandle.close(); + + run_next_test(); + }); + + test_collection_operations(); + + cleanup_kinto(); + run_next_test(); +}); diff --git a/services/common/tests/unit/test_storage_adapter/empty.sqlite b/services/common/tests/unit/test_storage_adapter/empty.sqlite new file mode 100644 index 0000000000..7f295b4146 Binary files /dev/null and b/services/common/tests/unit/test_storage_adapter/empty.sqlite differ diff --git a/services/common/tests/unit/test_storage_adapter/v1.sqlite b/services/common/tests/unit/test_storage_adapter/v1.sqlite new file mode 100644 index 0000000000..8482b8b31d Binary files /dev/null and b/services/common/tests/unit/test_storage_adapter/v1.sqlite differ diff --git a/services/common/tests/unit/test_storage_adapter_shutdown.js b/services/common/tests/unit/test_storage_adapter_shutdown.js new file mode 100644 index 0000000000..dce26ce842 --- /dev/null +++ b/services/common/tests/unit/test_storage_adapter_shutdown.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AsyncShutdown } = ChromeUtils.importESModule( + "resource://gre/modules/AsyncShutdown.sys.mjs" +); + +const { FirefoxAdapter } = ChromeUtils.importESModule( + "resource://services-common/kinto-storage-adapter.sys.mjs" +); + +add_task(async function test_sqlite_shutdown() { + const sqliteHandle = await FirefoxAdapter.openConnection({ + path: "kinto.sqlite", + }); + + // Shutdown Sqlite.sys.mjs synchronously. + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + AsyncShutdown.profileBeforeChange._trigger(); + Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); + + try { + sqliteHandle.execute("SELECT 1;"); + equal("Should not succeed, connection should be closed.", false); + } catch (e) { + equal(e.message, "Connection is not open."); + } +}); diff --git a/services/common/tests/unit/test_tokenauthenticatedrequest.js b/services/common/tests/unit/test_tokenauthenticatedrequest.js new file mode 100644 index 0000000000..70735894e4 --- /dev/null +++ b/services/common/tests/unit/test_tokenauthenticatedrequest.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { CryptoUtils } = ChromeUtils.importESModule( + "resource://services-crypto/utils.sys.mjs" +); +const { TokenAuthenticatedRESTRequest } = ChromeUtils.importESModule( + "resource://services-common/rest.sys.mjs" +); + +function run_test() { + initTestLogging("Trace"); + run_next_test(); +} + +add_task(async function test_authenticated_request() { + _("Ensure that sending a MAC authenticated GET request works as expected."); + + let message = "Great Success!"; + + let id = "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x"; + let key = "qTZf4ZFpAMpMoeSsX3zVRjiqmNs="; + let method = "GET"; + + let nonce = btoa(CryptoUtils.generateRandomBytesLegacy(16)); + let ts = Math.floor(Date.now() / 1000); + let extra = { ts, nonce }; + + let auth; + + let server = httpd_setup({ + "/foo": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + Assert.equal(auth, request.getHeader("Authorization")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + let uri = CommonUtils.makeURI(server.baseURI + "/foo"); + let sig = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, extra); + auth = sig.getHeader(); + + let req = new TokenAuthenticatedRESTRequest(uri, { id, key }, extra); + await req.get(); + + Assert.equal(message, req.response.body); + + await promiseStopServer(server); +}); diff --git a/services/common/tests/unit/test_tokenserverclient.js b/services/common/tests/unit/test_tokenserverclient.js new file mode 100644 index 0000000000..c1c47f6e1a --- /dev/null +++ b/services/common/tests/unit/test_tokenserverclient.js @@ -0,0 +1,382 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { + TokenServerClient, + TokenServerClientError, + TokenServerClientServerError, +} = ChromeUtils.importESModule( + "resource://services-common/tokenserverclient.sys.mjs" +); + +initTestLogging("Trace"); + +add_task(async function test_working_token_exchange() { + _("Ensure that working OAuth token exchange works as expected."); + + let service = "http://example.com/foo"; + let duration = 300; + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + Assert.ok(request.hasHeader("accept")); + Assert.equal("application/json", request.getHeader("accept")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + + let body = JSON.stringify({ + id: "id", + key: "key", + api_endpoint: service, + uid: "uid", + duration, + }); + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + let result = await client.getTokenUsingOAuth(url, "access_token"); + Assert.equal("object", typeof result); + do_check_attribute_count(result, 7); + Assert.equal(service, result.endpoint); + Assert.equal("id", result.id); + Assert.equal("key", result.key); + Assert.equal("uid", result.uid); + Assert.equal(duration, result.duration); + Assert.deepEqual(undefined, result.node_type); + await promiseStopServer(server); +}); + +add_task(async function test_working_token_exchange_with_nodetype() { + _("Ensure that a token response with a node type as expected."); + + let service = "http://example.com/foo"; + let duration = 300; + let nodeType = "the-node-type"; + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + Assert.ok(request.hasHeader("accept")); + Assert.equal("application/json", request.getHeader("accept")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + + let body = JSON.stringify({ + id: "id", + key: "key", + api_endpoint: service, + uid: "uid", + duration, + node_type: nodeType, + }); + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + let result = await client.getTokenUsingOAuth(url, "access_token"); + Assert.equal("object", typeof result); + do_check_attribute_count(result, 7); + Assert.equal(service, result.endpoint); + Assert.equal("id", result.id); + Assert.equal("key", result.key); + Assert.equal("uid", result.uid); + Assert.equal(duration, result.duration); + Assert.equal(nodeType, result.node_type); + await promiseStopServer(server); +}); + +add_task(async function test_invalid_arguments() { + _("Ensure invalid arguments to APIs are rejected."); + + let args = [ + [null, "access_token"], + ["http://example.com/", null], + ]; + + for (let arg of args) { + let client = new TokenServerClient(); + await Assert.rejects(client.getTokenUsingOAuth(arg[0], arg[1]), ex => { + Assert.ok(ex instanceof TokenServerClientError); + return true; + }); + } +}); + +add_task(async function test_invalid_403_no_content_type() { + _("Ensure that a 403 without content-type is handled properly."); + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + response.setStatusLine(request.httpVersion, 403, "Forbidden"); + // No Content-Type header by design. + + let body = JSON.stringify({ + errors: [{ description: "irrelevant", location: "body", name: "" }], + urls: { foo: "http://bar" }, + }); + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + + await Assert.rejects( + client.getTokenUsingOAuth(url, "access_token"), + error => { + Assert.ok(error instanceof TokenServerClientServerError); + Assert.equal(error.cause, "malformed-response"); + + Assert.equal(null, error.urls); + return true; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_send_extra_headers() { + _("Ensures that the condition acceptance header is sent when asked."); + + let duration = 300; + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + Assert.ok(request.hasHeader("x-foo")); + Assert.equal(request.getHeader("x-foo"), "42"); + + Assert.ok(request.hasHeader("x-bar")); + Assert.equal(request.getHeader("x-bar"), "17"); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + + let body = JSON.stringify({ + id: "id", + key: "key", + api_endpoint: "http://example.com/", + uid: "uid", + duration, + }); + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + + let extra = { + "X-Foo": 42, + "X-Bar": 17, + }; + + await client.getTokenUsingOAuth(url, "access_token", extra); + // Other tests validate other things. + + await promiseStopServer(server); +}); + +add_task(async function test_error_404_empty() { + _("Ensure that 404 responses without proper response are handled properly."); + + let server = httpd_setup(); + + let client = new TokenServerClient(); + let url = server.baseURI + "/foo"; + + await Assert.rejects( + client.getTokenUsingOAuth(url, "access_token"), + error => { + Assert.ok(error instanceof TokenServerClientServerError); + Assert.equal(error.cause, "malformed-response"); + + Assert.notEqual(null, error.response); + return true; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_error_404_proper_response() { + _("Ensure that a Cornice error report for 404 is handled properly."); + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "application/json; charset=utf-8"); + + let body = JSON.stringify({ + status: 404, + errors: [{ description: "No service", location: "body", name: "" }], + }); + + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + + await Assert.rejects( + client.getTokenUsingOAuth(url, "access_token"), + error => { + Assert.ok(error instanceof TokenServerClientServerError); + Assert.equal(error.cause, "unknown-service"); + return true; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_bad_json() { + _("Ensure that malformed JSON is handled properly."); + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + + let body = '{"id": "id", baz}'; + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + + await Assert.rejects( + client.getTokenUsingOAuth(url, "access_token"), + error => { + Assert.notEqual(null, error); + Assert.equal("TokenServerClientServerError", error.name); + Assert.equal(error.cause, "malformed-response"); + Assert.notEqual(null, error.response); + return true; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_400_response() { + _("Ensure HTTP 400 is converted to malformed-request."); + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + response.setHeader("Content-Type", "application/json; charset=utf-8"); + + let body = "{}"; // Actual content may not be used. + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + + await Assert.rejects( + client.getTokenUsingOAuth(url, "access_token"), + error => { + Assert.notEqual(null, error); + Assert.equal("TokenServerClientServerError", error.name); + Assert.notEqual(null, error.response); + Assert.equal(error.cause, "malformed-request"); + return true; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_401_with_error_cause() { + _("Ensure 401 cause is specified in body.status"); + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.setHeader("Content-Type", "application/json; charset=utf-8"); + + let body = JSON.stringify({ status: "no-soup-for-you" }); + response.bodyOutputStream.write(body, body.length); + }, + }); + + let client = new TokenServerClient(); + let url = server.baseURI + "/1.0/foo/1.0"; + + await Assert.rejects( + client.getTokenUsingOAuth(url, "access_token"), + error => { + Assert.notEqual(null, error); + Assert.equal("TokenServerClientServerError", error.name); + Assert.notEqual(null, error.response); + Assert.equal(error.cause, "no-soup-for-you"); + return true; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_unhandled_media_type() { + _("Ensure that unhandled media types throw an error."); + + let server = httpd_setup({ + "/1.0/foo/1.0": function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain"); + + let body = "hello, world"; + response.bodyOutputStream.write(body, body.length); + }, + }); + + let url = server.baseURI + "/1.0/foo/1.0"; + let client = new TokenServerClient(); + + await Assert.rejects( + client.getTokenUsingOAuth(url, "access_token"), + error => { + Assert.notEqual(null, error); + Assert.equal("TokenServerClientServerError", error.name); + Assert.notEqual(null, error.response); + return true; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_rich_media_types() { + _("Ensure that extra tokens in the media type aren't rejected."); + + let duration = 300; + let server = httpd_setup({ + "/foo": function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json; foo=bar; bar=foo"); + + let body = JSON.stringify({ + id: "id", + key: "key", + api_endpoint: "foo", + uid: "uid", + duration, + }); + response.bodyOutputStream.write(body, body.length); + }, + }); + + let url = server.baseURI + "/foo"; + let client = new TokenServerClient(); + + await client.getTokenUsingOAuth(url, "access_token"); + await promiseStopServer(server); +}); diff --git a/services/common/tests/unit/test_uptake_telemetry.js b/services/common/tests/unit/test_uptake_telemetry.js new file mode 100644 index 0000000000..24967b641f --- /dev/null +++ b/services/common/tests/unit/test_uptake_telemetry.js @@ -0,0 +1,121 @@ +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); +const { UptakeTelemetry } = ChromeUtils.importESModule( + "resource://services-common/uptake-telemetry.sys.mjs" +); + +const COMPONENT = "remotesettings"; + +async function withFakeClientID(uuid, f) { + const { Policy } = ChromeUtils.importESModule( + "resource://services-common/uptake-telemetry.sys.mjs" + ); + let oldGetClientID = Policy.getClientID; + Policy._clientIDHash = null; + Policy.getClientID = () => Promise.resolve(uuid); + try { + return await f(); + } finally { + Policy.getClientID = oldGetClientID; + } +} + +add_task(async function test_unknown_status_is_not_reported() { + const source = "update-source"; + const startSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + + try { + await UptakeTelemetry.report(COMPONENT, "unknown-status", { source }); + } catch (e) {} + + const endSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + Assert.deepEqual(startSnapshot, endSnapshot); +}); + +add_task(async function test_age_is_converted_to_string_and_reported() { + const status = UptakeTelemetry.STATUS.SUCCESS; + const age = 42; + + await withFakeChannel("nightly", async () => { + await UptakeTelemetry.report(COMPONENT, status, { source: "s", age }); + }); + + TelemetryTestUtils.assertEvents([ + [ + "uptake.remotecontent.result", + "uptake", + COMPONENT, + status, + { source: "s", age: `${age}` }, + ], + ]); +}); + +add_task(async function test_each_status_can_be_caught_in_snapshot() { + const source = "some-source"; + const startSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + + const expectedIncrements = {}; + await withFakeChannel("nightly", async () => { + for (const status of Object.values(UptakeTelemetry.STATUS)) { + expectedIncrements[status] = 1; + await UptakeTelemetry.report(COMPONENT, status, { source }); + } + }); + + const endSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); +}); + +add_task(async function test_events_are_sent_when_hash_is_mod_0() { + const source = "some-source"; + const startSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + const startSuccess = startSnapshot.success || 0; + const uuid = "d81bbfad-d741-41f5-a7e6-29f6bde4972a"; // hash % 100 = 0 + await withFakeClientID(uuid, async () => { + await withFakeChannel("release", async () => { + await UptakeTelemetry.report(COMPONENT, UptakeTelemetry.STATUS.SUCCESS, { + source, + }); + }); + }); + const endSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + Assert.equal(endSnapshot.success, startSuccess + 1); +}); + +add_task( + async function test_events_are_not_sent_when_hash_is_greater_than_pref() { + const source = "some-source"; + const startSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + const startSuccess = startSnapshot.success || 0; + const uuid = "d81bbfad-d741-41f5-a7e6-29f6bde49721"; // hash % 100 = 1 + await withFakeClientID(uuid, async () => { + await withFakeChannel("release", async () => { + await UptakeTelemetry.report( + COMPONENT, + UptakeTelemetry.STATUS.SUCCESS, + { source } + ); + }); + }); + const endSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + Assert.equal(endSnapshot.success || 0, startSuccess); + } +); + +add_task(async function test_events_are_sent_when_nightly() { + const source = "some-source"; + const startSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + const startSuccess = startSnapshot.success || 0; + const uuid = "d81bbfad-d741-41f5-a7e6-29f6bde49721"; // hash % 100 = 1 + await withFakeClientID(uuid, async () => { + await withFakeChannel("nightly", async () => { + await UptakeTelemetry.report(COMPONENT, UptakeTelemetry.STATUS.SUCCESS, { + source, + }); + }); + }); + const endSnapshot = getUptakeTelemetrySnapshot(COMPONENT, source); + Assert.equal(endSnapshot.success, startSuccess + 1); +}); diff --git a/services/common/tests/unit/test_utils_atob.js b/services/common/tests/unit/test_utils_atob.js new file mode 100644 index 0000000000..9e24b56f6f --- /dev/null +++ b/services/common/tests/unit/test_utils_atob.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + let data = ["Zm9vYmE=", "Zm9vYmE==", "Zm9vYmE==="]; + for (let d in data) { + Assert.equal(CommonUtils.safeAtoB(data[d]), "fooba"); + } +} diff --git a/services/common/tests/unit/test_utils_convert_string.js b/services/common/tests/unit/test_utils_convert_string.js new file mode 100644 index 0000000000..fcfd874994 --- /dev/null +++ b/services/common/tests/unit/test_utils_convert_string.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// A wise line of Greek verse, and the utf-8 byte encoding. +// N.b., Greek begins at utf-8 ce 91 +const TEST_STR = "πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα"; +const TEST_HEX = h( + "cf 80 cf 8c ce bb ce bb 27 20 ce bf e1 bc b6 ce" + + "b4 27 20 e1 bc 80 ce bb cf 8e cf 80 ce b7 ce be" + + "2c 20 e1 bc 80 ce bb ce bb 27 20 e1 bc 90 cf 87" + + "e1 bf 96 ce bd ce bf cf 82 20 e1 bc 93 ce bd 20" + + "ce bc ce ad ce b3 ce b1" +); +// Integer byte values for the above +const TEST_BYTES = [ + 207, 128, 207, 140, 206, 187, 206, 187, 39, 32, 206, 191, 225, 188, 182, 206, + 180, 39, 32, 225, 188, 128, 206, 187, 207, 142, 207, 128, 206, 183, 206, 190, + 44, 32, 225, 188, 128, 206, 187, 206, 187, 39, 32, 225, 188, 144, 207, 135, + 225, 191, 150, 206, 189, 206, 191, 207, 130, 32, 225, 188, 147, 206, 189, 32, + 206, 188, 206, 173, 206, 179, 206, 177, +]; + +add_test(function test_compress_string() { + const INPUT = "hello"; + + let result = CommonUtils.convertString(INPUT, "uncompressed", "deflate"); + Assert.equal(result.length, 13); + + let result2 = CommonUtils.convertString(INPUT, "uncompressed", "deflate"); + Assert.equal(result, result2); + + let result3 = CommonUtils.convertString(result, "deflate", "uncompressed"); + Assert.equal(result3, INPUT); + + run_next_test(); +}); + +add_test(function test_compress_utf8() { + const INPUT = + "Árvíztűrő tükörfúrógép いろはにほへとちりぬるを Pijamalı hasta, yağız şoföre çabucak güvendi."; + let inputUTF8 = CommonUtils.encodeUTF8(INPUT); + + let compressed = CommonUtils.convertString( + inputUTF8, + "uncompressed", + "deflate" + ); + let uncompressed = CommonUtils.convertString( + compressed, + "deflate", + "uncompressed" + ); + + Assert.equal(uncompressed, inputUTF8); + + let outputUTF8 = CommonUtils.decodeUTF8(uncompressed); + Assert.equal(outputUTF8, INPUT); + + run_next_test(); +}); + +add_test(function test_bad_argument() { + let failed = false; + try { + CommonUtils.convertString(null, "uncompressed", "deflate"); + } catch (ex) { + failed = true; + Assert.ok(ex.message.startsWith("Input string must be defined")); + } finally { + Assert.ok(failed); + } + + run_next_test(); +}); + +add_task(function test_stringAsHex() { + Assert.equal(TEST_HEX, CommonUtils.stringAsHex(TEST_STR)); +}); + +add_task(function test_hexAsString() { + Assert.equal(TEST_STR, CommonUtils.hexAsString(TEST_HEX)); +}); + +add_task(function test_hexToBytes() { + let bytes = CommonUtils.hexToBytes(TEST_HEX); + Assert.equal(TEST_BYTES.length, bytes.length); + // Ensure that the decimal values of each byte are correct + Assert.ok(arraysEqual(TEST_BYTES, CommonUtils.stringToByteArray(bytes))); +}); + +add_task(function test_bytesToHex() { + // Create a list of our character bytes from the reference int values + let bytes = CommonUtils.byteArrayToString(TEST_BYTES); + Assert.equal(TEST_HEX, CommonUtils.bytesAsHex(bytes)); +}); + +add_task(function test_stringToBytes() { + Assert.ok( + arraysEqual( + TEST_BYTES, + CommonUtils.stringToByteArray(CommonUtils.stringToBytes(TEST_STR)) + ) + ); +}); + +add_task(function test_stringRoundTrip() { + Assert.equal( + TEST_STR, + CommonUtils.hexAsString(CommonUtils.stringAsHex(TEST_STR)) + ); +}); + +add_task(function test_hexRoundTrip() { + Assert.equal( + TEST_HEX, + CommonUtils.stringAsHex(CommonUtils.hexAsString(TEST_HEX)) + ); +}); + +add_task(function test_byteArrayRoundTrip() { + Assert.ok( + arraysEqual( + TEST_BYTES, + CommonUtils.stringToByteArray(CommonUtils.byteArrayToString(TEST_BYTES)) + ) + ); +}); + +// turn formatted test vectors into normal hex strings +function h(hexStr) { + return hexStr.replace(/\s+/g, ""); +} + +function arraysEqual(a1, a2) { + if (a1.length !== a2.length) { + return false; + } + for (let i = 0; i < a1.length; i++) { + if (a1[i] !== a2[i]) { + return false; + } + } + return true; +} diff --git a/services/common/tests/unit/test_utils_dateprefs.js b/services/common/tests/unit/test_utils_dateprefs.js new file mode 100644 index 0000000000..35b0c26470 --- /dev/null +++ b/services/common/tests/unit/test_utils_dateprefs.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var prefs = Services.prefs.getBranch("servicescommon.tests."); + +function DummyLogger() { + this.messages = []; +} +DummyLogger.prototype.warn = function warn(message) { + this.messages.push(message); +}; + +add_test(function test_set_basic() { + let now = new Date(); + + CommonUtils.setDatePref(prefs, "test00", now); + let value = prefs.getStringPref("test00"); + Assert.equal(value, "" + now.getTime()); + + let now2 = CommonUtils.getDatePref(prefs, "test00"); + + Assert.equal(now.getTime(), now2.getTime()); + + run_next_test(); +}); + +add_test(function test_set_bounds_checking() { + let d = new Date(2342354); + + let failed = false; + try { + CommonUtils.setDatePref(prefs, "test01", d); + } catch (ex) { + Assert.ok(ex.message.startsWith("Trying to set")); + failed = true; + } + + Assert.ok(failed); + run_next_test(); +}); + +add_test(function test_get_bounds_checking() { + prefs.setStringPref("test_bounds_checking", "13241431"); + + let log = new DummyLogger(); + let d = CommonUtils.getDatePref(prefs, "test_bounds_checking", 0, log); + Assert.equal(d.getTime(), 0); + Assert.equal(log.messages.length, 1); + + run_next_test(); +}); + +add_test(function test_get_bad_default() { + let failed = false; + try { + CommonUtils.getDatePref(prefs, "get_bad_default", new Date()); + } catch (ex) { + Assert.ok(ex.message.startsWith("Default value is not a number")); + failed = true; + } + + Assert.ok(failed); + run_next_test(); +}); + +add_test(function test_get_invalid_number() { + prefs.setStringPref("get_invalid_number", "hello world"); + + let log = new DummyLogger(); + let d = CommonUtils.getDatePref(prefs, "get_invalid_number", 42, log); + Assert.equal(d.getTime(), 42); + Assert.equal(log.messages.length, 1); + + run_next_test(); +}); diff --git a/services/common/tests/unit/test_utils_encodeBase32.js b/services/common/tests/unit/test_utils_encodeBase32.js new file mode 100644 index 0000000000..3299ac8d05 --- /dev/null +++ b/services/common/tests/unit/test_utils_encodeBase32.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + // Testing byte array manipulation. + Assert.equal( + "FOOBAR", + CommonUtils.byteArrayToString([70, 79, 79, 66, 65, 82]) + ); + Assert.equal("", CommonUtils.byteArrayToString([])); + + _("Testing encoding..."); + // Test vectors from RFC 4648 + Assert.equal(CommonUtils.encodeBase32(""), ""); + Assert.equal(CommonUtils.encodeBase32("f"), "MY======"); + Assert.equal(CommonUtils.encodeBase32("fo"), "MZXQ===="); + Assert.equal(CommonUtils.encodeBase32("foo"), "MZXW6==="); + Assert.equal(CommonUtils.encodeBase32("foob"), "MZXW6YQ="); + Assert.equal(CommonUtils.encodeBase32("fooba"), "MZXW6YTB"); + Assert.equal(CommonUtils.encodeBase32("foobar"), "MZXW6YTBOI======"); + + Assert.equal( + CommonUtils.encodeBase32("Bacon is a vegetable."), + "IJQWG33OEBUXGIDBEB3GKZ3FORQWE3DFFY======" + ); + + _("Checking assumptions..."); + for (let i = 0; i <= 255; ++i) { + Assert.equal(undefined | i, i); + } + + _("Testing decoding..."); + Assert.equal(CommonUtils.decodeBase32(""), ""); + Assert.equal(CommonUtils.decodeBase32("MY======"), "f"); + Assert.equal(CommonUtils.decodeBase32("MZXQ===="), "fo"); + Assert.equal(CommonUtils.decodeBase32("MZXW6YTB"), "fooba"); + Assert.equal(CommonUtils.decodeBase32("MZXW6YTBOI======"), "foobar"); + + // Same with incorrect or missing padding. + Assert.equal(CommonUtils.decodeBase32("MZXW6YTBOI=="), "foobar"); + Assert.equal(CommonUtils.decodeBase32("MZXW6YTBOI"), "foobar"); + + let encoded = CommonUtils.encodeBase32("Bacon is a vegetable."); + _("Encoded to " + JSON.stringify(encoded)); + Assert.equal(CommonUtils.decodeBase32(encoded), "Bacon is a vegetable."); + + // Test failure. + let err; + try { + CommonUtils.decodeBase32("000"); + } catch (ex) { + err = ex; + } + Assert.equal(err.message, "Unknown character in base32: 0"); +} diff --git a/services/common/tests/unit/test_utils_encodeBase64URL.js b/services/common/tests/unit/test_utils_encodeBase64URL.js new file mode 100644 index 0000000000..84d98b06f1 --- /dev/null +++ b/services/common/tests/unit/test_utils_encodeBase64URL.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_test(function test_simple() { + let expected = { + hello: "aGVsbG8=", + "<>?": "PD4_", + }; + + for (let [k, v] of Object.entries(expected)) { + Assert.equal(CommonUtils.encodeBase64URL(k), v); + } + + run_next_test(); +}); + +add_test(function test_no_padding() { + Assert.equal(CommonUtils.encodeBase64URL("hello", false), "aGVsbG8"); + + run_next_test(); +}); diff --git a/services/common/tests/unit/test_utils_ensureMillisecondsTimestamp.js b/services/common/tests/unit/test_utils_ensureMillisecondsTimestamp.js new file mode 100644 index 0000000000..f334364b2d --- /dev/null +++ b/services/common/tests/unit/test_utils_ensureMillisecondsTimestamp.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + Assert.equal(null, CommonUtils.ensureMillisecondsTimestamp(null)); + Assert.equal(null, CommonUtils.ensureMillisecondsTimestamp(0)); + Assert.equal(null, CommonUtils.ensureMillisecondsTimestamp("0")); + Assert.equal(null, CommonUtils.ensureMillisecondsTimestamp("000")); + + Assert.equal( + null, + CommonUtils.ensureMillisecondsTimestamp(999 * 10000000000) + ); + + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp(-1); + }); + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp(1); + }); + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp(1.5); + }); + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp(999 * 10000000000 + 0.5); + }); + + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp("-1"); + }); + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp("1"); + }); + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp("1.5"); + }); + do_check_throws(function err() { + CommonUtils.ensureMillisecondsTimestamp("" + (999 * 10000000000 + 0.5)); + }); +} diff --git a/services/common/tests/unit/test_utils_makeURI.js b/services/common/tests/unit/test_utils_makeURI.js new file mode 100644 index 0000000000..d6d7e19db9 --- /dev/null +++ b/services/common/tests/unit/test_utils_makeURI.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +_("Make sure uri strings are converted to nsIURIs"); + +function run_test() { + _test_makeURI(); +} + +function _test_makeURI() { + _("Check http uris"); + let uri1 = "http://mozillalabs.com/"; + Assert.equal(CommonUtils.makeURI(uri1).spec, uri1); + let uri2 = "http://www.mozillalabs.com/"; + Assert.equal(CommonUtils.makeURI(uri2).spec, uri2); + let uri3 = "http://mozillalabs.com/path"; + Assert.equal(CommonUtils.makeURI(uri3).spec, uri3); + let uri4 = "http://mozillalabs.com/multi/path"; + Assert.equal(CommonUtils.makeURI(uri4).spec, uri4); + let uri5 = "http://mozillalabs.com/?query"; + Assert.equal(CommonUtils.makeURI(uri5).spec, uri5); + let uri6 = "http://mozillalabs.com/#hash"; + Assert.equal(CommonUtils.makeURI(uri6).spec, uri6); + + _("Check https uris"); + let uris1 = "https://mozillalabs.com/"; + Assert.equal(CommonUtils.makeURI(uris1).spec, uris1); + let uris2 = "https://www.mozillalabs.com/"; + Assert.equal(CommonUtils.makeURI(uris2).spec, uris2); + let uris3 = "https://mozillalabs.com/path"; + Assert.equal(CommonUtils.makeURI(uris3).spec, uris3); + let uris4 = "https://mozillalabs.com/multi/path"; + Assert.equal(CommonUtils.makeURI(uris4).spec, uris4); + let uris5 = "https://mozillalabs.com/?query"; + Assert.equal(CommonUtils.makeURI(uris5).spec, uris5); + let uris6 = "https://mozillalabs.com/#hash"; + Assert.equal(CommonUtils.makeURI(uris6).spec, uris6); + + _("Check chrome uris"); + let uric1 = "chrome://browser/content/browser.xhtml"; + Assert.equal(CommonUtils.makeURI(uric1).spec, uric1); + let uric2 = "chrome://browser/skin/browser.css"; + Assert.equal(CommonUtils.makeURI(uric2).spec, uric2); + + _("Check about uris"); + let uria1 = "about:weave"; + Assert.equal(CommonUtils.makeURI(uria1).spec, uria1); + let uria2 = "about:weave/"; + Assert.equal(CommonUtils.makeURI(uria2).spec, uria2); + let uria3 = "about:weave/path"; + Assert.equal(CommonUtils.makeURI(uria3).spec, uria3); + let uria4 = "about:weave/multi/path"; + Assert.equal(CommonUtils.makeURI(uria4).spec, uria4); + let uria5 = "about:weave/?query"; + Assert.equal(CommonUtils.makeURI(uria5).spec, uria5); + let uria6 = "about:weave/#hash"; + Assert.equal(CommonUtils.makeURI(uria6).spec, uria6); + + _("Invalid uris are undefined"); + Assert.equal(CommonUtils.makeURI("mozillalabs.com"), undefined); + Assert.equal(CommonUtils.makeURI("this is a test"), undefined); +} diff --git a/services/common/tests/unit/test_utils_namedTimer.js b/services/common/tests/unit/test_utils_namedTimer.js new file mode 100644 index 0000000000..47ac0b9dc1 --- /dev/null +++ b/services/common/tests/unit/test_utils_namedTimer.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_test(function test_required_args() { + try { + CommonUtils.namedTimer(function callback() { + do_throw("Shouldn't fire."); + }, 0); + do_throw("Should have thrown!"); + } catch (ex) { + run_next_test(); + } +}); + +add_test(function test_simple() { + _("Test basic properties of CommonUtils.namedTimer."); + + const delay = 200; + let that = {}; + let t0 = Date.now(); + CommonUtils.namedTimer( + function callback(timer) { + Assert.equal(this, that); + Assert.equal(this._zetimer, null); + Assert.ok(timer instanceof Ci.nsITimer); + // Difference should be ~delay, but hard to predict on all platforms, + // particularly Windows XP. + Assert.ok(Date.now() > t0); + run_next_test(); + }, + delay, + that, + "_zetimer" + ); +}); + +add_test(function test_delay() { + _("Test delaying a timer that hasn't fired yet."); + + const delay = 100; + let that = {}; + let t0 = Date.now(); + function callback(timer) { + // Difference should be ~2*delay, but hard to predict on all platforms, + // particularly Windows XP. + Assert.ok(Date.now() - t0 > delay); + run_next_test(); + } + CommonUtils.namedTimer(callback, delay, that, "_zetimer"); + CommonUtils.namedTimer(callback, 2 * delay, that, "_zetimer"); + run_next_test(); +}); + +add_test(function test_clear() { + _("Test clearing a timer that hasn't fired yet."); + + const delay = 0; + let that = {}; + CommonUtils.namedTimer( + function callback(timer) { + do_throw("Shouldn't fire!"); + }, + delay, + that, + "_zetimer" + ); + + that._zetimer.clear(); + Assert.equal(that._zetimer, null); + CommonUtils.nextTick(run_next_test); + + run_next_test(); +}); diff --git a/services/common/tests/unit/test_utils_sets.js b/services/common/tests/unit/test_utils_sets.js new file mode 100644 index 0000000000..c15d48528f --- /dev/null +++ b/services/common/tests/unit/test_utils_sets.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const EMPTY = new Set(); +const A = new Set(["a"]); +const ABC = new Set(["a", "b", "c"]); +const ABCD = new Set(["a", "b", "c", "d"]); +const BC = new Set(["b", "c"]); +const BCD = new Set(["b", "c", "d"]); +const FGH = new Set(["f", "g", "h"]); +const BCDFGH = new Set(["b", "c", "d", "f", "g", "h"]); + +var union = CommonUtils.union; +var difference = CommonUtils.difference; +var intersection = CommonUtils.intersection; +var setEqual = CommonUtils.setEqual; + +function do_check_setEqual(a, b) { + Assert.ok(setEqual(a, b)); +} + +function do_check_not_setEqual(a, b) { + Assert.ok(!setEqual(a, b)); +} + +add_test(function test_setEqual() { + do_check_setEqual(EMPTY, EMPTY); + do_check_setEqual(EMPTY, new Set()); + do_check_setEqual(A, A); + do_check_setEqual(A, new Set(["a"])); + do_check_setEqual(new Set(["a"]), A); + do_check_not_setEqual(A, EMPTY); + do_check_not_setEqual(EMPTY, A); + do_check_not_setEqual(ABC, A); + run_next_test(); +}); + +add_test(function test_union() { + do_check_setEqual(EMPTY, union(EMPTY, EMPTY)); + do_check_setEqual(ABC, union(EMPTY, ABC)); + do_check_setEqual(ABC, union(ABC, ABC)); + do_check_setEqual(ABCD, union(ABC, BCD)); + do_check_setEqual(ABCD, union(BCD, ABC)); + do_check_setEqual(BCDFGH, union(BCD, FGH)); + run_next_test(); +}); + +add_test(function test_difference() { + do_check_setEqual(EMPTY, difference(EMPTY, EMPTY)); + do_check_setEqual(EMPTY, difference(EMPTY, A)); + do_check_setEqual(EMPTY, difference(A, A)); + do_check_setEqual(ABC, difference(ABC, EMPTY)); + do_check_setEqual(ABC, difference(ABC, FGH)); + do_check_setEqual(A, difference(ABC, BCD)); + run_next_test(); +}); + +add_test(function test_intersection() { + do_check_setEqual(EMPTY, intersection(EMPTY, EMPTY)); + do_check_setEqual(EMPTY, intersection(ABC, EMPTY)); + do_check_setEqual(EMPTY, intersection(ABC, FGH)); + do_check_setEqual(BC, intersection(ABC, BCD)); + run_next_test(); +}); diff --git a/services/common/tests/unit/test_utils_utf8.js b/services/common/tests/unit/test_utils_utf8.js new file mode 100644 index 0000000000..aa075873b9 --- /dev/null +++ b/services/common/tests/unit/test_utils_utf8.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + let str = "Umlaute: \u00FC \u00E4\n"; // Umlaute: ü ä + let encoded = CommonUtils.encodeUTF8(str); + let decoded = CommonUtils.decodeUTF8(encoded); + Assert.equal(decoded, str); +} diff --git a/services/common/tests/unit/test_utils_uuid.js b/services/common/tests/unit/test_utils_uuid.js new file mode 100644 index 0000000000..33b407e92d --- /dev/null +++ b/services/common/tests/unit/test_utils_uuid.js @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function run_test() { + let uuid = CommonUtils.generateUUID(); + Assert.equal(uuid.length, 36); + Assert.equal(uuid[8], "-"); + + run_next_test(); +} diff --git a/services/common/tests/unit/xpcshell.toml b/services/common/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..e4035f66b2 --- /dev/null +++ b/services/common/tests/unit/xpcshell.toml @@ -0,0 +1,63 @@ +[DEFAULT] +head = "head_global.js head_helpers.js head_http.js" +firefox-appdir = "browser" +support-files = ["test_storage_adapter/**"] + +# Test load modules first so syntax failures are caught early. + +["test_async_chain.js"] + +["test_async_foreach.js"] + +["test_hawkclient.js"] +skip-if = ["os == 'android'"] + +["test_hawkrequest.js"] +skip-if = ["os == 'android'"] + +["test_kinto.js"] +tags = "blocklist" + +["test_load_modules.js"] + +["test_logmanager.js"] + +["test_observers.js"] + +["test_restrequest.js"] + +["test_storage_adapter.js"] +tags = "remote-settings blocklist" + +["test_storage_adapter_shutdown.js"] +tags = "remote-settings blocklist" + +["test_tokenauthenticatedrequest.js"] + +["test_tokenserverclient.js"] +skip-if = ["os == 'android'"] + +["test_uptake_telemetry.js"] +tags = "remote-settings" + +["test_utils_atob.js"] + +["test_utils_convert_string.js"] + +["test_utils_dateprefs.js"] + +["test_utils_encodeBase32.js"] + +["test_utils_encodeBase64URL.js"] + +["test_utils_ensureMillisecondsTimestamp.js"] + +["test_utils_makeURI.js"] + +["test_utils_namedTimer.js"] + +["test_utils_sets.js"] + +["test_utils_utf8.js"] + +["test_utils_uuid.js"] diff --git a/services/common/tokenserverclient.sys.mjs b/services/common/tokenserverclient.sys.mjs new file mode 100644 index 0000000000..c23e3d779c --- /dev/null +++ b/services/common/tokenserverclient.sys.mjs @@ -0,0 +1,392 @@ +/* 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 { Log } from "resource://gre/modules/Log.sys.mjs"; + +import { RESTRequest } from "resource://services-common/rest.sys.mjs"; +import { Observers } from "resource://services-common/observers.sys.mjs"; + +const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient"; + +/** + * Represents a TokenServerClient error that occurred on the client. + * + * This is the base type for all errors raised by client operations. + * + * @param message + * (string) Error message. + */ +export function TokenServerClientError(message) { + this.name = "TokenServerClientError"; + this.message = message || "Client error."; + // Without explicitly setting .stack, all stacks from these errors will point + // to the "new Error()" call a few lines down, which isn't helpful. + this.stack = Error().stack; +} + +TokenServerClientError.prototype = new Error(); +TokenServerClientError.prototype.constructor = TokenServerClientError; +TokenServerClientError.prototype._toStringFields = function () { + return { message: this.message }; +}; +TokenServerClientError.prototype.toString = function () { + return this.name + "(" + JSON.stringify(this._toStringFields()) + ")"; +}; +TokenServerClientError.prototype.toJSON = function () { + let result = this._toStringFields(); + result.name = this.name; + return result; +}; + +/** + * Represents a TokenServerClient error that occurred in the network layer. + * + * @param error + * The underlying error thrown by the network layer. + */ +export function TokenServerClientNetworkError(error) { + this.name = "TokenServerClientNetworkError"; + this.error = error; + this.stack = Error().stack; +} + +TokenServerClientNetworkError.prototype = new TokenServerClientError(); +TokenServerClientNetworkError.prototype.constructor = + TokenServerClientNetworkError; +TokenServerClientNetworkError.prototype._toStringFields = function () { + return { error: this.error }; +}; + +/** + * Represents a TokenServerClient error that occurred on the server. + * + * This type will be encountered for all non-200 response codes from the + * server. The type of error is strongly enumerated and is stored in the + * `cause` property. This property can have the following string values: + * + * invalid-credentials -- A token could not be obtained because + * the credentials presented by the client were invalid. + * + * unknown-service -- The requested service was not found. + * + * malformed-request -- The server rejected the request because it + * was invalid. If you see this, code in this file is likely wrong. + * + * malformed-response -- The response from the server was not what was + * expected. + * + * general -- A general server error has occurred. Clients should + * interpret this as an opaque failure. + * + * @param message + * (string) Error message. + */ +export function TokenServerClientServerError(message, cause = "general") { + this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues. + this.name = "TokenServerClientServerError"; + this.message = message || "Server error."; + this.cause = cause; + this.stack = Error().stack; +} + +TokenServerClientServerError.prototype = new TokenServerClientError(); +TokenServerClientServerError.prototype.constructor = + TokenServerClientServerError; + +TokenServerClientServerError.prototype._toStringFields = function () { + let fields = { + now: this.now, + message: this.message, + cause: this.cause, + }; + if (this.response) { + fields.response_body = this.response.body; + fields.response_headers = this.response.headers; + fields.response_status = this.response.status; + } + return fields; +}; + +/** + * Represents a client to the Token Server. + * + * http://docs.services.mozilla.com/token/index.html + * + * The Token Server was designed to support obtaining tokens for arbitrary apps by + * constructing URI paths of the form /. In practice this was + * never used and it only supports an value of `sync`, and the API presented + * here reflects that. + * + * Areas to Improve: + * + * - The server sends a JSON response on error. The client does not currently + * parse this. It might be convenient if it did. + * - Currently most non-200 status codes are rolled into one error type. It + * might be helpful if callers had a richer API that communicated who was + * at fault (e.g. differentiating a 503 from a 401). + */ +export function TokenServerClient() { + this._log = Log.repository.getLogger("Services.Common.TokenServerClient"); + this._log.manageLevelFromPref(PREF_LOG_LEVEL); +} + +TokenServerClient.prototype = { + /** + * Logger instance. + */ + _log: null, + + /** + * Obtain a token from a provided OAuth token against a specific URL. + * + * This asynchronously obtains the token. + * It returns a Promise that resolves or rejects: + * + * Rejects with: + * (TokenServerClientError) If no token could be obtained, this + * will be a TokenServerClientError instance describing why. The + * type seen defines the type of error encountered. If an HTTP response + * was seen, a RESTResponse instance will be stored in the `response` + * property of this object. If there was no error and a token is + * available, this will be null. + * + * Resolves with: + * (map) On success, this will be a map containing the results from + * the server. If there was an error, this will be null. The map has the + * following properties: + * + * id (string) HTTP MAC public key identifier. + * key (string) HTTP MAC shared symmetric key. + * endpoint (string) URL where service can be connected to. + * uid (string) user ID for requested service. + * duration (string) the validity duration of the issued token. + * + * Example Usage + * ------------- + * + * let client = new TokenServerClient(); + * let access_token = getOAuthAccessTokenFromSomewhere(); + * let url = "https://token.services.mozilla.com/1.0/sync/2.0"; + * + * try { + * const result = await client.getTokenUsingOAuth(url, access_token); + * let {id, key, uid, endpoint, duration} = result; + * // Do stuff with data and carry on. + * } catch (error) { + * // Handle errors. + * } + * Obtain a token from a provided OAuth token against a specific URL. + * + * @param url + * (string) URL to fetch token from. + * @param oauthToken + * (string) FxA OAuth Token to exchange token for. + * @param addHeaders + * (object) Extra headers for the request. + */ + async getTokenUsingOAuth(url, oauthToken, addHeaders = {}) { + this._log.debug("Beginning OAuth token exchange: " + url); + + if (!oauthToken) { + throw new TokenServerClientError("oauthToken argument is not valid."); + } + + return this._tokenServerExchangeRequest( + url, + `Bearer ${oauthToken}`, + addHeaders + ); + }, + + /** + * Performs the exchange request to the token server to + * produce a token based on the authorizationHeader input. + * + * @param url + * (string) URL to fetch token from. + * @param authorizationHeader + * (string) The auth header string that populates the 'Authorization' header. + * @param addHeaders + * (object) Extra headers for the request. + */ + async _tokenServerExchangeRequest(url, authorizationHeader, addHeaders = {}) { + if (!url) { + throw new TokenServerClientError("url argument is not valid."); + } + + if (!authorizationHeader) { + throw new TokenServerClientError( + "authorizationHeader argument is not valid." + ); + } + + let req = this.newRESTRequest(url); + req.setHeader("Accept", "application/json"); + req.setHeader("Authorization", authorizationHeader); + + for (let header in addHeaders) { + req.setHeader(header, addHeaders[header]); + } + let response; + try { + response = await req.get(); + } catch (err) { + throw new TokenServerClientNetworkError(err); + } + + try { + return this._processTokenResponse(response); + } catch (ex) { + if (ex instanceof TokenServerClientServerError) { + throw ex; + } + this._log.warn("Error processing token server response", ex); + let error = new TokenServerClientError(ex); + error.response = response; + throw error; + } + }, + + /** + * Handler to process token request responses. + * + * @param response + * RESTResponse from token HTTP request. + */ + _processTokenResponse(response) { + this._log.debug("Got token response: " + response.status); + + // Responses should *always* be JSON, even in the case of 4xx and 5xx + // errors. If we don't see JSON, the server is likely very unhappy. + let ct = response.headers["content-type"] || ""; + if (ct != "application/json" && !ct.startsWith("application/json;")) { + this._log.warn("Did not receive JSON response. Misconfigured server?"); + this._log.debug("Content-Type: " + ct); + this._log.debug("Body: " + response.body); + + let error = new TokenServerClientServerError( + "Non-JSON response.", + "malformed-response" + ); + error.response = response; + throw error; + } + + let result; + try { + result = JSON.parse(response.body); + } catch (ex) { + this._log.warn("Invalid JSON returned by server: " + response.body); + let error = new TokenServerClientServerError( + "Malformed JSON.", + "malformed-response" + ); + error.response = response; + throw error; + } + + // Any response status can have X-Backoff or X-Weave-Backoff headers. + this._maybeNotifyBackoff(response, "x-weave-backoff"); + this._maybeNotifyBackoff(response, "x-backoff"); + + // The service shouldn't have any 3xx, so we don't need to handle those. + if (response.status != 200) { + // We /should/ have a Cornice error report in the JSON. We log that to + // help with debugging. + if ("errors" in result) { + // This could throw, but this entire function is wrapped in a try. If + // the server is sending something not an array of objects, it has + // failed to keep its contract with us and there is little we can do. + for (let error of result.errors) { + this._log.info("Server-reported error: " + JSON.stringify(error)); + } + } + + let error = new TokenServerClientServerError(); + error.response = response; + + if (response.status == 400) { + error.message = "Malformed request."; + error.cause = "malformed-request"; + } else if (response.status == 401) { + // Cause can be invalid-credentials, invalid-timestamp, or + // invalid-generation. + error.message = "Authentication failed."; + error.cause = result.status; + } else if (response.status == 404) { + error.message = "Unknown service."; + error.cause = "unknown-service"; + } + + // A Retry-After header should theoretically only appear on a 503, but + // we'll look for it on any error response. + this._maybeNotifyBackoff(response, "retry-after"); + + throw error; + } + + for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) { + if (!(k in result)) { + let error = new TokenServerClientServerError( + "Expected key not present in result: " + k + ); + error.cause = "malformed-response"; + error.response = response; + throw error; + } + } + + this._log.debug("Successful token response"); + return { + id: result.id, + key: result.key, + endpoint: result.api_endpoint, + uid: result.uid, + duration: result.duration, + hashed_fxa_uid: result.hashed_fxa_uid, + node_type: result.node_type, + }; + }, + + /* + * The prefix used for all notifications sent by this module. This + * allows the handler of notifications to be sure they are handling + * notifications for the service they expect. + * + * If not set, no notifications will be sent. + */ + observerPrefix: null, + + // Given an optional header value, notify that a backoff has been requested. + _maybeNotifyBackoff(response, headerName) { + if (!this.observerPrefix) { + return; + } + let headerVal = response.headers[headerName]; + if (!headerVal) { + return; + } + let backoffInterval; + try { + backoffInterval = parseInt(headerVal, 10); + } catch (ex) { + this._log.error( + "TokenServer response had invalid backoff value in '" + + headerName + + "' header: " + + headerVal + ); + return; + } + Observers.notify( + this.observerPrefix + ":backoff:interval", + backoffInterval + ); + }, + + // override points for testing. + newRESTRequest(url) { + return new RESTRequest(url); + }, +}; diff --git a/services/common/uptake-telemetry.sys.mjs b/services/common/uptake-telemetry.sys.mjs new file mode 100644 index 0000000000..9b456b137f --- /dev/null +++ b/services/common/uptake-telemetry.sys.mjs @@ -0,0 +1,193 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ClientID: "resource://gre/modules/ClientID.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => { + return Components.Constructor( + "@mozilla.org/security/hash;1", + "nsICryptoHash", + "initWithString" + ); +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gSampleRate", + "services.common.uptake.sampleRate" +); + +// Telemetry events id (see Events.yaml). +const TELEMETRY_EVENTS_ID = "uptake.remotecontent.result"; + +/** + * A wrapper around certain low-level operations that can be substituted for testing. + */ +export var Policy = { + _clientIDHash: null, + + getClientID() { + return lazy.ClientID.getClientID(); + }, + + /** + * Compute an integer in the range [0, 100) using a hash of the + * client ID. + * + * This is useful for sampling clients when trying to report + * telemetry only for a sample of clients. + */ + async getClientIDHash() { + if (this._clientIDHash === null) { + this._clientIDHash = this._doComputeClientIDHash(); + } + return this._clientIDHash; + }, + + async _doComputeClientIDHash() { + const clientID = await this.getClientID(); + let byteArr = new TextEncoder().encode(clientID); + let hash = new lazy.CryptoHash("sha256"); + hash.update(byteArr, byteArr.length); + const bytes = hash.finish(false); + let rem = 0; + for (let i = 0, len = bytes.length; i < len; i++) { + rem = ((rem << 8) + (bytes[i].charCodeAt(0) & 0xff)) % 100; + } + return rem; + }, + + getChannel() { + return AppConstants.MOZ_UPDATE_CHANNEL; + }, +}; + +/** + * A Telemetry helper to report uptake of remote content. + */ +export class UptakeTelemetry { + /** + * Supported uptake statuses: + * + * - `UP_TO_DATE`: Local content was already up-to-date with remote content. + * - `SUCCESS`: Local content was updated successfully. + * - `BACKOFF`: Remote server asked clients to backoff. + * - `PARSE_ERROR`: Parsing server response has failed. + * - `CONTENT_ERROR`: Server response has unexpected content. + * - `PREF_DISABLED`: Update is disabled in user preferences. + * - `SIGNATURE_ERROR`: Signature verification after diff-based sync has failed. + * - `SIGNATURE_RETRY_ERROR`: Signature verification after full fetch has failed. + * - `CONFLICT_ERROR`: Some remote changes are in conflict with local changes. + * - `CORRUPTION_ERROR`: Error related to corrupted local data. + * - `SYNC_ERROR`: Synchronization of remote changes has failed. + * - `APPLY_ERROR`: Application of changes locally has failed. + * - `SERVER_ERROR`: Server failed to respond. + * - `CERTIFICATE_ERROR`: Server certificate verification has failed. + * - `DOWNLOAD_ERROR`: Data could not be fully retrieved. + * - `TIMEOUT_ERROR`: Server response has timed out. + * - `NETWORK_ERROR`: Communication with server has failed. + * - `NETWORK_OFFLINE_ERROR`: Network not available. + * - `SHUTDOWN_ERROR`: Error occuring during shutdown. + * - `UNKNOWN_ERROR`: Uncategorized error. + * - `CLEANUP_ERROR`: Clean-up of temporary files has failed. + * - `SYNC_BROKEN_ERROR`: Synchronization is broken. + * - `CUSTOM_1_ERROR`: Update source specific error #1. + * - `CUSTOM_2_ERROR`: Update source specific error #2. + * - `CUSTOM_3_ERROR`: Update source specific error #3. + * - `CUSTOM_4_ERROR`: Update source specific error #4. + * - `CUSTOM_5_ERROR`: Update source specific error #5. + * + * @type {Object} + */ + static get STATUS() { + return { + UP_TO_DATE: "up_to_date", + SUCCESS: "success", + BACKOFF: "backoff", + PARSE_ERROR: "parse_error", + CONTENT_ERROR: "content_error", + PREF_DISABLED: "pref_disabled", + SIGNATURE_ERROR: "sign_error", + SIGNATURE_RETRY_ERROR: "sign_retry_error", + CONFLICT_ERROR: "conflict_error", + CORRUPTION_ERROR: "corruption_error", + SYNC_ERROR: "sync_error", + APPLY_ERROR: "apply_error", + SERVER_ERROR: "server_error", + CERTIFICATE_ERROR: "certificate_error", + DOWNLOAD_ERROR: "download_error", + TIMEOUT_ERROR: "timeout_error", + NETWORK_ERROR: "network_error", + NETWORK_OFFLINE_ERROR: "offline_error", + SHUTDOWN_ERROR: "shutdown_error", + UNKNOWN_ERROR: "unknown_error", + CLEANUP_ERROR: "cleanup_error", + SYNC_BROKEN_ERROR: "sync_broken_error", + CUSTOM_1_ERROR: "custom_1_error", + CUSTOM_2_ERROR: "custom_2_error", + CUSTOM_3_ERROR: "custom_3_error", + CUSTOM_4_ERROR: "custom_4_error", + CUSTOM_5_ERROR: "custom_5_error", + }; + } + + static get Policy() { + return Policy; + } + + /** + * Reports the uptake status for the specified source. + * + * @param {string} component the component reporting the uptake (eg. "normandy"). + * @param {string} status the uptake status (eg. "network_error") + * @param {Object} extra extra values to report + * @param {string} extra.source the update source (eg. "recipe-42"). + * @param {string} extra.trigger what triggered the polling/fetching (eg. "broadcast", "timer"). + * @param {int} extra.age age of pulled data in seconds + */ + static async report(component, status, extra = {}) { + const { source } = extra; + + if (!source) { + throw new Error("`source` value is mandatory."); + } + + if (!Object.values(UptakeTelemetry.STATUS).includes(status)) { + throw new Error(`Unknown status '${status}'`); + } + + // Report event for real-time monitoring. See Events.yaml for registration. + // Contrary to histograms, Telemetry Events are not enabled by default. + // Enable them on first call to `report()`. + if (!this._eventsEnabled) { + Services.telemetry.setEventRecordingEnabled(TELEMETRY_EVENTS_ID, true); + this._eventsEnabled = true; + } + + const hash = await UptakeTelemetry.Policy.getClientIDHash(); + const channel = UptakeTelemetry.Policy.getChannel(); + const shouldSendEvent = + !["release", "esr"].includes(channel) || hash < lazy.gSampleRate; + if (shouldSendEvent) { + // The Event API requires `extra` values to be of type string. Force it! + const extraStr = Object.keys(extra).reduce((acc, k) => { + acc[k] = extra[k].toString(); + return acc; + }, {}); + Services.telemetry.recordEvent( + TELEMETRY_EVENTS_ID, + "uptake", + component, + status, + extraStr + ); + } + } +} diff --git a/services/common/utils.sys.mjs b/services/common/utils.sys.mjs new file mode 100644 index 0000000000..3b427c0ad4 --- /dev/null +++ b/services/common/utils.sys.mjs @@ -0,0 +1,693 @@ +/* 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 { Log } from "resource://gre/modules/Log.sys.mjs"; + +export var CommonUtils = { + /* + * Set manipulation methods. These should be lifted into toolkit, or added to + * `Set` itself. + */ + + /** + * Return elements of `a` or `b`. + */ + union(a, b) { + let out = new Set(a); + for (let x of b) { + out.add(x); + } + return out; + }, + + /** + * Return elements of `a` that are not present in `b`. + */ + difference(a, b) { + let out = new Set(a); + for (let x of b) { + out.delete(x); + } + return out; + }, + + /** + * Return elements of `a` that are also in `b`. + */ + intersection(a, b) { + let out = new Set(); + for (let x of a) { + if (b.has(x)) { + out.add(x); + } + } + return out; + }, + + /** + * Return true if `a` and `b` are the same size, and + * every element of `a` is in `b`. + */ + setEqual(a, b) { + if (a.size != b.size) { + return false; + } + for (let x of a) { + if (!b.has(x)) { + return false; + } + } + return true; + }, + + /** + * Checks elements in two arrays for equality, as determined by the `===` + * operator. This function does not perform a deep comparison; see Sync's + * `Util.deepEquals` for that. + */ + arrayEqual(a, b) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; + }, + + /** + * Encode byte string as base64URL (RFC 4648). + * + * @param bytes + * (string) Raw byte string to encode. + * @param pad + * (bool) Whether to include padding characters (=). Defaults + * to true for historical reasons. + */ + encodeBase64URL: function encodeBase64URL(bytes, pad = true) { + let s = btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_"); + + if (!pad) { + return s.replace(/=+$/, ""); + } + + return s; + }, + + /** + * Create a nsIURI instance from a string. + */ + makeURI: function makeURI(URIString) { + if (!URIString) { + return null; + } + try { + return Services.io.newURI(URIString); + } catch (e) { + let log = Log.repository.getLogger("Common.Utils"); + log.debug("Could not create URI", e); + return null; + } + }, + + /** + * Execute a function on the next event loop tick. + * + * @param callback + * Function to invoke. + * @param thisObj [optional] + * Object to bind the callback to. + */ + nextTick: function nextTick(callback, thisObj) { + if (thisObj) { + callback = callback.bind(thisObj); + } + Services.tm.dispatchToMainThread(callback); + }, + + /** + * Return a timer that is scheduled to call the callback after waiting the + * provided time or as soon as possible. The timer will be set as a property + * of the provided object with the given timer name. + */ + namedTimer: function namedTimer(callback, wait, thisObj, name) { + if (!thisObj || !name) { + throw new Error( + "You must provide both an object and a property name for the timer!" + ); + } + + // Delay an existing timer if it exists + if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) { + thisObj[name].delay = wait; + return thisObj[name]; + } + + // Create a special timer that we can add extra properties + let timer = Object.create( + Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer) + ); + + // Provide an easy way to clear out the timer + timer.clear = function () { + thisObj[name] = null; + timer.cancel(); + }; + + // Initialize the timer with a smart callback + timer.initWithCallback( + { + notify: function notify() { + // Clear out the timer once it's been triggered + timer.clear(); + callback.call(thisObj, timer); + }, + }, + wait, + timer.TYPE_ONE_SHOT + ); + + return (thisObj[name] = timer); + }, + + encodeUTF8: function encodeUTF8(str) { + try { + str = this._utf8Converter.ConvertFromUnicode(str); + return str + this._utf8Converter.Finish(); + } catch (ex) { + return null; + } + }, + + decodeUTF8: function decodeUTF8(str) { + try { + str = this._utf8Converter.ConvertToUnicode(str); + return str + this._utf8Converter.Finish(); + } catch (ex) { + return null; + } + }, + + byteArrayToString: function byteArrayToString(bytes) { + return bytes.map(byte => String.fromCharCode(byte)).join(""); + }, + + stringToByteArray: function stringToByteArray(bytesString) { + return Array.prototype.slice.call(bytesString).map(c => c.charCodeAt(0)); + }, + + // A lot of Util methods work with byte strings instead of ArrayBuffers. + // A patch should address this problem, but in the meantime let's provide + // helpers method to convert byte strings to Uint8Array. + byteStringToArrayBuffer(byteString) { + if (byteString === undefined) { + return new Uint8Array(); + } + const bytes = new Uint8Array(byteString.length); + for (let i = 0; i < byteString.length; ++i) { + bytes[i] = byteString.charCodeAt(i) & 0xff; + } + return bytes; + }, + + arrayBufferToByteString(buffer) { + return CommonUtils.byteArrayToString([...buffer]); + }, + + bufferToHex(buffer) { + return Array.prototype.map + .call(buffer, x => ("00" + x.toString(16)).slice(-2)) + .join(""); + }, + + bytesAsHex: function bytesAsHex(bytes) { + let s = ""; + for (let i = 0, len = bytes.length; i < len; i++) { + let c = (bytes[i].charCodeAt(0) & 0xff).toString(16); + if (c.length == 1) { + c = "0" + c; + } + s += c; + } + return s; + }, + + stringAsHex: function stringAsHex(str) { + return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str)); + }, + + stringToBytes: function stringToBytes(str) { + return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str)); + }, + + hexToBytes: function hexToBytes(str) { + let bytes = []; + for (let i = 0; i < str.length - 1; i += 2) { + bytes.push(parseInt(str.substr(i, 2), 16)); + } + return String.fromCharCode.apply(String, bytes); + }, + + hexToArrayBuffer(str) { + const octString = CommonUtils.hexToBytes(str); + return CommonUtils.byteStringToArrayBuffer(octString); + }, + + hexAsString: function hexAsString(hex) { + return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex)); + }, + + base64urlToHex(b64str) { + return CommonUtils.bufferToHex( + new Uint8Array(ChromeUtils.base64URLDecode(b64str, { padding: "reject" })) + ); + }, + + /** + * Base32 encode (RFC 4648) a string + */ + encodeBase32: function encodeBase32(bytes) { + const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + let leftover = bytes.length % 5; + + // Pad the last quantum with zeros so the length is a multiple of 5. + if (leftover) { + for (let i = leftover; i < 5; i++) { + bytes += "\0"; + } + } + + // Chop the string into quanta of 5 bytes (40 bits). Each quantum + // is turned into 8 characters from the 32 character base. + let ret = ""; + for (let i = 0; i < bytes.length; i += 5) { + let c = Array.prototype.slice + .call(bytes.slice(i, i + 5)) + .map(byte => byte.charCodeAt(0)); + ret += + key[c[0] >> 3] + + key[((c[0] << 2) & 0x1f) | (c[1] >> 6)] + + key[(c[1] >> 1) & 0x1f] + + key[((c[1] << 4) & 0x1f) | (c[2] >> 4)] + + key[((c[2] << 1) & 0x1f) | (c[3] >> 7)] + + key[(c[3] >> 2) & 0x1f] + + key[((c[3] << 3) & 0x1f) | (c[4] >> 5)] + + key[c[4] & 0x1f]; + } + + switch (leftover) { + case 1: + return ret.slice(0, -6) + "======"; + case 2: + return ret.slice(0, -4) + "===="; + case 3: + return ret.slice(0, -3) + "==="; + case 4: + return ret.slice(0, -1) + "="; + default: + return ret; + } + }, + + /** + * Base32 decode (RFC 4648) a string. + */ + decodeBase32: function decodeBase32(str) { + const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + let padChar = str.indexOf("="); + let chars = padChar == -1 ? str.length : padChar; + let bytes = Math.floor((chars * 5) / 8); + let blocks = Math.ceil(chars / 8); + + // Process a chunk of 5 bytes / 8 characters. + // The processing of this is known in advance, + // so avoid arithmetic! + function processBlock(ret, cOffset, rOffset) { + let c, val; + + // N.B., this relies on + // undefined | foo == foo. + function accumulate(val) { + ret[rOffset] |= val; + } + + function advance() { + c = str[cOffset++]; + if (!c || c == "" || c == "=") { + // Easier than range checking. + throw new Error("Done"); + } // Will be caught far away. + val = key.indexOf(c); + if (val == -1) { + throw new Error(`Unknown character in base32: ${c}`); + } + } + + // Handle a left shift, restricted to bytes. + function left(octet, shift) { + return (octet << shift) & 0xff; + } + + advance(); + accumulate(left(val, 3)); + advance(); + accumulate(val >> 2); + ++rOffset; + accumulate(left(val, 6)); + advance(); + accumulate(left(val, 1)); + advance(); + accumulate(val >> 4); + ++rOffset; + accumulate(left(val, 4)); + advance(); + accumulate(val >> 1); + ++rOffset; + accumulate(left(val, 7)); + advance(); + accumulate(left(val, 2)); + advance(); + accumulate(val >> 3); + ++rOffset; + accumulate(left(val, 5)); + advance(); + accumulate(val); + ++rOffset; + } + + // Our output. Define to be explicit (and maybe the compiler will be smart). + let ret = new Array(bytes); + let i = 0; + let cOff = 0; + let rOff = 0; + + for (; i < blocks; ++i) { + try { + processBlock(ret, cOff, rOff); + } catch (ex) { + // Handle the detection of padding. + if (ex.message == "Done") { + break; + } + throw ex; + } + cOff += 8; + rOff += 5; + } + + // Slice in case our shift overflowed to the right. + return CommonUtils.byteArrayToString(ret.slice(0, bytes)); + }, + + /** + * Trim excess padding from a Base64 string and atob(). + * + * See bug 562431 comment 4. + */ + safeAtoB: function safeAtoB(b64) { + let len = b64.length; + let over = len % 4; + return over ? atob(b64.substr(0, len - over)) : atob(b64); + }, + + /** + * Ensure that the specified value is defined in integer milliseconds since + * UNIX epoch. + * + * This throws an error if the value is not an integer, is negative, or looks + * like seconds, not milliseconds. + * + * If the value is null or 0, no exception is raised. + * + * @param value + * Value to validate. + */ + ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) { + if (!value) { + return; + } + + if (!/^[0-9]+$/.test(value)) { + throw new Error("Timestamp value is not a positive integer: " + value); + } + + let intValue = parseInt(value, 10); + + if (!intValue) { + return; + } + + // Catch what looks like seconds, not milliseconds. + if (intValue < 10000000000) { + throw new Error("Timestamp appears to be in seconds: " + intValue); + } + }, + + /** + * Read bytes from an nsIInputStream into a string. + * + * @param stream + * (nsIInputStream) Stream to read from. + * @param count + * (number) Integer number of bytes to read. If not defined, or + * 0, all available input is read. + */ + readBytesFromInputStream: function readBytesFromInputStream(stream, count) { + let BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" + ); + if (!count) { + count = stream.available(); + } + + return new BinaryInputStream(stream).readBytes(count); + }, + + /** + * Generate a new UUID using nsIUUIDGenerator. + * + * Example value: "1e00a2e2-1570-443e-bf5e-000354124234" + * + * @return string A hex-formatted UUID string. + */ + generateUUID: function generateUUID() { + let uuid = Services.uuid.generateUUID().toString(); + + return uuid.substring(1, uuid.length - 1); + }, + + /** + * Obtain an epoch value from a preference. + * + * This reads a string preference and returns an integer. The string + * preference is expected to contain the integer milliseconds since epoch. + * For best results, only read preferences that have been saved with + * setDatePref(). + * + * We need to store times as strings because integer preferences are only + * 32 bits and likely overflow most dates. + * + * If the pref contains a non-integer value, the specified default value will + * be returned. + * + * @param branch + * (Preferences) Branch from which to retrieve preference. + * @param pref + * (string) The preference to read from. + * @param def + * (Number) The default value to use if the preference is not defined. + * @param log + * (Log.Logger) Logger to write warnings to. + */ + getEpochPref: function getEpochPref(branch, pref, def = 0, log = null) { + if (!Number.isInteger(def)) { + throw new Error("Default value is not a number: " + def); + } + + let valueStr = branch.getStringPref(pref, null); + + if (valueStr !== null) { + let valueInt = parseInt(valueStr, 10); + if (Number.isNaN(valueInt)) { + if (log) { + log.warn( + "Preference value is not an integer. Using default. " + + pref + + "=" + + valueStr + + " -> " + + def + ); + } + + return def; + } + + return valueInt; + } + + return def; + }, + + /** + * Obtain a Date from a preference. + * + * This is a wrapper around getEpochPref. It converts the value to a Date + * instance and performs simple range checking. + * + * The range checking ensures the date is newer than the oldestYear + * parameter. + * + * @param branch + * (Preferences) Branch from which to read preference. + * @param pref + * (string) The preference from which to read. + * @param def + * (Number) The default value (in milliseconds) if the preference is + * not defined or invalid. + * @param log + * (Log.Logger) Logger to write warnings to. + * @param oldestYear + * (Number) Oldest year to accept in read values. + */ + getDatePref: function getDatePref( + branch, + pref, + def = 0, + log = null, + oldestYear = 2010 + ) { + let valueInt = this.getEpochPref(branch, pref, def, log); + let date = new Date(valueInt); + + if (valueInt == def || date.getFullYear() >= oldestYear) { + return date; + } + + if (log) { + log.warn( + "Unexpected old date seen in pref. Returning default: " + + pref + + "=" + + date + + " -> " + + def + ); + } + + return new Date(def); + }, + + /** + * Store a Date in a preference. + * + * This is the opposite of getDatePref(). The same notes apply. + * + * If the range check fails, an Error will be thrown instead of a default + * value silently being used. + * + * @param branch + * (Preference) Branch from which to read preference. + * @param pref + * (string) Name of preference to write to. + * @param date + * (Date) The value to save. + * @param oldestYear + * (Number) The oldest year to accept for values. + */ + setDatePref: function setDatePref(branch, pref, date, oldestYear = 2010) { + if (date.getFullYear() < oldestYear) { + throw new Error( + "Trying to set " + + pref + + " to a very old time: " + + date + + ". The current time is " + + new Date() + + ". Is the system clock wrong?" + ); + } + + branch.setStringPref(pref, "" + date.getTime()); + }, + + /** + * Convert a string between two encodings. + * + * Output is only guaranteed if the input stream is composed of octets. If + * the input string has characters with values larger than 255, data loss + * will occur. + * + * The returned string is guaranteed to consist of character codes no greater + * than 255. + * + * @param s + * (string) The source string to convert. + * @param source + * (string) The current encoding of the string. + * @param dest + * (string) The target encoding of the string. + * + * @return string + */ + convertString: function convertString(s, source, dest) { + if (!s) { + throw new Error("Input string must be defined."); + } + + let is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + is.setData(s, s.length); + + let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( + Ci.nsIStreamLoader + ); + + let result; + + listener.init({ + onStreamComplete: function onStreamComplete( + loader, + context, + status, + length, + data + ) { + result = String.fromCharCode.apply(this, data); + }, + }); + + let converter = this._converterService.asyncConvertData( + source, + dest, + listener, + null + ); + converter.onStartRequest(null, null); + converter.onDataAvailable(null, is, 0, s.length); + converter.onStopRequest(null, null, null); + + return result; + }, +}; + +ChromeUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function () { + let converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + return converter; +}); + +ChromeUtils.defineLazyGetter(CommonUtils, "_converterService", function () { + return Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); +}); diff --git a/services/crypto/cryptoComponents.manifest b/services/crypto/cryptoComponents.manifest new file mode 100644 index 0000000000..f9f47bb42a --- /dev/null +++ b/services/crypto/cryptoComponents.manifest @@ -0,0 +1 @@ +resource services-crypto resource://gre/modules/services-crypto/ diff --git a/services/crypto/modules/WeaveCrypto.sys.mjs b/services/crypto/modules/WeaveCrypto.sys.mjs new file mode 100644 index 0000000000..63ee51a1a2 --- /dev/null +++ b/services/crypto/modules/WeaveCrypto.sys.mjs @@ -0,0 +1,232 @@ +/* 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 CRYPT_ALGO = "AES-CBC"; +const CRYPT_ALGO_LENGTH = 256; +const CRYPT_ALGO_USAGES = ["encrypt", "decrypt"]; +const AES_CBC_IV_SIZE = 16; +const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 }; +const UTF_LABEL = "utf-8"; + +export function WeaveCrypto() { + this.init(); +} + +WeaveCrypto.prototype = { + prefBranch: null, + debug: true, // services.sync.log.cryptoDebug + + observer: { + _self: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + observe(subject, topic, data) { + let self = this._self; + self.log("Observed " + topic + " topic."); + if (topic == "nsPref:changed") { + self.debug = self.prefBranch.getBoolPref("cryptoDebug"); + } + }, + }, + + init() { + // Preferences. Add observer so we get notified of changes. + this.prefBranch = Services.prefs.getBranch("services.sync.log."); + this.prefBranch.addObserver("cryptoDebug", this.observer); + this.observer._self = this; + this.debug = this.prefBranch.getBoolPref("cryptoDebug", false); + ChromeUtils.defineLazyGetter( + this, + "encoder", + () => new TextEncoder(UTF_LABEL) + ); + ChromeUtils.defineLazyGetter( + this, + "decoder", + () => new TextDecoder(UTF_LABEL, { fatal: true }) + ); + }, + + log(message) { + if (!this.debug) { + return; + } + dump("WeaveCrypto: " + message + "\n"); + Services.console.logStringMessage("WeaveCrypto: " + message); + }, + + // /!\ Only use this for tests! /!\ + _getCrypto() { + return crypto; + }, + + async encrypt(clearTextUCS2, symmetricKey, iv) { + this.log("encrypt() called"); + let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer; + let encrypted = await this._commonCrypt( + clearTextBuffer, + symmetricKey, + iv, + OPERATIONS.ENCRYPT + ); + return this.encodeBase64(encrypted); + }, + + async decrypt(cipherText, symmetricKey, iv) { + this.log("decrypt() called"); + if (cipherText.length) { + cipherText = atob(cipherText); + } + let cipherTextBuffer = this.byteCompressInts(cipherText); + let decrypted = await this._commonCrypt( + cipherTextBuffer, + symmetricKey, + iv, + OPERATIONS.DECRYPT + ); + return this.decoder.decode(decrypted); + }, + + /** + * _commonCrypt + * + * @args + * data: data to encrypt/decrypt (ArrayBuffer) + * symKeyStr: symmetric key (Base64 String) + * ivStr: initialization vector (Base64 String) + * operation: operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT) + * @returns + * the encrypted/decrypted data (ArrayBuffer) + */ + async _commonCrypt(data, symKeyStr, ivStr, operation) { + this.log("_commonCrypt() called"); + ivStr = atob(ivStr); + + if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) { + throw new Error("Unsupported operation in _commonCrypt."); + } + // We never want an IV longer than the block size, which is 16 bytes + // for AES, neither do we want one smaller; throw in both cases. + if (ivStr.length !== AES_CBC_IV_SIZE) { + throw new Error(`Invalid IV size; must be ${AES_CBC_IV_SIZE} bytes.`); + } + + let iv = this.byteCompressInts(ivStr); + let symKey = await this.importSymKey(symKeyStr, operation); + let cryptMethod = ( + operation === OPERATIONS.ENCRYPT + ? crypto.subtle.encrypt + : crypto.subtle.decrypt + ).bind(crypto.subtle); + let algo = { name: CRYPT_ALGO, iv }; + + let keyBytes = await cryptMethod.call(crypto.subtle, algo, symKey, data); + return new Uint8Array(keyBytes); + }, + + async generateRandomKey() { + this.log("generateRandomKey() called"); + let algo = { + name: CRYPT_ALGO, + length: CRYPT_ALGO_LENGTH, + }; + let key = await crypto.subtle.generateKey(algo, true, CRYPT_ALGO_USAGES); + let keyBytes = await crypto.subtle.exportKey("raw", key); + return this.encodeBase64(new Uint8Array(keyBytes)); + }, + + generateRandomIV() { + return this.generateRandomBytes(AES_CBC_IV_SIZE); + }, + + generateRandomBytes(byteCount) { + this.log("generateRandomBytes() called"); + + let randBytes = new Uint8Array(byteCount); + crypto.getRandomValues(randBytes); + + return this.encodeBase64(randBytes); + }, + + // + // SymKey CryptoKey memoization. + // + + // Memoize the import of symmetric keys. We do this by using the base64 + // string itself as a key. + _encryptionSymKeyMemo: {}, + _decryptionSymKeyMemo: {}, + async importSymKey(encodedKeyString, operation) { + let memo; + + // We use two separate memos for thoroughness: operation is an input to + // key import. + switch (operation) { + case OPERATIONS.ENCRYPT: + memo = this._encryptionSymKeyMemo; + break; + case OPERATIONS.DECRYPT: + memo = this._decryptionSymKeyMemo; + break; + default: + throw new Error("Unsupported operation in importSymKey."); + } + + if (encodedKeyString in memo) { + return memo[encodedKeyString]; + } + + let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true); + let algo = { name: CRYPT_ALGO }; + let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"]; + let symKey = await crypto.subtle.importKey( + "raw", + symmetricKeyBuffer, + algo, + false, + usages + ); + memo[encodedKeyString] = symKey; + return symKey; + }, + + // + // Utility functions + // + + /** + * Returns an Uint8Array filled with a JS string, + * which means we only keep utf-16 characters from 0x00 to 0xFF. + */ + byteCompressInts(str) { + let arrayBuffer = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arrayBuffer[i] = str.charCodeAt(i) & 0xff; + } + return arrayBuffer; + }, + + expandData(data) { + let expanded = ""; + for (let i = 0; i < data.length; i++) { + expanded += String.fromCharCode(data[i]); + } + return expanded; + }, + + encodeBase64(data) { + return btoa(this.expandData(data)); + }, + + makeUint8Array(input, isEncoded) { + if (isEncoded) { + input = atob(input); + } + return this.byteCompressInts(input); + }, +}; diff --git a/services/crypto/modules/jwcrypto.sys.mjs b/services/crypto/modules/jwcrypto.sys.mjs new file mode 100644 index 0000000000..d1f37eaa58 --- /dev/null +++ b/services/crypto/modules/jwcrypto.sys.mjs @@ -0,0 +1,214 @@ +/* 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 ECDH_PARAMS = { + name: "ECDH", + namedCurve: "P-256", +}; +const AES_PARAMS = { + name: "AES-GCM", + length: 256, +}; +const AES_TAG_LEN = 128; +const AES_GCM_IV_SIZE = 12; +const UTF8_ENCODER = new TextEncoder(); +const UTF8_DECODER = new TextDecoder(); + +class JWCrypto { + /** + * Encrypts the given data into a JWE using AES-256-GCM content encryption. + * + * This function implements a very small subset of the JWE encryption standard + * from https://tools.ietf.org/html/rfc7516. The only supported content encryption + * algorithm is enc="A256GCM" [1] and the only supported key encryption algorithm + * is alg="ECDH-ES" [2]. + * + * @param {Object} key Peer Public JWK. + * @param {ArrayBuffer} data + * + * [1] https://tools.ietf.org/html/rfc7518#section-5.3 + * [2] https://tools.ietf.org/html/rfc7518#section-4.6 + * + * @returns {Promise} + */ + async generateJWE(key, data) { + // Generate an ephemeral key to use just for this encryption. + // The public component gets embedded in the JWE header. + const epk = await crypto.subtle.generateKey(ECDH_PARAMS, true, [ + "deriveKey", + ]); + const ownPublicJWK = await crypto.subtle.exportKey("jwk", epk.publicKey); + // Remove properties added by our WebCrypto implementation but that aren't typically + // used with JWE in the wild. This saves space in the resulting JWE, and makes it easier + // to re-import the resulting JWK. + delete ownPublicJWK.key_ops; + delete ownPublicJWK.ext; + let header = { alg: "ECDH-ES", enc: "A256GCM", epk: ownPublicJWK }; + // Import the peer's public key. + const peerPublicKey = await crypto.subtle.importKey( + "jwk", + key, + ECDH_PARAMS, + false, + ["deriveKey"] + ); + if (key.hasOwnProperty("kid")) { + header.kid = key.kid; + } + // Do ECDH agreement to get the content encryption key. + const contentKey = await deriveECDHSharedAESKey( + epk.privateKey, + peerPublicKey, + ["encrypt"] + ); + // Encrypt with AES-GCM using the generated key. + // Note that the IV is generated randomly, which *in general* is not safe to do with AES-GCM because + // it's too short to guarantee uniqueness. But we know that the AES-GCM key itself is unique and will + // only be used for this single encryption, making a random IV safe to use for this particular use-case. + let iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_SIZE)); + // Yes, additionalData is the byte representation of the base64 representation of the stringified header. + const additionalData = UTF8_ENCODER.encode( + ChromeUtils.base64URLEncode(UTF8_ENCODER.encode(JSON.stringify(header)), { + pad: false, + }) + ); + const encrypted = await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv, + additionalData, + tagLength: AES_TAG_LEN, + }, + contentKey, + data + ); + // JWE needs the authentication tag as a separate string. + const tagIdx = encrypted.byteLength - ((AES_TAG_LEN + 7) >> 3); + let ciphertext = encrypted.slice(0, tagIdx); + let tag = encrypted.slice(tagIdx); + // JWE serialization in compact format. + header = UTF8_ENCODER.encode(JSON.stringify(header)); + header = ChromeUtils.base64URLEncode(header, { pad: false }); + tag = ChromeUtils.base64URLEncode(tag, { pad: false }); + ciphertext = ChromeUtils.base64URLEncode(ciphertext, { pad: false }); + iv = ChromeUtils.base64URLEncode(iv, { pad: false }); + return `${header}..${iv}.${ciphertext}.${tag}`; // No CEK + } + + /** + * Decrypts the given JWE using AES-256-GCM content encryption into a byte array. + * This function does the opposite of `JWCrypto.generateJWE`. + * The only supported content encryption algorithm is enc="A256GCM" [1] + * and the only supported key encryption algorithm is alg="ECDH-ES" [2]. + * + * @param {"ECDH-ES"} algorithm + * @param {CryptoKey} key Local private key + * + * [1] https://tools.ietf.org/html/rfc7518#section-5.3 + * [2] https://tools.ietf.org/html/rfc7518#section-4.6 + * + * @returns {Promise} + */ + async decryptJWE(jwe, key) { + let [header, cek, iv, ciphertext, authTag] = jwe.split("."); + const additionalData = UTF8_ENCODER.encode(header); + header = JSON.parse( + UTF8_DECODER.decode( + ChromeUtils.base64URLDecode(header, { padding: "reject" }) + ) + ); + if (!!cek.length || header.enc !== "A256GCM" || header.alg !== "ECDH-ES") { + throw new Error("Unknown algorithm."); + } + if ("apu" in header || "apv" in header) { + throw new Error("apu and apv header values are not supported."); + } + const peerPublicKey = await crypto.subtle.importKey( + "jwk", + header.epk, + ECDH_PARAMS, + false, + ["deriveKey"] + ); + // Do ECDH agreement to get the content encryption key. + const contentKey = await deriveECDHSharedAESKey(key, peerPublicKey, [ + "decrypt", + ]); + iv = ChromeUtils.base64URLDecode(iv, { padding: "reject" }); + ciphertext = new Uint8Array( + ChromeUtils.base64URLDecode(ciphertext, { padding: "reject" }) + ); + authTag = new Uint8Array( + ChromeUtils.base64URLDecode(authTag, { padding: "reject" }) + ); + const bundle = new Uint8Array([...ciphertext, ...authTag]); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv, + tagLength: AES_TAG_LEN, + additionalData, + }, + contentKey, + bundle + ); + return new Uint8Array(decrypted); + } +} + +/** + * Do an ECDH agreement between a public and private key, + * returning the derived encryption key as specced by + * JWA RFC. + * The raw ECDH secret is derived into a key using + * Concat KDF, as defined in Section 5.8.1 of [NIST.800-56A]. + * @param {CryptoKey} privateKey + * @param {CryptoKey} publicKey + * @param {String[]} keyUsages See `SubtleCrypto.deriveKey` 5th paramater documentation. + * @returns {Promise} + */ +async function deriveECDHSharedAESKey(privateKey, publicKey, keyUsages) { + const params = { ...ECDH_PARAMS, ...{ public: publicKey } }; + const sharedKey = await crypto.subtle.deriveKey( + params, + privateKey, + AES_PARAMS, + true, + keyUsages + ); + // This is the NIST Concat KDF specialized to a specific set of parameters, + // which basically turn it into a single application of SHA256. + // The details are from the JWA RFC. + let sharedKeyBytes = await crypto.subtle.exportKey("raw", sharedKey); + sharedKeyBytes = new Uint8Array(sharedKeyBytes); + const info = [ + "\x00\x00\x00\x07A256GCM", // 7-byte algorithm identifier + "\x00\x00\x00\x00", // empty PartyUInfo + "\x00\x00\x00\x00", // empty PartyVInfo + "\x00\x00\x01\x00", // keylen == 256 + ].join(""); + const pkcs = `\x00\x00\x00\x01${String.fromCharCode.apply( + null, + sharedKeyBytes + )}${info}`; + const pkcsBuf = Uint8Array.from( + Array.prototype.map.call(pkcs, c => c.charCodeAt(0)) + ); + const derivedKeyBytes = await crypto.subtle.digest( + { + name: "SHA-256", + }, + pkcsBuf + ); + return crypto.subtle.importKey( + "raw", + derivedKeyBytes, + AES_PARAMS, + false, + keyUsages + ); +} + +export const jwcrypto = new JWCrypto(); diff --git a/services/crypto/modules/utils.sys.mjs b/services/crypto/modules/utils.sys.mjs new file mode 100644 index 0000000000..3aa3db32d1 --- /dev/null +++ b/services/crypto/modules/utils.sys.mjs @@ -0,0 +1,539 @@ +/* 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 { Observers } from "resource://services-common/observers.sys.mjs"; + +import { CommonUtils } from "resource://services-common/utils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "textEncoder", function () { + return new TextEncoder(); +}); + +/** + * A number of `Legacy` suffixed functions are exposed by CryptoUtils. + * They work with octet strings, which were used before Javascript + * got ArrayBuffer and friends. + */ +export var CryptoUtils = { + xor(a, b) { + let bytes = []; + + if (a.length != b.length) { + throw new Error( + "can't xor unequal length strings: " + a.length + " vs " + b.length + ); + } + + for (let i = 0; i < a.length; i++) { + bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i); + } + + return String.fromCharCode.apply(String, bytes); + }, + + /** + * Generate a string of random bytes. + * @returns {String} Octet string + */ + generateRandomBytesLegacy(length) { + let bytes = CryptoUtils.generateRandomBytes(length); + return CommonUtils.arrayBufferToByteString(bytes); + }, + + generateRandomBytes(length) { + return crypto.getRandomValues(new Uint8Array(length)); + }, + + /** + * UTF8-encode a message and hash it with the given hasher. Returns a + * string containing bytes. + */ + digestUTF8(message, hasher) { + let data = lazy.textEncoder.encode(message); + hasher.update(data, data.length); + let result = hasher.finish(false); + return result; + }, + + /** + * Treat the given message as a bytes string (if necessary) and hash it with + * the given hasher. Returns a string containing bytes. + */ + digestBytes(bytes, hasher) { + if (typeof bytes == "string" || bytes instanceof String) { + bytes = CommonUtils.byteStringToArrayBuffer(bytes); + } + return CryptoUtils.digestBytesArray(bytes, hasher); + }, + + digestBytesArray(bytes, hasher) { + hasher.update(bytes, bytes.length); + let result = hasher.finish(false); + return result; + }, + + /** + * Encode the message into UTF-8 and feed the resulting bytes into the + * given hasher. Does not return a hash. This can be called multiple times + * with a single hasher, but eventually you must extract the result + * yourself. + */ + updateUTF8(message, hasher) { + let bytes = lazy.textEncoder.encode(message); + hasher.update(bytes, bytes.length); + }, + + sha256(message) { + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(hasher.SHA256); + return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher)); + }, + + sha256Base64(message) { + let data = lazy.textEncoder.encode(message); + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(hasher.SHA256); + hasher.update(data, data.length); + return hasher.finish(true); + }, + + /** + * @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256) + * @param {string} key Key as an octet string. + * @param {string} data Data as an octet string. + */ + async hmacLegacy(alg, key, data) { + if (!key || !key.length) { + key = "\0"; + } + data = CommonUtils.byteStringToArrayBuffer(data); + key = CommonUtils.byteStringToArrayBuffer(key); + const result = await CryptoUtils.hmac(alg, key, data); + return CommonUtils.arrayBufferToByteString(result); + }, + + /** + * @param {string} ikm IKM as an octet string. + * @param {string} salt Salt as an Hex string. + * @param {string} info Info as a regular string. + * @param {Number} len Desired output length in bytes. + */ + async hkdfLegacy(ikm, xts, info, len) { + ikm = CommonUtils.byteStringToArrayBuffer(ikm); + xts = CommonUtils.byteStringToArrayBuffer(xts); + info = lazy.textEncoder.encode(info); + const okm = await CryptoUtils.hkdf(ikm, xts, info, len); + return CommonUtils.arrayBufferToByteString(okm); + }, + + /** + * @param {String} alg Hash algorithm (common values are SHA-1 or SHA-256) + * @param {ArrayBuffer} key + * @param {ArrayBuffer} data + * @param {Number} len Desired output length in bytes. + * @returns {Uint8Array} + */ + async hmac(alg, key, data) { + const hmacKey = await crypto.subtle.importKey( + "raw", + key, + { name: "HMAC", hash: alg }, + false, + ["sign"] + ); + const result = await crypto.subtle.sign("HMAC", hmacKey, data); + return new Uint8Array(result); + }, + + /** + * @param {ArrayBuffer} ikm + * @param {ArrayBuffer} salt + * @param {ArrayBuffer} info + * @param {Number} len Desired output length in bytes. + * @returns {Uint8Array} + */ + async hkdf(ikm, salt, info, len) { + const key = await crypto.subtle.importKey( + "raw", + ikm, + { name: "HKDF" }, + false, + ["deriveBits"] + ); + const okm = await crypto.subtle.deriveBits( + { + name: "HKDF", + hash: "SHA-256", + salt, + info, + }, + key, + len * 8 + ); + return new Uint8Array(okm); + }, + + /** + * PBKDF2 password stretching with SHA-256 hmac. + * + * @param {string} passphrase Passphrase as an octet string. + * @param {string} salt Salt as an octet string. + * @param {string} iterations Number of iterations, a positive integer. + * @param {string} len Desired output length in bytes. + */ + async pbkdf2Generate(passphrase, salt, iterations, len) { + passphrase = CommonUtils.byteStringToArrayBuffer(passphrase); + salt = CommonUtils.byteStringToArrayBuffer(salt); + const key = await crypto.subtle.importKey( + "raw", + passphrase, + { name: "PBKDF2" }, + false, + ["deriveBits"] + ); + const output = await crypto.subtle.deriveBits( + { + name: "PBKDF2", + hash: "SHA-256", + salt, + iterations, + }, + key, + len * 8 + ); + return CommonUtils.arrayBufferToByteString(new Uint8Array(output)); + }, + + /** + * Compute the HTTP MAC SHA-1 for an HTTP request. + * + * @param identifier + * (string) MAC Key Identifier. + * @param key + * (string) MAC Key. + * @param method + * (string) HTTP request method. + * @param URI + * (nsIURI) HTTP request URI. + * @param extra + * (object) Optional extra parameters. Valid keys are: + * nonce_bytes - How many bytes the nonce should be. This defaults + * to 8. Note that this many bytes are Base64 encoded, so the + * string length of the nonce will be longer than this value. + * ts - Timestamp to use. Should only be defined for testing. + * nonce - String nonce. Should only be defined for testing as this + * function will generate a cryptographically secure random one + * if not defined. + * ext - Extra string to be included in MAC. Per the HTTP MAC spec, + * the format is undefined and thus application specific. + * @returns + * (object) Contains results of operation and input arguments (for + * symmetry). The object has the following keys: + * + * identifier - (string) MAC Key Identifier (from arguments). + * key - (string) MAC Key (from arguments). + * method - (string) HTTP request method (from arguments). + * hostname - (string) HTTP hostname used (derived from arguments). + * port - (string) HTTP port number used (derived from arguments). + * mac - (string) Raw HMAC digest bytes. + * getHeader - (function) Call to obtain the string Authorization + * header value for this invocation. + * nonce - (string) Nonce value used. + * ts - (number) Integer seconds since Unix epoch that was used. + */ + async computeHTTPMACSHA1(identifier, key, method, uri, extra) { + let ts = extra && extra.ts ? extra.ts : Math.floor(Date.now() / 1000); + let nonce_bytes = extra && extra.nonce_bytes > 0 ? extra.nonce_bytes : 8; + + // We are allowed to use more than the Base64 alphabet if we want. + let nonce = + extra && extra.nonce + ? extra.nonce + : btoa(CryptoUtils.generateRandomBytesLegacy(nonce_bytes)); + + let host = uri.asciiHost; + let port; + let usedMethod = method.toUpperCase(); + + if (uri.port != -1) { + port = uri.port; + } else if (uri.scheme == "http") { + port = "80"; + } else if (uri.scheme == "https") { + port = "443"; + } else { + throw new Error("Unsupported URI scheme: " + uri.scheme); + } + + let ext = extra && extra.ext ? extra.ext : ""; + + let requestString = + ts.toString(10) + + "\n" + + nonce + + "\n" + + usedMethod + + "\n" + + uri.pathQueryRef + + "\n" + + host + + "\n" + + port + + "\n" + + ext + + "\n"; + + const mac = await CryptoUtils.hmacLegacy("SHA-1", key, requestString); + + function getHeader() { + return CryptoUtils.getHTTPMACSHA1Header( + this.identifier, + this.ts, + this.nonce, + this.mac, + this.ext + ); + } + + return { + identifier, + key, + method: usedMethod, + hostname: host, + port, + mac, + nonce, + ts, + ext, + getHeader, + }; + }, + + /** + * Obtain the HTTP MAC Authorization header value from fields. + * + * @param identifier + * (string) MAC key identifier. + * @param ts + * (number) Integer seconds since Unix epoch. + * @param nonce + * (string) Nonce value. + * @param mac + * (string) Computed HMAC digest (raw bytes). + * @param ext + * (optional) (string) Extra string content. + * @returns + * (string) Value to put in Authorization header. + */ + getHTTPMACSHA1Header: function getHTTPMACSHA1Header( + identifier, + ts, + nonce, + mac, + ext + ) { + let header = + 'MAC id="' + + identifier + + '", ' + + 'ts="' + + ts + + '", ' + + 'nonce="' + + nonce + + '", ' + + 'mac="' + + btoa(mac) + + '"'; + + if (!ext) { + return header; + } + + return (header += ', ext="' + ext + '"'); + }, + + /** + * Given an HTTP header value, strip out any attributes. + */ + + stripHeaderAttributes(value) { + value = value || ""; + let i = value.indexOf(";"); + return value + .substring(0, i >= 0 ? i : undefined) + .trim() + .toLowerCase(); + }, + + /** + * Compute the HAWK client values (mostly the header) for an HTTP request. + * + * @param URI + * (nsIURI) HTTP request URI. + * @param method + * (string) HTTP request method. + * @param options + * (object) extra parameters (all but "credentials" are optional): + * credentials - (object, mandatory) HAWK credentials object. + * All three keys are required: + * id - (string) key identifier + * key - (string) raw key bytes + * ext - (string) application-specific data, included in MAC + * localtimeOffsetMsec - (number) local clock offset (vs server) + * payload - (string) payload to include in hash, containing the + * HTTP request body. If not provided, the HAWK hash + * will not cover the request body, and the server + * should not check it either. This will be UTF-8 + * encoded into bytes before hashing. This function + * cannot handle arbitrary binary data, sorry (the + * UTF-8 encoding process will corrupt any codepoints + * between U+0080 and U+00FF). Callers must be careful + * to use an HTTP client function which encodes the + * payload exactly the same way, otherwise the hash + * will not match. + * contentType - (string) payload Content-Type. This is included + * (without any attributes like "charset=") in the + * HAWK hash. It does *not* affect interpretation + * of the "payload" property. + * hash - (base64 string) pre-calculated payload hash. If + * provided, "payload" is ignored. + * ts - (number) pre-calculated timestamp, secs since epoch + * now - (number) current time, ms-since-epoch, for tests + * nonce - (string) pre-calculated nonce. Should only be defined + * for testing as this function will generate a + * cryptographically secure random one if not defined. + * @returns + * Promise Contains results of operation. The object has the + * following keys: + * field - (string) HAWK header, to use in Authorization: header + * artifacts - (object) other generated values: + * ts - (number) timestamp, in seconds since epoch + * nonce - (string) + * method - (string) + * resource - (string) path plus querystring + * host - (string) + * port - (number) + * hash - (string) payload hash (base64) + * ext - (string) app-specific data + * MAC - (string) request MAC (base64) + */ + async computeHAWK(uri, method, options) { + let credentials = options.credentials; + let ts = + options.ts || + Math.floor( + ((options.now || Date.now()) + (options.localtimeOffsetMsec || 0)) / + 1000 + ); + let port; + if (uri.port != -1) { + port = uri.port; + } else if (uri.scheme == "http") { + port = 80; + } else if (uri.scheme == "https") { + port = 443; + } else { + throw new Error("Unsupported URI scheme: " + uri.scheme); + } + + let artifacts = { + ts, + nonce: options.nonce || btoa(CryptoUtils.generateRandomBytesLegacy(8)), + method: method.toUpperCase(), + resource: uri.pathQueryRef, // This includes both path and search/queryarg. + host: uri.asciiHost.toLowerCase(), // This includes punycoding. + port: port.toString(10), + hash: options.hash, + ext: options.ext, + }; + + let contentType = CryptoUtils.stripHeaderAttributes(options.contentType); + + if ( + !artifacts.hash && + options.hasOwnProperty("payload") && + options.payload + ) { + const buffer = lazy.textEncoder.encode( + `hawk.1.payload\n${contentType}\n${options.payload}\n` + ); + const hash = await crypto.subtle.digest("SHA-256", buffer); + // HAWK specifies this .hash to use +/ (not _-) and include the + // trailing "==" padding. + artifacts.hash = ChromeUtils.base64URLEncode(hash, { pad: true }) + .replace(/-/g, "+") + .replace(/_/g, "/"); + } + + let requestString = + "hawk.1.header\n" + + artifacts.ts.toString(10) + + "\n" + + artifacts.nonce + + "\n" + + artifacts.method + + "\n" + + artifacts.resource + + "\n" + + artifacts.host + + "\n" + + artifacts.port + + "\n" + + (artifacts.hash || "") + + "\n"; + if (artifacts.ext) { + requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n"); + } + requestString += "\n"; + + const hash = await CryptoUtils.hmacLegacy( + "SHA-256", + credentials.key, + requestString + ); + artifacts.mac = btoa(hash); + // The output MAC uses "+" and "/", and padded== . + + function escape(attribute) { + // This is used for "x=y" attributes inside HTTP headers. + return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); + } + let header = + 'Hawk id="' + + credentials.id + + '", ' + + 'ts="' + + artifacts.ts + + '", ' + + 'nonce="' + + artifacts.nonce + + '", ' + + (artifacts.hash ? 'hash="' + artifacts.hash + '", ' : "") + + (artifacts.ext ? 'ext="' + escape(artifacts.ext) + '", ' : "") + + 'mac="' + + artifacts.mac + + '"'; + return { + artifacts, + field: header, + }; + }, +}; + +var Svc = {}; + +Observers.add("xpcom-shutdown", function unloadServices() { + Observers.remove("xpcom-shutdown", unloadServices); + + for (let k in Svc) { + delete Svc[k]; + } +}); diff --git a/services/crypto/moz.build b/services/crypto/moz.build new file mode 100644 index 0000000000..4a1650cb6d --- /dev/null +++ b/services/crypto/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Sync") + +XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.toml"] + +EXTRA_JS_MODULES["services-crypto"] += [ + "modules/jwcrypto.sys.mjs", + "modules/utils.sys.mjs", + "modules/WeaveCrypto.sys.mjs", +] + +EXTRA_COMPONENTS += [ + "cryptoComponents.manifest", +] diff --git a/services/crypto/tests/unit/head_helpers.js b/services/crypto/tests/unit/head_helpers.js new file mode 100644 index 0000000000..322f6761a9 --- /dev/null +++ b/services/crypto/tests/unit/head_helpers.js @@ -0,0 +1,78 @@ +/* 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); +} + +// Register resource alias. Normally done in SyncComponents.manifest. +function addResourceAlias() { + const resProt = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + let uri = Services.io.newURI("resource://gre/modules/services-crypto/"); + resProt.setSubstitution("services-crypto", uri); +} +addResourceAlias(); + +/** + * 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 (some, debug, text, to) { + print(Array.from(arguments).join(" ")); +}; diff --git a/services/crypto/tests/unit/test_crypto_crypt.js b/services/crypto/tests/unit/test_crypto_crypt.js new file mode 100644 index 0000000000..8fadd307c7 --- /dev/null +++ b/services/crypto/tests/unit/test_crypto_crypt.js @@ -0,0 +1,226 @@ +const { WeaveCrypto } = ChromeUtils.importESModule( + "resource://services-crypto/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); +}); diff --git a/services/crypto/tests/unit/test_crypto_random.js b/services/crypto/tests/unit/test_crypto_random.js new file mode 100644 index 0000000000..bd913c81e8 --- /dev/null +++ b/services/crypto/tests/unit/test_crypto_random.js @@ -0,0 +1,52 @@ +const { WeaveCrypto } = ChromeUtils.importESModule( + "resource://services-crypto/WeaveCrypto.sys.mjs" +); + +var cryptoSvc = new WeaveCrypto(); + +add_task(async function test_crypto_random() { + if (this.gczeal) { + _("Running crypto random tests with gczeal(2)."); + gczeal(2); + } + + // Test salt generation. + var salt; + + salt = cryptoSvc.generateRandomBytes(0); + Assert.equal(salt.length, 0); + salt = cryptoSvc.generateRandomBytes(1); + Assert.equal(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(2); + Assert.equal(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(3); + Assert.equal(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(4); + Assert.equal(salt.length, 8); + salt = cryptoSvc.generateRandomBytes(8); + Assert.equal(salt.length, 12); + + // sanity check to make sure salts seem random + var salt2 = cryptoSvc.generateRandomBytes(8); + Assert.equal(salt2.length, 12); + Assert.notEqual(salt, salt2); + + salt = cryptoSvc.generateRandomBytes(1024); + Assert.equal(salt.length, 1368); + salt = cryptoSvc.generateRandomBytes(16); + Assert.equal(salt.length, 24); + + // Test random key generation + var keydata, keydata2, iv; + + keydata = await cryptoSvc.generateRandomKey(); + Assert.equal(keydata.length, 44); + keydata2 = await cryptoSvc.generateRandomKey(); + Assert.notEqual(keydata, keydata2); // sanity check for randomness + iv = cryptoSvc.generateRandomIV(); + Assert.equal(iv.length, 24); + + if (this.gczeal) { + gczeal(0); + } +}); diff --git a/services/crypto/tests/unit/test_jwcrypto.js b/services/crypto/tests/unit/test_jwcrypto.js new file mode 100644 index 0000000000..02f064d431 --- /dev/null +++ b/services/crypto/tests/unit/test_jwcrypto.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs", +}); + +// Enable logging from jwcrypto.jsm. +Services.prefs.setStringPref("services.crypto.jwcrypto.log.level", "Debug"); + +add_task(async function test_jwe_roundtrip_ecdh_es_encryption() { + const plaintext = crypto.getRandomValues(new Uint8Array(123)); + const remoteKey = await crypto.subtle.generateKey( + { + name: "ECDH", + namedCurve: "P-256", + }, + true, + ["deriveKey"] + ); + const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey); + delete remoteJWK.key_ops; + const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext); + const decrypted = await jwcrypto.decryptJWE(jwe, remoteKey.privateKey); + Assert.deepEqual(plaintext, decrypted); +}); + +add_task(async function test_jwe_header_includes_key_id() { + const plaintext = crypto.getRandomValues(new Uint8Array(123)); + const remoteKey = await crypto.subtle.generateKey( + { + name: "ECDH", + namedCurve: "P-256", + }, + true, + ["deriveKey"] + ); + const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey); + delete remoteJWK.key_ops; + remoteJWK.kid = "key identifier"; + const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext); + let [header /* other items deliberately ignored */] = jwe.split("."); + header = JSON.parse( + new TextDecoder().decode( + ChromeUtils.base64URLDecode(header, { padding: "reject" }) + ) + ); + Assert.equal(header.kid, "key identifier"); +}); diff --git a/services/crypto/tests/unit/test_load_modules.js b/services/crypto/tests/unit/test_load_modules.js new file mode 100644 index 0000000000..2c850d3dab --- /dev/null +++ b/services/crypto/tests/unit/test_load_modules.js @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const modules = ["utils.sys.mjs", "WeaveCrypto.sys.mjs"]; + +function run_test() { + for (let m of modules) { + let resource = "resource://services-crypto/" + m; + _("Attempting to import: " + resource); + ChromeUtils.importESModule(resource); + } +} diff --git a/services/crypto/tests/unit/test_utils_hawk.js b/services/crypto/tests/unit/test_utils_hawk.js new file mode 100644 index 0000000000..71702b7349 --- /dev/null +++ b/services/crypto/tests/unit/test_utils_hawk.js @@ -0,0 +1,346 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { CryptoUtils } = ChromeUtils.importESModule( + "resource://services-crypto/utils.sys.mjs" +); + +function run_test() { + initTestLogging(); + + run_next_test(); +} + +add_task(async function test_hawk() { + let compute = CryptoUtils.computeHAWK; + + let method = "POST"; + let ts = 1353809207; + let nonce = "Ygvqdz"; + + let credentials = { + id: "123456", + key: "2983d45yun89q", + }; + + let uri_https = CommonUtils.makeURI( + "https://example.net/somewhere/over/the/rainbow" + ); + let opts = { + credentials, + ext: "Bazinga!", + ts, + nonce, + payload: "something to write about", + contentType: "text/plain", + }; + + let result = await compute(uri_https, method, opts); + Assert.equal( + result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'ext="Bazinga!", ' + + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' + ); + Assert.equal(result.artifacts.ts, ts); + Assert.equal(result.artifacts.nonce, nonce); + Assert.equal(result.artifacts.method, method); + Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow"); + Assert.equal(result.artifacts.host, "example.net"); + Assert.equal(result.artifacts.port, 443); + Assert.equal( + result.artifacts.hash, + "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=" + ); + Assert.equal(result.artifacts.ext, "Bazinga!"); + + let opts_noext = { + credentials, + ts, + nonce, + payload: "something to write about", + contentType: "text/plain", + }; + result = await compute(uri_https, method, opts_noext); + Assert.equal( + result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="' + ); + Assert.equal(result.artifacts.ts, ts); + Assert.equal(result.artifacts.nonce, nonce); + Assert.equal(result.artifacts.method, method); + Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow"); + Assert.equal(result.artifacts.host, "example.net"); + Assert.equal(result.artifacts.port, 443); + Assert.equal( + result.artifacts.hash, + "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=" + ); + + /* Leaving optional fields out should work, although of course then we can't + * assert much about the resulting hashes. The resulting header should look + * roughly like: + * Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U=" + */ + + result = await compute(uri_https, method, { credentials }); + let fields = result.field.split(" "); + Assert.equal(fields[0], "Hawk"); + Assert.equal(fields[1], 'id="123456",'); // from creds.id + Assert.ok(fields[2].startsWith('ts="')); + /* The HAWK spec calls for seconds-since-epoch, not ms-since-epoch. + * Warning: this test will fail in the year 33658, and for time travellers + * who journey earlier than 2001. Please plan accordingly. */ + Assert.ok(result.artifacts.ts > 1000 * 1000 * 1000); + Assert.ok(result.artifacts.ts < 1000 * 1000 * 1000 * 1000); + Assert.ok(fields[3].startsWith('nonce="')); + Assert.equal(fields[3].length, 'nonce="12345678901=",'.length); + Assert.equal(result.artifacts.nonce.length, "12345678901=".length); + + let result2 = await compute(uri_https, method, { credentials }); + Assert.notEqual(result.artifacts.nonce, result2.artifacts.nonce); + + /* Using an upper-case URI hostname shouldn't affect the hash. */ + + let uri_https_upper = CommonUtils.makeURI( + "https://EXAMPLE.NET/somewhere/over/the/rainbow" + ); + result = await compute(uri_https_upper, method, opts); + Assert.equal( + result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'ext="Bazinga!", ' + + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' + ); + + /* Using a lower-case method name shouldn't affect the hash. */ + result = await compute(uri_https_upper, method.toLowerCase(), opts); + Assert.equal( + result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + + 'ext="Bazinga!", ' + + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' + ); + + /* The localtimeOffsetMsec field should be honored. HAWK uses this to + * compensate for clock skew between client and server: if the request is + * rejected with a timestamp out-of-range error, the error includes the + * server's time, and the client computes its clock offset and tries again. + * Clients can remember this offset for a while. + */ + + result = await compute(uri_https, method, { + credentials, + now: 1378848968650, + }); + Assert.equal(result.artifacts.ts, 1378848968); + + result = await compute(uri_https, method, { + credentials, + now: 1378848968650, + localtimeOffsetMsec: 1000 * 1000, + }); + Assert.equal(result.artifacts.ts, 1378848968 + 1000); + + /* Search/query-args in URIs should be included in the hash. */ + let makeURI = CommonUtils.makeURI; + result = await compute(makeURI("http://example.net/path"), method, opts); + Assert.equal(result.artifacts.resource, "/path"); + Assert.equal( + result.artifacts.mac, + "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI=" + ); + + result = await compute(makeURI("http://example.net/path/"), method, opts); + Assert.equal(result.artifacts.resource, "/path/"); + Assert.equal( + result.artifacts.mac, + "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw=" + ); + + result = await compute( + makeURI("http://example.net/path?query=search"), + method, + opts + ); + Assert.equal(result.artifacts.resource, "/path?query=search"); + Assert.equal( + result.artifacts.mac, + "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho=" + ); + + /* Test handling of the payload, which is supposed to be a bytestring + (String with codepoints from U+0000 to U+00FF, pre-encoded). */ + + result = await compute(makeURI("http://example.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + }); + Assert.equal(result.artifacts.hash, undefined); + Assert.equal( + result.artifacts.mac, + "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY=" + ); + + // Empty payload changes nothing. + result = await compute(makeURI("http://example.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + payload: null, + }); + Assert.equal(result.artifacts.hash, undefined); + Assert.equal( + result.artifacts.mac, + "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY=" + ); + + result = await compute(makeURI("http://example.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + payload: "hello", + }); + Assert.equal( + result.artifacts.hash, + "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA=" + ); + Assert.equal( + result.artifacts.mac, + "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM=" + ); + + // update, utf-8 payload + result = await compute(makeURI("http://example.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + payload: "andré@example.org", // non-ASCII + }); + Assert.equal( + result.artifacts.hash, + "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=" + ); + Assert.equal( + result.artifacts.mac, + "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=" + ); + + /* If "hash" is provided, "payload" is ignored. */ + result = await compute(makeURI("http://example.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=", + payload: "something else", + }); + Assert.equal( + result.artifacts.hash, + "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=" + ); + Assert.equal( + result.artifacts.mac, + "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=" + ); + + // the payload "hash" is also non-urlsafe base64 (+/) + result = await compute(makeURI("http://example.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + payload: "something else", + }); + Assert.equal( + result.artifacts.hash, + "lERFXr/IKOaAoYw+eBseDUSwmqZTX0uKZpcWLxsdzt8=" + ); + Assert.equal( + result.artifacts.mac, + "jiZuhsac35oD7IdcblhFncBr8tJFHcwWLr8NIYWr9PQ=" + ); + + /* Test non-ascii hostname. HAWK (via the node.js "url" module) punycodes + * "ëxample.net" into "xn--xample-ova.net" before hashing. I still think + * punycode was a bad joke that got out of the lab and into a spec. + */ + + result = await compute(makeURI("http://ëxample.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + }); + Assert.equal( + result.artifacts.mac, + "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0=" + ); + Assert.equal(result.artifacts.host, "xn--xample-ova.net"); + + result = await compute(makeURI("http://example.net/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + ext: 'backslash=\\ quote=" EOF', + }); + Assert.equal( + result.artifacts.mac, + "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc=" + ); + Assert.equal( + result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="' + ); + + result = await compute(makeURI("http://example.net:1234/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + }); + Assert.equal( + result.artifacts.mac, + "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE=" + ); + Assert.equal( + result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="' + ); + + /* HAWK (the node.js library) uses a URL parser which stores the "port" + * field as a string, but makeURI() gives us an integer. So we'll diverge + * on ports with a leading zero. This test vector would fail on the node.js + * library (HAWK-1.1.1), where they get a MAC of + * "T+GcAsDO8GRHIvZLeepSvXLwDlFJugcZroAy9+uAtcw=". I think HAWK should be + * updated to do what we do here, so port="01234" should get the same hash + * as port="1234". + */ + result = await compute(makeURI("http://example.net:01234/path"), method, { + credentials, + ts: 1353809207, + nonce: "Ygvqdz", + }); + Assert.equal( + result.artifacts.mac, + "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE=" + ); + Assert.equal( + result.field, + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="' + ); +}); + +add_test(function test_strip_header_attributes() { + let strip = CryptoUtils.stripHeaderAttributes; + + Assert.equal(strip(undefined), ""); + Assert.equal(strip("text/plain"), "text/plain"); + Assert.equal(strip("TEXT/PLAIN"), "text/plain"); + Assert.equal(strip(" text/plain "), "text/plain"); + Assert.equal(strip("text/plain ; charset=utf-8 "), "text/plain"); + + run_next_test(); +}); diff --git a/services/crypto/tests/unit/test_utils_httpmac.js b/services/crypto/tests/unit/test_utils_httpmac.js new file mode 100644 index 0000000000..4831683e70 --- /dev/null +++ b/services/crypto/tests/unit/test_utils_httpmac.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { CryptoUtils } = ChromeUtils.importESModule( + "resource://services-crypto/utils.sys.mjs" +); + +add_test(function setup() { + initTestLogging(); + run_next_test(); +}); + +add_task(async function test_sha1() { + _("Ensure HTTP MAC SHA1 generation works as expected."); + + let id = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7"; + let key = "b8u1cc5iiio5o319og7hh8faf2gi5ym4aq0zwf112cv1287an65fudu5zj7zo7dz"; + let ts = 1329181221; + let method = "GET"; + let nonce = "wGX71"; + let uri = CommonUtils.makeURI("http://10.250.2.176/alias/"); + + let result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, { + ts, + nonce, + }); + + Assert.equal(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck="); + + Assert.equal( + result.getHeader(), + 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' + + 'ts="1329181221", nonce="wGX71", mac="jzh5chjQc2zFEvLbyHnPdX11Yck="' + ); + + let ext = "EXTRA DATA; foo,bar=1"; + + result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, { + ts, + nonce, + ext, + }); + Assert.equal(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68="); + Assert.equal( + result.getHeader(), + 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' + + 'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' + + 'ext="EXTRA DATA; foo,bar=1"' + ); +}); + +add_task(async function test_nonce_length() { + _("Ensure custom nonce lengths are honoured."); + + function get_mac(length) { + let uri = CommonUtils.makeURI("http://example.com/"); + return CryptoUtils.computeHTTPMACSHA1("foo", "bar", "GET", uri, { + nonce_bytes: length, + }); + } + + let result = await get_mac(12); + Assert.equal(12, atob(result.nonce).length); + + result = await get_mac(2); + Assert.equal(2, atob(result.nonce).length); + + result = await get_mac(0); + Assert.equal(8, atob(result.nonce).length); + + result = await get_mac(-1); + Assert.equal(8, atob(result.nonce).length); +}); diff --git a/services/crypto/tests/unit/xpcshell.toml b/services/crypto/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..9ce7748357 --- /dev/null +++ b/services/crypto/tests/unit/xpcshell.toml @@ -0,0 +1,18 @@ +[DEFAULT] +head = "head_helpers.js ../../../common/tests/unit/head_helpers.js" +firefox-appdir = "browser" +support-files = ["!/services/common/tests/unit/head_helpers.js"] + +["test_crypto_crypt.js"] + +["test_crypto_random.js"] +skip-if = ["appname == 'thunderbird'"] + +["test_jwcrypto.js"] +skip-if = ["appname == 'thunderbird'"] + +["test_load_modules.js"] + +["test_utils_hawk.js"] + +["test_utils_httpmac.js"] diff --git a/services/docs/index.rst b/services/docs/index.rst new file mode 100644 index 0000000000..93b9506cb6 --- /dev/null +++ b/services/docs/index.rst @@ -0,0 +1,11 @@ +======== +Services +======== + +This is the nascent documentation of the Firefox services. + +.. toctree:: + :maxdepth: 2 + + settings/index + sync/index diff --git a/services/docs/moz.build b/services/docs/moz.build new file mode 100644 index 0000000000..8eba15268d --- /dev/null +++ b/services/docs/moz.build @@ -0,0 +1,6 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Sync") diff --git a/services/fxaccounts/Credentials.sys.mjs b/services/fxaccounts/Credentials.sys.mjs new file mode 100644 index 0000000000..30c88fafdc --- /dev/null +++ b/services/fxaccounts/Credentials.sys.mjs @@ -0,0 +1,134 @@ +/* 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/. */ + +/** + * This module implements client-side key stretching for use in Firefox + * Accounts account creation and login. + * + * See https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol + */ + +import { Log } from "resource://gre/modules/Log.sys.mjs"; + +import { CryptoUtils } from "resource://services-crypto/utils.sys.mjs"; + +import { CommonUtils } from "resource://services-common/utils.sys.mjs"; + +const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/"; +const PBKDF2_ROUNDS = 1000; +const STRETCHED_PW_LENGTH_BYTES = 32; +const HKDF_SALT = CommonUtils.hexToBytes("00"); +const HKDF_LENGTH = 32; + +// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO", +// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by +// default. +const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel"; +let LOG_LEVEL = Log.Level.Error; +try { + LOG_LEVEL = + Services.prefs.getPrefType(PREF_LOG_LEVEL) == + Ci.nsIPrefBranch.PREF_STRING && + Services.prefs.getStringPref(PREF_LOG_LEVEL); +} catch (e) {} + +var log = Log.repository.getLogger("Identity.FxAccounts"); +log.level = LOG_LEVEL; +log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); + +export var Credentials = Object.freeze({ + /** + * Make constants accessible to tests + */ + constants: { + PROTOCOL_VERSION, + PBKDF2_ROUNDS, + STRETCHED_PW_LENGTH_BYTES, + HKDF_SALT, + HKDF_LENGTH, + }, + + /** + * KW function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol + * + * keyWord derivation for use as a salt. + * + * + * @param {String} context String for use in generating salt + * + * @return {bitArray} the salt + * + * Note that PROTOCOL_VERSION does not refer in any way to the version of the + * Firefox Accounts API. + */ + keyWord(context) { + return CommonUtils.stringToBytes(PROTOCOL_VERSION + context); + }, + + /** + * KWE function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol + * + * keyWord extended with a name and an email. + * + * @param {String} name The name of the salt + * @param {String} email The email of the user. + * + * @return {bitArray} the salt combination with the namespace + * + * Note that PROTOCOL_VERSION does not refer in any way to the version of the + * Firefox Accounts API. + */ + keyWordExtended(name, email) { + return CommonUtils.stringToBytes(PROTOCOL_VERSION + name + ":" + email); + }, + + setup(emailInput, passwordInput, options = {}) { + return new Promise(resolve => { + log.debug("setup credentials for " + emailInput); + + let hkdfSalt = options.hkdfSalt || HKDF_SALT; + let hkdfLength = options.hkdfLength || HKDF_LENGTH; + let stretchedPWLength = + options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES; + let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS; + + let result = {}; + + let password = CommonUtils.encodeUTF8(passwordInput); + let salt = this.keyWordExtended("quickStretch", emailInput); + + let runnable = async () => { + let start = Date.now(); + let quickStretchedPW = await CryptoUtils.pbkdf2Generate( + password, + salt, + pbkdf2Rounds, + stretchedPWLength + ); + + result.quickStretchedPW = quickStretchedPW; + + result.authPW = await CryptoUtils.hkdfLegacy( + quickStretchedPW, + hkdfSalt, + this.keyWord("authPW"), + hkdfLength + ); + + result.unwrapBKey = await CryptoUtils.hkdfLegacy( + quickStretchedPW, + hkdfSalt, + this.keyWord("unwrapBkey"), + hkdfLength + ); + + log.debug("Credentials set up after " + (Date.now() - start) + " ms"); + resolve(result); + }; + + Services.tm.dispatchToMainThread(runnable); + log.debug("Dispatched thread for credentials setup crypto work"); + }); + }, +}); diff --git a/services/fxaccounts/FxAccounts.sys.mjs b/services/fxaccounts/FxAccounts.sys.mjs new file mode 100644 index 0000000000..18169c6b2d --- /dev/null +++ b/services/fxaccounts/FxAccounts.sys.mjs @@ -0,0 +1,1657 @@ +/* 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 { CryptoUtils } from "resource://services-crypto/utils.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +import { FxAccountsStorageManager } from "resource://gre/modules/FxAccountsStorage.sys.mjs"; + +import { + ERRNO_INVALID_AUTH_TOKEN, + ERROR_AUTH_ERROR, + ERROR_INVALID_PARAMETER, + ERROR_NO_ACCOUNT, + ERROR_TO_GENERAL_ERROR_CLASS, + ERROR_UNKNOWN, + ERROR_UNVERIFIED_ACCOUNT, + FXA_PWDMGR_PLAINTEXT_FIELDS, + FXA_PWDMGR_REAUTH_ALLOWLIST, + FXA_PWDMGR_SECURE_FIELDS, + FX_OAUTH_CLIENT_ID, + ON_ACCOUNT_STATE_CHANGE_NOTIFICATION, + ONLOGIN_NOTIFICATION, + ONLOGOUT_NOTIFICATION, + ON_PRELOGOUT_NOTIFICATION, + ONVERIFIED_NOTIFICATION, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + POLL_SESSION, + PREF_ACCOUNT_ROOT, + PREF_LAST_FXA_USER, + SERVER_ERRNO_TO_ERROR, + log, + logPII, + logManager, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs", + FxAccountsCommands: "resource://gre/modules/FxAccountsCommands.sys.mjs", + FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs", + FxAccountsDevice: "resource://gre/modules/FxAccountsDevice.sys.mjs", + FxAccountsKeys: "resource://gre/modules/FxAccountsKeys.sys.mjs", + FxAccountsOAuth: "resource://gre/modules/FxAccountsOAuth.sys.mjs", + FxAccountsProfile: "resource://gre/modules/FxAccountsProfile.sys.mjs", + FxAccountsTelemetry: "resource://gre/modules/FxAccountsTelemetry.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "mpLocked", () => { + return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs") + .Utils.mpLocked; +}); + +ChromeUtils.defineLazyGetter(lazy, "ensureMPUnlocked", () => { + return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs") + .Utils.ensureMPUnlocked; +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "FXA_ENABLED", + "identity.fxaccounts.enabled", + true +); + +// An AccountState object holds all state related to one specific account. +// It is considered "private" to the FxAccounts modules. +// Only one AccountState is ever "current" in the FxAccountsInternal object - +// whenever a user logs out or logs in, the current AccountState is discarded, +// making it impossible for the wrong state or state data to be accidentally +// used. +// In addition, it has some promise-related helpers to ensure that if an +// attempt is made to resolve a promise on a "stale" state (eg, if an +// operation starts, but a different user logs in before the operation +// completes), the promise will be rejected. +// It is intended to be used thusly: +// somePromiseBasedFunction: function() { +// let currentState = this.currentAccountState; +// return someOtherPromiseFunction().then( +// data => currentState.resolve(data) +// ); +// } +// If the state has changed between the function being called and the promise +// being resolved, the .resolve() call will actually be rejected. +export function AccountState(storageManager) { + this.storageManager = storageManager; + this.inFlightTokenRequests = new Map(); + this.promiseInitialized = this.storageManager + .getAccountData() + .then(data => { + this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {}; + }) + .catch(err => { + log.error("Failed to initialize the storage manager", err); + // Things are going to fall apart, but not much we can do about it here. + }); +} + +AccountState.prototype = { + oauthTokens: null, + whenVerifiedDeferred: null, + whenKeysReadyDeferred: null, + + // If the storage manager has been nuked then we are no longer current. + get isCurrent() { + return this.storageManager != null; + }, + + abort() { + if (this.whenVerifiedDeferred) { + this.whenVerifiedDeferred.reject( + new Error("Verification aborted; Another user signing in") + ); + this.whenVerifiedDeferred = null; + } + if (this.whenKeysReadyDeferred) { + this.whenKeysReadyDeferred.reject( + new Error("Key fetching aborted; Another user signing in") + ); + this.whenKeysReadyDeferred = null; + } + this.inFlightTokenRequests.clear(); + return this.signOut(); + }, + + // Clobber all cached data and write that empty data to storage. + async signOut() { + this.cert = null; + this.keyPair = null; + this.oauthTokens = null; + this.inFlightTokenRequests.clear(); + + // Avoid finalizing the storageManager multiple times (ie, .signOut() + // followed by .abort()) + if (!this.storageManager) { + return; + } + const storageManager = this.storageManager; + this.storageManager = null; + + await storageManager.deleteAccountData(); + await storageManager.finalize(); + }, + + // Get user account data. Optionally specify explicit field names to fetch + // (and note that if you require an in-memory field you *must* specify the + // field name(s).) + getUserAccountData(fieldNames = null) { + if (!this.isCurrent) { + return Promise.reject(new Error("Another user has signed in")); + } + return this.storageManager.getAccountData(fieldNames).then(result => { + return this.resolve(result); + }); + }, + + async updateUserAccountData(updatedFields) { + if ("uid" in updatedFields) { + const existing = await this.getUserAccountData(["uid"]); + if (existing.uid != updatedFields.uid) { + throw new Error( + "The specified credentials aren't for the current user" + ); + } + // We need to nuke uid as storage will complain if we try and + // update it (even when the value is the same) + updatedFields = Cu.cloneInto(updatedFields, {}); // clone it first + delete updatedFields.uid; + } + if (!this.isCurrent) { + return Promise.reject(new Error("Another user has signed in")); + } + return this.storageManager.updateAccountData(updatedFields); + }, + + resolve(result) { + if (!this.isCurrent) { + log.info( + "An accountState promise was resolved, but was actually rejected" + + " due to a different user being signed in. Originally resolved" + + " with", + result + ); + return Promise.reject(new Error("A different user signed in")); + } + return Promise.resolve(result); + }, + + reject(error) { + // It could be argued that we should just let it reject with the original + // error - but this runs the risk of the error being (eg) a 401, which + // might cause the consumer to attempt some remediation and cause other + // problems. + if (!this.isCurrent) { + log.info( + "An accountState promise was rejected, but we are ignoring that " + + "reason and rejecting it due to a different user being signed in. " + + "Originally rejected with", + error + ); + return Promise.reject(new Error("A different user signed in")); + } + return Promise.reject(error); + }, + + // Abstractions for storage of cached tokens - these are all sync, and don't + // handle revocation etc - it's just storage (and the storage itself is async, + // but we don't return the storage promises, so it *looks* sync) + // These functions are sync simply so we can handle "token races" - when there + // are multiple in-flight requests for the same scope, we can detect this + // and revoke the redundant token. + + // A preamble for the cache helpers... + _cachePreamble() { + if (!this.isCurrent) { + throw new Error("Another user has signed in"); + } + }, + + // Set a cached token. |tokenData| must have a 'token' element, but may also + // have additional fields. + // The 'get' functions below return the entire |tokenData| value. + setCachedToken(scopeArray, tokenData) { + this._cachePreamble(); + if (!tokenData.token) { + throw new Error("No token"); + } + let key = getScopeKey(scopeArray); + this.oauthTokens[key] = tokenData; + // And a background save... + this._persistCachedTokens(); + }, + + // Return data for a cached token or null (or throws on bad state etc) + getCachedToken(scopeArray) { + this._cachePreamble(); + let key = getScopeKey(scopeArray); + let result = this.oauthTokens[key]; + if (result) { + // later we might want to check an expiry date - but we currently + // have no such concept, so just return it. + log.trace("getCachedToken returning cached token"); + return result; + } + return null; + }, + + // Remove a cached token from the cache. Does *not* revoke it from anywhere. + // Returns the entire token entry if found, null otherwise. + removeCachedToken(token) { + this._cachePreamble(); + let data = this.oauthTokens; + for (let [key, tokenValue] of Object.entries(data)) { + if (tokenValue.token == token) { + delete data[key]; + // And a background save... + this._persistCachedTokens(); + return tokenValue; + } + } + return null; + }, + + // A hook-point for tests. Returns a promise that's ignored in most cases + // (notable exceptions are tests and when we explicitly are saving the entire + // set of user data.) + _persistCachedTokens() { + this._cachePreamble(); + return this.updateUserAccountData({ oauthTokens: this.oauthTokens }).catch( + err => { + log.error("Failed to update cached tokens", err); + } + ); + }, +}; + +/* Given an array of scopes, make a string key by normalizing. */ +function getScopeKey(scopeArray) { + let normalizedScopes = scopeArray.map(item => item.toLowerCase()); + return normalizedScopes.sort().join("|"); +} + +function getPropertyDescriptor(obj, prop) { + return ( + Object.getOwnPropertyDescriptor(obj, prop) || + getPropertyDescriptor(Object.getPrototypeOf(obj), prop) + ); +} + +/** + * Copies properties from a given object to another object. + * + * @param from (object) + * The object we read property descriptors from. + * @param to (object) + * The object that we set property descriptors on. + * @param thisObj (object) + * The object that will be used to .bind() all function properties we find to. + * @param keys ([...]) + * The names of all properties to be copied. + */ +function copyObjectProperties(from, to, thisObj, keys) { + for (let prop of keys) { + // Look for the prop in the prototype chain. + let desc = getPropertyDescriptor(from, prop); + + if (typeof desc.value == "function") { + desc.value = desc.value.bind(thisObj); + } + + if (desc.get) { + desc.get = desc.get.bind(thisObj); + } + + if (desc.set) { + desc.set = desc.set.bind(thisObj); + } + + Object.defineProperty(to, prop, desc); + } +} + +/** + * The public API. + * + * TODO - *all* non-underscore stuff here should have sphinx docstrings so + * that docs magically appear on https://firefox-source-docs.mozilla.org/ + * (although |./mach doc| is broken on windows (bug 1232403) and on Linux for + * markh (some obscure npm issue he gave up on) - so later...) + */ +export class FxAccounts { + constructor(mocks = null) { + this._internal = new FxAccountsInternal(); + if (mocks) { + // it's slightly unfortunate that we need to mock the main "internal" object + // before calling initialize, primarily so a mock `newAccountState` is in + // place before initialize calls it, but we need to initialize the + // "sub-object" mocks after. This can probably be fixed, but whatever... + copyObjectProperties( + mocks, + this._internal, + this._internal, + Object.keys(mocks).filter(key => !["device", "commands"].includes(key)) + ); + } + this._internal.initialize(); + // allow mocking our "sub-objects" too. + if (mocks) { + for (let subobject of [ + "currentAccountState", + "keys", + "fxaPushService", + "device", + "commands", + ]) { + if (typeof mocks[subobject] == "object") { + copyObjectProperties( + mocks[subobject], + this._internal[subobject], + this._internal[subobject], + Object.keys(mocks[subobject]) + ); + } + } + } + } + + get commands() { + return this._internal.commands; + } + + static get config() { + return lazy.FxAccountsConfig; + } + + get device() { + return this._internal.device; + } + + get keys() { + return this._internal.keys; + } + + get telemetry() { + return this._internal.telemetry; + } + + _withCurrentAccountState(func) { + return this._internal.withCurrentAccountState(func); + } + + _withVerifiedAccountState(func) { + return this._internal.withVerifiedAccountState(func); + } + + _withSessionToken(func, mustBeVerified = true) { + return this._internal.withSessionToken(func, mustBeVerified); + } + + /** + * Returns an array listing all the OAuth clients connected to the + * authenticated user's account. This includes browsers and web sessions - no + * filtering is done of the set returned by the FxA server. + * + * @typedef {Object} AttachedClient + * @property {String} id - OAuth `client_id` of the client. + * @property {Number} lastAccessedDaysAgo - How many days ago the client last + * accessed the FxA server APIs. + * + * @returns {Array.} A list of attached clients. + */ + async listAttachedOAuthClients() { + // We expose last accessed times in 'days ago' + const ONE_DAY = 24 * 60 * 60 * 1000; + + return this._withSessionToken(async sessionToken => { + const response = await this._internal.fxAccountsClient.attachedClients( + sessionToken + ); + const attachedClients = response.body; + const timestamp = response.headers["x-timestamp"]; + const now = + timestamp !== undefined + ? new Date(parseInt(timestamp, 10)) + : Date.now(); + return attachedClients.map(client => { + const daysAgo = client.lastAccessTime + ? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0) + : null; + return { + id: client.clientId, + lastAccessedDaysAgo: daysAgo, + }; + }); + }); + } + + /** + * Get an OAuth token for the user. + * + * @param options + * { + * scope: (string/array) the oauth scope(s) being requested. As a + * convenience, you may pass a string if only one scope is + * required, or an array of strings if multiple are needed. + * ttl: (number) OAuth token TTL in seconds. + * } + * + * @return Promise. + * The promise resolves the oauth token as a string or rejects with + * an error object ({error: ERROR, details: {}}) of the following: + * INVALID_PARAMETER + * NO_ACCOUNT + * UNVERIFIED_ACCOUNT + * NETWORK_ERROR + * AUTH_ERROR + * UNKNOWN_ERROR + */ + async getOAuthToken(options = {}) { + try { + return await this._internal.getOAuthToken(options); + } catch (err) { + throw this._internal._errorToErrorClass(err); + } + } + + /** + * Remove an OAuth token from the token cache. Callers should call this + * after they determine a token is invalid, so a new token will be fetched + * on the next call to getOAuthToken(). + * + * @param options + * { + * token: (string) A previously fetched token. + * } + * @return Promise. This function will always resolve, even if + * an unknown token is passed. + */ + removeCachedOAuthToken(options) { + return this._internal.removeCachedOAuthToken(options); + } + + /** + * Get details about the user currently signed in to Firefox Accounts. + * + * @return Promise + * The promise resolves to the credentials object of the signed-in user: + * { + * email: String: The user's email address + * uid: String: The user's unique id + * verified: Boolean: email verification status + * displayName: String or null if not known. + * avatar: URL of the avatar for the user. May be the default + * avatar, or null in edge-cases (eg, if there's an account + * issue, etc + * avatarDefault: boolean - whether `avatar` is specific to the user + * or the default avatar. + * } + * + * or null if no user is signed in. This function never fails except + * in pathological cases (eg, file-system errors, etc) + */ + getSignedInUser() { + // Note we don't return the session token, but use it to see if we + // should fetch the profile. + const ACCT_DATA_FIELDS = ["email", "uid", "verified", "sessionToken"]; + const PROFILE_FIELDS = ["displayName", "avatar", "avatarDefault"]; + return this._withCurrentAccountState(async currentState => { + const data = await currentState.getUserAccountData(ACCT_DATA_FIELDS); + if (!data) { + return null; + } + if (!lazy.FXA_ENABLED) { + await this.signOut(); + return null; + } + if (!this._internal.isUserEmailVerified(data)) { + // If the email is not verified, start polling for verification, + // but return null right away. We don't want to return a promise + // that might not be fulfilled for a long time. + this._internal.startVerifiedCheck(data); + } + + let profileData = null; + if (data.sessionToken) { + delete data.sessionToken; + try { + profileData = await this._internal.profile.getProfile(); + } catch (error) { + log.error("Could not retrieve profile data", error); + } + } + for (let field of PROFILE_FIELDS) { + data[field] = profileData ? profileData[field] : null; + } + // and email is a special case - if we have profile data we prefer the + // email from that, as the email we stored for the account itself might + // not have been updated if the email changed since the user signed in. + if (profileData && profileData.email) { + data.email = profileData.email; + } + return data; + }); + } + + /** + * Checks the status of the account. Resolves with Promise, where + * true indicates the account status is OK and false indicates there's some + * issue with the account - either that there's no user currently signed in, + * the entire account has been deleted (in which case there will be no user + * signed in after this call returns), or that the user must reauthenticate (in + * which case `this.hasLocalSession()` will return `false` after this call + * returns). + * + * Typically used when some external code which uses, for example, oauth tokens + * received a 401 error using the token, or that this external code has some + * other reason to believe the account status may be bad. Note that this will + * be called automatically in many cases - for example, if calls to fetch the + * profile, or fetch keys, etc return a 401, there's no need to call this + * function. + * + * Because this hits the server, you should only call this method when you have + * good reason to believe the session very recently became invalid (eg, because + * you saw an auth related exception from a remote service.) + */ + checkAccountStatus() { + // Note that we don't use _withCurrentAccountState here because that will + // cause an exception to be thrown if we end up signing out due to the + // account not existing, which isn't what we want here. + let state = this._internal.currentAccountState; + return this._internal.checkAccountStatus(state); + } + + /** + * Checks if we have a valid local session state for the current account. + * + * @return Promise + * Resolves with a boolean, with true indicating that we appear to + * have a valid local session, or false if we need to reauthenticate + * with the content server to obtain one. + * Note that this only checks local state, although typically that's + * OK, because we drop the local session information whenever we detect + * we are in this state. However, see checkAccountStatus() for a way to + * check the account and session status with the server, which can be + * considered the canonical, albiet expensive, way to determine the + * status of the account. + */ + hasLocalSession() { + return this._withCurrentAccountState(async state => { + let data = await state.getUserAccountData(["sessionToken"]); + return !!(data && data.sessionToken); + }); + } + + /** Returns a promise that resolves to true if we can currently connect (ie, + * sign in, or re-connect after a password change) to a Firefox Account. + * If this returns false, the caller can assume that some UI was shown + * which tells the user why we could not connect. + * + * Currently, the primary password being locked is the only reason why + * this returns false, and in this scenario, the primary password unlock + * dialog will have been shown. + * + * This currently doesn't need to return a promise, but does so that + * future enhancements, such as other explanatory UI which requires + * async can work without modification of the call-sites. + */ + static canConnectAccount() { + return Promise.resolve(!lazy.mpLocked() || lazy.ensureMPUnlocked()); + } + + /** + * Send a message to a set of devices in the same account + * + * @param deviceIds: (null/string/array) The device IDs to send the message to. + * If null, will be sent to all devices. + * + * @param excludedIds: (null/string/array) If deviceIds is null, this may + * list device IDs which should not receive the message. + * + * @param payload: (object) The payload, which will be JSON.stringified. + * + * @param TTL: How long the message should be retained before it is discarded. + */ + // XXX - used only by sync to tell other devices that the clients collection + // has changed so they should sync asap. The API here is somewhat vague (ie, + // "an object"), but to be useful across devices, the payload really needs + // formalizing. We should try and do something better here. + notifyDevices(deviceIds, excludedIds, payload, TTL) { + return this._internal.notifyDevices(deviceIds, excludedIds, payload, TTL); + } + + /** + * Resend the verification email for the currently signed-in user. + * + */ + resendVerificationEmail() { + return this._withSessionToken((token, currentState) => { + this._internal.startPollEmailStatus(currentState, token, "start"); + return this._internal.fxAccountsClient.resendVerificationEmail(token); + }, false); + } + + async signOut(localOnly) { + // Note that we do not use _withCurrentAccountState here, otherwise we + // end up with an exception due to the user signing out before the call is + // complete - but that's the entire point of this method :) + return this._internal.signOut(localOnly); + } + + // XXX - we should consider killing this - the only reason it is public is + // so that sync can change it when it notices the device name being changed, + // and that could probably be replaced with a pref observer. + updateDeviceRegistration() { + return this._withCurrentAccountState(_ => { + return this._internal.updateDeviceRegistration(); + }); + } + + // we should try and kill this too. + whenVerified(data) { + return this._withCurrentAccountState(_ => { + return this._internal.whenVerified(data); + }); + } + + /** + * Generate a log file for the FxA action that just completed + * and refresh the input & output streams. + */ + async flushLogFile() { + const logType = await logManager.resetFileLog(); + if (logType == logManager.ERROR_LOG_WRITTEN) { + console.error( + "FxA encountered an error - see about:sync-log for the log file." + ); + } + Services.obs.notifyObservers(null, "service:log-manager:flush-log-file"); + } +} + +var FxAccountsInternal = function () {}; + +/** + * The internal API's prototype. + */ +FxAccountsInternal.prototype = { + // Make a local copy of this constant so we can mock it in testing + POLL_SESSION, + + // The timeout (in ms) we use to poll for a verified mail for the first + // VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD minutes if the user has + // logged-in in this session. + VERIFICATION_POLL_TIMEOUT_INITIAL: 60000, // 1 minute. + // All the other cases (> 5 min, on restart etc). + VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 5 * 60000, // 5 minutes. + // After X minutes, the polling will slow down to _SUBSEQUENT if we have + // logged-in in this session. + VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD: 5, + + _fxAccountsClient: null, + + // All significant initialization should be done in this initialize() method + // to help with our mocking story. + initialize() { + ChromeUtils.defineLazyGetter(this, "fxaPushService", function () { + return Cc["@mozilla.org/fxaccounts/push;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + }); + + this.keys = new lazy.FxAccountsKeys(this); + + if (!this.observerPreloads) { + // A registry of promise-returning functions that `notifyObservers` should + // call before sending notifications. Primarily used so parts of Firefox + // which have yet to load for performance reasons can be force-loaded, and + // thus not miss notifications. + this.observerPreloads = [ + // Sync + () => { + let { Weave } = ChromeUtils.importESModule( + "resource://services-sync/main.sys.mjs" + ); + return Weave.Service.promiseInitialized; + }, + ]; + } + + this.currentTimer = null; + // This object holds details about, and storage for, the current user. It + // is replaced when a different user signs in. Instead of using it directly, + // you should try and use `withCurrentAccountState`. + this.currentAccountState = this.newAccountState(); + }, + + async withCurrentAccountState(func) { + const state = this.currentAccountState; + let result; + try { + result = await func(state); + } catch (ex) { + return state.reject(ex); + } + return state.resolve(result); + }, + + async withVerifiedAccountState(func) { + return this.withCurrentAccountState(async state => { + let data = await state.getUserAccountData(); + if (!data) { + // No signed-in user + throw this._error(ERROR_NO_ACCOUNT); + } + + if (!this.isUserEmailVerified(data)) { + // Signed-in user has not verified email + throw this._error(ERROR_UNVERIFIED_ACCOUNT); + } + return func(state); + }); + }, + + async withSessionToken(func, mustBeVerified = true) { + const state = this.currentAccountState; + let data = await state.getUserAccountData(); + if (!data) { + // No signed-in user + throw this._error(ERROR_NO_ACCOUNT); + } + + if (mustBeVerified && !this.isUserEmailVerified(data)) { + // Signed-in user has not verified email + throw this._error(ERROR_UNVERIFIED_ACCOUNT); + } + + if (!data.sessionToken) { + throw this._error(ERROR_AUTH_ERROR, "no session token"); + } + try { + // Anyone who needs the session token is going to send it to the server, + // so there's a chance we'll see an auth related error - so handle that + // here rather than requiring each caller to remember to. + let result = await func(data.sessionToken, state); + return state.resolve(result); + } catch (err) { + return this._handleTokenError(err); + } + }, + + get fxAccountsClient() { + if (!this._fxAccountsClient) { + this._fxAccountsClient = new lazy.FxAccountsClient(); + } + return this._fxAccountsClient; + }, + + // The profile object used to fetch the actual user profile. + _profile: null, + get profile() { + if (!this._profile) { + let profileServerUrl = Services.urlFormatter.formatURLPref( + "identity.fxaccounts.remote.profile.uri" + ); + this._profile = new lazy.FxAccountsProfile({ + fxa: this, + profileServerUrl, + }); + } + return this._profile; + }, + + _commands: null, + get commands() { + if (!this._commands) { + this._commands = new lazy.FxAccountsCommands(this); + } + return this._commands; + }, + + _device: null, + get device() { + if (!this._device) { + this._device = new lazy.FxAccountsDevice(this); + } + return this._device; + }, + + _oauth: null, + get oauth() { + if (!this._oauth) { + this._oauth = new lazy.FxAccountsOAuth(this.fxAccountsClient); + } + return this._oauth; + }, + + _telemetry: null, + get telemetry() { + if (!this._telemetry) { + this._telemetry = new lazy.FxAccountsTelemetry(this); + } + return this._telemetry; + }, + + beginOAuthFlow(scopes) { + return this.oauth.beginOAuthFlow(scopes); + }, + + completeOAuthFlow(sessionToken, code, state) { + return this.oauth.completeOAuthFlow(sessionToken, code, state); + }, + + setScopedKeys(scopedKeys) { + return this.keys.setScopedKeys(scopedKeys); + }, + + // A hook-point for tests who may want a mocked AccountState or mocked storage. + newAccountState(credentials) { + let storage = new FxAccountsStorageManager(); + storage.initialize(credentials); + return new AccountState(storage); + }, + + notifyDevices(deviceIds, excludedIds, payload, TTL) { + if (typeof deviceIds == "string") { + deviceIds = [deviceIds]; + } + return this.withSessionToken(sessionToken => { + return this.fxAccountsClient.notifyDevices( + sessionToken, + deviceIds, + excludedIds, + payload, + TTL + ); + }); + }, + + /** + * Return the current time in milliseconds as an integer. Allows tests to + * manipulate the date to simulate token expiration. + */ + now() { + return this.fxAccountsClient.now(); + }, + + /** + * Return clock offset in milliseconds, as reported by the fxAccountsClient. + * This can be overridden for testing. + * + * The offset is the number of milliseconds that must be added to the client + * clock to make it equal to the server clock. For example, if the client is + * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. + */ + get localtimeOffsetMsec() { + return this.fxAccountsClient.localtimeOffsetMsec; + }, + + /** + * Ask the server whether the user's email has been verified + */ + checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) { + if (!sessionToken) { + return Promise.reject( + new Error("checkEmailStatus called without a session token") + ); + } + return this.fxAccountsClient + .recoveryEmailStatus(sessionToken, options) + .catch(error => this._handleTokenError(error)); + }, + + // set() makes sure that polling is happening, if necessary. + // get() does not wait for verification, and returns an object even if + // unverified. The caller of get() must check .verified . + // The "fxaccounts:onverified" event will fire only when the verified + // state goes from false to true, so callers must register their observer + // and then call get(). In particular, it will not fire when the account + // was found to be verified in a previous boot: if our stored state says + // the account is verified, the event will never fire. So callers must do: + // register notification observer (go) + // userdata = get() + // if (userdata.verified()) {go()} + + /** + * Set the current user signed in to Firefox Accounts. + * + * @param credentials + * The credentials object obtained by logging in or creating + * an account on the FxA server: + * { + * authAt: The time (seconds since epoch) that this record was + * authenticated + * email: The users email address + * keyFetchToken: a keyFetchToken which has not yet been used + * sessionToken: Session for the FxA server + * uid: The user's unique id + * unwrapBKey: used to unwrap kB, derived locally from the + * password (not revealed to the FxA server) + * verified: true/false + * } + * @return Promise + * The promise resolves to null when the data is saved + * successfully and is rejected on error. + */ + async setSignedInUser(credentials) { + if (!lazy.FXA_ENABLED) { + throw new Error("Cannot call setSignedInUser when FxA is disabled."); + } + for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) { + Services.prefs.clearUserPref(pref); + } + log.debug("setSignedInUser - aborting any existing flows"); + const signedInUser = await this.currentAccountState.getUserAccountData(); + if (signedInUser) { + await this._signOutServer( + signedInUser.sessionToken, + signedInUser.oauthTokens + ); + } + await this.abortExistingFlow(); + let currentAccountState = (this.currentAccountState = this.newAccountState( + Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object. + )); + // This promise waits for storage, but not for verification. + // We're telling the caller that this is durable now (although is that + // really something we should commit to? Why not let the write happen in + // the background? Already does for updateAccountData ;) + await currentAccountState.promiseInitialized; + // Starting point for polling if new user + if (!this.isUserEmailVerified(credentials)) { + this.startVerifiedCheck(credentials); + } + await this.notifyObservers(ONLOGIN_NOTIFICATION); + await this.updateDeviceRegistration(); + return currentAccountState.resolve(); + }, + + /** + * Update account data for the currently signed in user. + * + * @param credentials + * The credentials object containing the fields to be updated. + * This object must contain the |uid| field and it must + * match the currently signed in user. + */ + updateUserAccountData(credentials) { + log.debug( + "updateUserAccountData called with fields", + Object.keys(credentials) + ); + if (logPII()) { + log.debug("updateUserAccountData called with data", credentials); + } + let currentAccountState = this.currentAccountState; + return currentAccountState.promiseInitialized.then(() => { + if (!credentials.uid) { + throw new Error("The specified credentials have no uid"); + } + return currentAccountState.updateUserAccountData(credentials); + }); + }, + + /* + * Reset state such that any previous flow is canceled. + */ + abortExistingFlow() { + if (this.currentTimer) { + log.debug("Polling aborted; Another user signing in"); + clearTimeout(this.currentTimer); + this.currentTimer = 0; + } + if (this._profile) { + this._profile.tearDown(); + this._profile = null; + } + if (this._commands) { + this._commands = null; + } + if (this._device) { + this._device.reset(); + } + // We "abort" the accountState and assume our caller is about to throw it + // away and replace it with a new one. + return this.currentAccountState.abort(); + }, + + async checkVerificationStatus() { + log.trace("checkVerificationStatus"); + let state = this.currentAccountState; + let data = await state.getUserAccountData(); + if (!data) { + log.trace("checkVerificationStatus - no user data"); + return null; + } + + // Always check the verification status, even if the local state indicates + // we're already verified. If the user changed their password, the check + // will fail, and we'll enter the reauth state. + log.trace("checkVerificationStatus - forcing verification status check"); + return this.startPollEmailStatus(state, data.sessionToken, "push"); + }, + + /** Destroyes an OAuth Token by sending a request to the FxA server + * @param { Object } tokenData: The token's data, with `tokenData.token` being the token itself + **/ + destroyOAuthToken(tokenData) { + return this.fxAccountsClient.oauthDestroy( + FX_OAUTH_CLIENT_ID, + tokenData.token + ); + }, + + _destroyAllOAuthTokens(tokenInfos) { + if (!tokenInfos) { + return Promise.resolve(); + } + // let's just destroy them all in parallel... + let promises = []; + for (let tokenInfo of Object.values(tokenInfos)) { + promises.push(this.destroyOAuthToken(tokenInfo)); + } + return Promise.all(promises); + }, + + async signOut(localOnly) { + let sessionToken; + let tokensToRevoke; + const data = await this.currentAccountState.getUserAccountData(); + // Save the sessionToken, tokens before resetting them in _signOutLocal(). + if (data) { + sessionToken = data.sessionToken; + tokensToRevoke = data.oauthTokens; + } + await this.notifyObservers(ON_PRELOGOUT_NOTIFICATION); + await this._signOutLocal(); + if (!localOnly) { + // Do this in the background so *any* slow request won't + // block the local sign out. + Services.tm.dispatchToMainThread(async () => { + await this._signOutServer(sessionToken, tokensToRevoke); + lazy.FxAccountsConfig.resetConfigURLs(); + this.notifyObservers("testhelper-fxa-signout-complete"); + }); + } else { + // We want to do this either way -- but if we're signing out remotely we + // need to wait until we destroy the oauth tokens if we want that to succeed. + lazy.FxAccountsConfig.resetConfigURLs(); + } + return this.notifyObservers(ONLOGOUT_NOTIFICATION); + }, + + async _signOutLocal() { + for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) { + Services.prefs.clearUserPref(pref); + } + await this.currentAccountState.signOut(); + // this "aborts" this.currentAccountState but doesn't make a new one. + await this.abortExistingFlow(); + this.currentAccountState = this.newAccountState(); + return this.currentAccountState.promiseInitialized; + }, + + async _signOutServer(sessionToken, tokensToRevoke) { + log.debug("Unsubscribing from FxA push."); + try { + await this.fxaPushService.unsubscribe(); + } catch (err) { + log.error("Could not unsubscribe from push.", err); + } + if (sessionToken) { + log.debug("Destroying session and device."); + try { + await this.fxAccountsClient.signOut(sessionToken, { service: "sync" }); + } catch (err) { + log.error("Error during remote sign out of Firefox Accounts", err); + } + } else { + log.warn("Missing session token; skipping remote sign out"); + } + log.debug("Destroying all OAuth tokens."); + try { + await this._destroyAllOAuthTokens(tokensToRevoke); + } catch (err) { + log.error("Error during destruction of oauth tokens during signout", err); + } + }, + + getUserAccountData(fieldNames = null) { + return this.currentAccountState.getUserAccountData(fieldNames); + }, + + isUserEmailVerified: function isUserEmailVerified(data) { + return !!(data && data.verified); + }, + + /** + * Setup for and if necessary do email verification polling. + */ + loadAndPoll() { + let currentState = this.currentAccountState; + return currentState.getUserAccountData().then(data => { + if (data) { + if (!this.isUserEmailVerified(data)) { + this.startPollEmailStatus( + currentState, + data.sessionToken, + "browser-startup" + ); + } + } + return data; + }); + }, + + startVerifiedCheck(data) { + log.debug("startVerifiedCheck", data && data.verified); + if (logPII()) { + log.debug("startVerifiedCheck with user data", data); + } + + // Get us to the verified state. This returns a promise that will fire when + // verification is complete. + + // The callers of startVerifiedCheck never consume a returned promise (ie, + // this is simply kicking off a background fetch) so we must add a rejection + // handler to avoid runtime warnings about the rejection not being handled. + this.whenVerified(data).catch(err => + log.info("startVerifiedCheck promise was rejected: " + err) + ); + }, + + whenVerified(data) { + let currentState = this.currentAccountState; + if (data.verified) { + log.debug("already verified"); + return currentState.resolve(data); + } + if (!currentState.whenVerifiedDeferred) { + log.debug("whenVerified promise starts polling for verified email"); + this.startPollEmailStatus(currentState, data.sessionToken, "start"); + } + return currentState.whenVerifiedDeferred.promise.then(result => + currentState.resolve(result) + ); + }, + + async notifyObservers(topic, data) { + for (let f of this.observerPreloads) { + try { + await f(); + } catch (O_o) {} + } + log.debug("Notifying observers of " + topic); + Services.obs.notifyObservers(null, topic, data); + }, + + startPollEmailStatus(currentState, sessionToken, why) { + log.debug("entering startPollEmailStatus: " + why); + // If we were already polling, stop and start again. This could happen + // if the user requested the verification email to be resent while we + // were already polling for receipt of an earlier email. + if (this.currentTimer) { + log.debug( + "startPollEmailStatus starting while existing timer is running" + ); + clearTimeout(this.currentTimer); + this.currentTimer = null; + } + + this.pollStartDate = Date.now(); + if (!currentState.whenVerifiedDeferred) { + currentState.whenVerifiedDeferred = Promise.withResolvers(); + // This deferred might not end up with any handlers (eg, if sync + // is yet to start up.) This might cause "A promise chain failed to + // handle a rejection" messages, so add an error handler directly + // on the promise to log the error. + currentState.whenVerifiedDeferred.promise.then( + () => { + log.info("the user became verified"); + // We are now ready for business. This should only be invoked once + // per setSignedInUser(), regardless of whether we've rebooted since + // setSignedInUser() was called. + this.notifyObservers(ONVERIFIED_NOTIFICATION); + }, + err => { + log.info("the wait for user verification was stopped: " + err); + } + ); + } + return this.pollEmailStatus(currentState, sessionToken, why); + }, + + // We return a promise for testing only. Other callers can ignore this, + // since verification polling continues in the background. + async pollEmailStatus(currentState, sessionToken, why) { + log.debug("entering pollEmailStatus: " + why); + let nextPollMs; + try { + const response = await this.checkEmailStatus(sessionToken, { + reason: why, + }); + log.debug("checkEmailStatus -> " + JSON.stringify(response)); + if (response && response.verified) { + await this.onPollEmailSuccess(currentState); + return; + } + } catch (error) { + if (error && error.code && error.code == 401) { + let error = new Error("Verification status check failed"); + this._rejectWhenVerified(currentState, error); + return; + } + if (error && error.retryAfter) { + // If the server told us to back off, back off the requested amount. + nextPollMs = (error.retryAfter + 3) * 1000; + log.warn( + `the server rejected our email status check and told us to try again in ${nextPollMs}ms` + ); + } else { + log.error(`checkEmailStatus failed to poll`, error); + } + } + if (why == "push") { + return; + } + let pollDuration = Date.now() - this.pollStartDate; + // Polling session expired. + if (pollDuration >= this.POLL_SESSION) { + if (currentState.whenVerifiedDeferred) { + let error = new Error("User email verification timed out."); + this._rejectWhenVerified(currentState, error); + } + log.debug("polling session exceeded, giving up"); + return; + } + // Poll email status again after a short delay. + if (nextPollMs === undefined) { + let currentMinute = Math.ceil(pollDuration / 60000); + nextPollMs = + why == "start" && + currentMinute < this.VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD + ? this.VERIFICATION_POLL_TIMEOUT_INITIAL + : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT; + } + this._scheduleNextPollEmailStatus( + currentState, + sessionToken, + nextPollMs, + why + ); + }, + + // Easy-to-mock testable method + _scheduleNextPollEmailStatus(currentState, sessionToken, nextPollMs, why) { + log.debug("polling with timeout = " + nextPollMs); + this.currentTimer = setTimeout(() => { + this.pollEmailStatus(currentState, sessionToken, why); + }, nextPollMs); + }, + + async onPollEmailSuccess(currentState) { + try { + await currentState.updateUserAccountData({ verified: true }); + const accountData = await currentState.getUserAccountData(); + this._setLastUserPref(accountData.email); + // Now that the user is verified, we can proceed to fetch keys + if (currentState.whenVerifiedDeferred) { + currentState.whenVerifiedDeferred.resolve(accountData); + delete currentState.whenVerifiedDeferred; + } + } catch (e) { + log.error(e); + } + }, + + _rejectWhenVerified(currentState, error) { + currentState.whenVerifiedDeferred.reject(error); + delete currentState.whenVerifiedDeferred; + }, + + /** + * Does the actual fetch of an oauth token for getOAuthToken() + * using the account session token. + * + * It's split out into a separate method so that we can easily + * stash in-flight calls in a cache. + * + * @param {String} scopeString + * @param {Number} ttl + * @returns {Promise} + * @private + */ + async _doTokenFetchWithSessionToken(sessionToken, scopeString, ttl) { + const result = await this.fxAccountsClient.accessTokenWithSessionToken( + sessionToken, + FX_OAUTH_CLIENT_ID, + scopeString, + ttl + ); + return result.access_token; + }, + + getOAuthToken(options = {}) { + log.debug("getOAuthToken enter"); + let scope = options.scope; + if (typeof scope === "string") { + scope = [scope]; + } + + if (!scope || !scope.length) { + return Promise.reject( + this._error( + ERROR_INVALID_PARAMETER, + "Missing or invalid 'scope' option" + ) + ); + } + + return this.withSessionToken(async (sessionToken, currentState) => { + // Early exit for a cached token. + let cached = currentState.getCachedToken(scope); + if (cached) { + log.debug("getOAuthToken returning a cached token"); + return cached.token; + } + + // Build the string we use in our "inflight" map and that we send to the + // server. Because it's used as a key in the map we sort the scopes. + let scopeString = scope.sort().join(" "); + + // We keep a map of in-flight requests to avoid multiple promise-based + // consumers concurrently requesting the same token. + let maybeInFlight = currentState.inFlightTokenRequests.get(scopeString); + if (maybeInFlight) { + log.debug("getOAuthToken has an in-flight request for this scope"); + return maybeInFlight; + } + + // We need to start a new fetch and stick the promise in our in-flight map + // and remove it when it resolves. + let promise = this._doTokenFetchWithSessionToken( + sessionToken, + scopeString, + options.ttl + ) + .then(token => { + // As a sanity check, ensure something else hasn't raced getting a token + // of the same scope. If something has we just make noise rather than + // taking any concrete action because it should never actually happen. + if (currentState.getCachedToken(scope)) { + log.error(`detected a race for oauth token with scope ${scope}`); + } + // If we got one, cache it. + if (token) { + let entry = { token }; + currentState.setCachedToken(scope, entry); + } + return token; + }) + .finally(() => { + // Remove ourself from the in-flight map. There's no need to check the + // result of .delete() to handle a signout race, because setCachedToken + // above will fail in that case and cause the entire call to fail. + currentState.inFlightTokenRequests.delete(scopeString); + }); + + currentState.inFlightTokenRequests.set(scopeString, promise); + return promise; + }); + }, + + /** + * Remove an OAuth token from the token cache + * and makes a network request to FxA server to destroy the token. + * + * @param options + * { + * token: (string) A previously fetched token. + * } + * @return Promise. This function will always resolve, even if + * an unknown token is passed. + */ + removeCachedOAuthToken(options) { + if (!options.token || typeof options.token !== "string") { + throw this._error( + ERROR_INVALID_PARAMETER, + "Missing or invalid 'token' option" + ); + } + return this.withCurrentAccountState(currentState => { + let existing = currentState.removeCachedToken(options.token); + if (existing) { + // background destroy. + this.destroyOAuthToken(existing).catch(err => { + log.warn("FxA failed to revoke a cached token", err); + }); + } + }); + }, + + /** Sets the user to be verified in the account state, + * This prevents any polling for the user's verification state from the FxA server + **/ + setUserVerified() { + return this.withCurrentAccountState(async currentState => { + const userData = await currentState.getUserAccountData(); + if (!userData.verified) { + await currentState.updateAccountData({ verified: true }); + } + }); + }, + + async _getVerifiedAccountOrReject() { + let data = await this.currentAccountState.getUserAccountData(); + if (!data) { + // No signed-in user + throw this._error(ERROR_NO_ACCOUNT); + } + if (!this.isUserEmailVerified(data)) { + // Signed-in user has not verified email + throw this._error(ERROR_UNVERIFIED_ACCOUNT); + } + return data; + }, + + // _handle* methods used by push, used when the account/device status is + // changed on a different device. + async _handleAccountDestroyed(uid) { + let state = this.currentAccountState; + const accountData = await state.getUserAccountData(); + const localUid = accountData ? accountData.uid : null; + if (!localUid) { + log.info( + `Account destroyed push notification received, but we're already logged-out` + ); + return null; + } + if (uid == localUid) { + const data = JSON.stringify({ isLocalDevice: true }); + await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data); + return this.signOut(true); + } + log.info( + `The destroyed account uid doesn't match with the local uid. ` + + `Local: ${localUid}, account uid destroyed: ${uid}` + ); + return null; + }, + + async _handleDeviceDisconnection(deviceId) { + let state = this.currentAccountState; + const accountData = await state.getUserAccountData(); + if (!accountData || !accountData.device) { + // Nothing we can do here. + return; + } + const localDeviceId = accountData.device.id; + const isLocalDevice = deviceId == localDeviceId; + if (isLocalDevice) { + this.signOut(true); + } + const data = JSON.stringify({ isLocalDevice }); + await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data); + }, + + _setLastUserPref(newEmail) { + Services.prefs.setStringPref( + PREF_LAST_FXA_USER, + CryptoUtils.sha256Base64(newEmail) + ); + }, + + async _handleEmailUpdated(newEmail) { + this._setLastUserPref(newEmail); + await this.currentAccountState.updateUserAccountData({ email: newEmail }); + }, + + /* + * Coerce an error into one of the general error cases: + * NETWORK_ERROR + * AUTH_ERROR + * UNKNOWN_ERROR + * + * These errors will pass through: + * INVALID_PARAMETER + * NO_ACCOUNT + * UNVERIFIED_ACCOUNT + */ + _errorToErrorClass(aError) { + if (aError.errno) { + let error = SERVER_ERRNO_TO_ERROR[aError.errno]; + return this._error( + ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN, + aError + ); + } else if ( + aError.message && + (aError.message === "INVALID_PARAMETER" || + aError.message === "NO_ACCOUNT" || + aError.message === "UNVERIFIED_ACCOUNT" || + aError.message === "AUTH_ERROR") + ) { + return aError; + } + return this._error(ERROR_UNKNOWN, aError); + }, + + _error(aError, aDetails) { + log.error("FxA rejecting with error ${aError}, details: ${aDetails}", { + aError, + aDetails, + }); + let reason = new Error(aError); + if (aDetails) { + reason.details = aDetails; + } + return reason; + }, + + // Attempt to update the auth server with whatever device details are stored + // in the account data. Returns a promise that always resolves, never rejects. + // If the promise resolves to a value, that value is the device id. + updateDeviceRegistration() { + return this.device.updateDeviceRegistration(); + }, + + /** + * Delete all the persisted credentials we store for FxA. After calling + * this, the user will be forced to re-authenticate to continue. + * + * @return Promise resolves when the user data has been persisted + */ + dropCredentials(state) { + // Delete all fields except those required for the user to + // reauthenticate. + let updateData = {}; + let clearField = field => { + if (!FXA_PWDMGR_REAUTH_ALLOWLIST.has(field)) { + updateData[field] = null; + } + }; + FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField); + FXA_PWDMGR_SECURE_FIELDS.forEach(clearField); + + return state.updateUserAccountData(updateData); + }, + + async checkAccountStatus(state) { + log.info("checking account status..."); + let data = await state.getUserAccountData(["uid", "sessionToken"]); + if (!data) { + log.info("account status: no user"); + return false; + } + // If we have a session token, then check if that remains valid - if this + // works we know the account must also be OK. + if (data.sessionToken) { + if (await this.fxAccountsClient.sessionStatus(data.sessionToken)) { + log.info("account status: ok"); + return true; + } + } + let exists = await this.fxAccountsClient.accountStatus(data.uid); + if (!exists) { + // Delete all local account data. Since the account no longer + // exists, we can skip the remote calls. + log.info("account status: deleted"); + await this._handleAccountDestroyed(data.uid); + } else { + // Note that we may already have been in a "needs reauth" state (ie, if + // this function was called when we already had no session token), but + // that's OK - re-notifying etc should cause no harm. + log.info("account status: needs reauthentication"); + await this.dropCredentials(this.currentAccountState); + // Notify the account state has changed so the UI updates. + await this.notifyObservers(ON_ACCOUNT_STATE_CHANGE_NOTIFICATION); + } + return false; + }, + + async _handleTokenError(err) { + if (!err || err.code != 401 || err.errno != ERRNO_INVALID_AUTH_TOKEN) { + throw err; + } + log.warn("handling invalid token error", err); + // Note that we don't use `withCurrentAccountState` here as that will cause + // an error to be thrown if we sign out due to the account not existing. + let state = this.currentAccountState; + let ok = await this.checkAccountStatus(state); + if (ok) { + log.warn("invalid token error, but account state appears ok?"); + } + // always re-throw the error. + throw err; + }, +}; + +let fxAccountsSingleton = null; + +export function getFxAccountsSingleton() { + if (fxAccountsSingleton) { + return fxAccountsSingleton; + } + + fxAccountsSingleton = new FxAccounts(); + + // XXX Bug 947061 - We need a strategy for resuming email verification after + // browser restart + fxAccountsSingleton._internal.loadAndPoll(); + + return fxAccountsSingleton; +} + +// `AccountState` is exported for tests. diff --git a/services/fxaccounts/FxAccountsClient.sys.mjs b/services/fxaccounts/FxAccountsClient.sys.mjs new file mode 100644 index 0000000000..9dc80ff419 --- /dev/null +++ b/services/fxaccounts/FxAccountsClient.sys.mjs @@ -0,0 +1,839 @@ +/* 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 { CommonUtils } from "resource://services-common/utils.sys.mjs"; + +import { HawkClient } from "resource://services-common/hawkclient.sys.mjs"; +import { deriveHawkCredentials } from "resource://services-common/hawkrequest.sys.mjs"; +import { CryptoUtils } from "resource://services-crypto/utils.sys.mjs"; + +import { + ERRNO_ACCOUNT_DOES_NOT_EXIST, + ERRNO_INCORRECT_EMAIL_CASE, + ERRNO_INCORRECT_PASSWORD, + ERRNO_INVALID_AUTH_NONCE, + ERRNO_INVALID_AUTH_TIMESTAMP, + ERRNO_INVALID_AUTH_TOKEN, + log, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +import { Credentials } from "resource://gre/modules/Credentials.sys.mjs"; + +const HOST_PREF = "identity.fxaccounts.auth.uri"; + +const SIGNIN = "/account/login"; +const SIGNUP = "/account/create"; +// Devices older than this many days will not appear in the devices list +const DEVICES_FILTER_DAYS = 21; + +export var FxAccountsClient = function ( + host = Services.prefs.getStringPref(HOST_PREF) +) { + this.host = host; + + // The FxA auth server expects requests to certain endpoints to be authorized + // using Hawk. + this.hawk = new HawkClient(host); + this.hawk.observerPrefix = "FxA:hawk"; + + // Manage server backoff state. C.f. + // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol + this.backoffError = null; +}; + +FxAccountsClient.prototype = { + /** + * Return client clock offset, in milliseconds, as determined by hawk client. + * Provided because callers should not have to know about hawk + * implementation. + * + * The offset is the number of milliseconds that must be added to the client + * clock to make it equal to the server clock. For example, if the client is + * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. + */ + get localtimeOffsetMsec() { + return this.hawk.localtimeOffsetMsec; + }, + + /* + * Return current time in milliseconds + * + * Not used by this module, but made available to the FxAccounts.jsm + * that uses this client. + */ + now() { + return this.hawk.now(); + }, + + /** + * Common code from signIn and signUp. + * + * @param path + * Request URL path. Can be /account/create or /account/login + * @param email + * The email address for the account (utf8) + * @param password + * The user's password + * @param [getKeys=false] + * If set to true the keyFetchToken will be retrieved + * @param [retryOK=true] + * If capitalization of the email is wrong and retryOK is set to true, + * we will retry with the suggested capitalization from the server + * @return Promise + * Returns a promise that resolves to an object: + * { + * authAt: authentication time for the session (seconds since epoch) + * email: the primary email for this account + * keyFetchToken: a key fetch token (hex) + * sessionToken: a session token (hex) + * uid: the user's unique ID (hex) + * unwrapBKey: used to unwrap kB, derived locally from the + * password (not revealed to the FxA server) + * verified (optional): flag indicating verification status of the + * email + * } + */ + _createSession(path, email, password, getKeys = false, retryOK = true) { + return Credentials.setup(email, password).then(creds => { + let data = { + authPW: CommonUtils.bytesAsHex(creds.authPW), + email, + }; + let keys = getKeys ? "?keys=true" : ""; + + return this._request(path + keys, "POST", null, data).then( + // Include the canonical capitalization of the email in the response so + // the caller can set its signed-in user state accordingly. + result => { + result.email = data.email; + result.unwrapBKey = CommonUtils.bytesAsHex(creds.unwrapBKey); + + return result; + }, + error => { + log.debug("Session creation failed", error); + // If the user entered an email with different capitalization from + // what's stored in the database (e.g., Greta.Garbo@gmail.COM as + // opposed to greta.garbo@gmail.com), the server will respond with a + // errno 120 (code 400) and the expected capitalization of the email. + // We retry with this email exactly once. If successful, we use the + // server's version of the email as the signed-in-user's email. This + // is necessary because the email also serves as salt; so we must be + // in agreement with the server on capitalization. + // + // API reference: + // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md + if (ERRNO_INCORRECT_EMAIL_CASE === error.errno && retryOK) { + if (!error.email) { + log.error("Server returned errno 120 but did not provide email"); + throw error; + } + return this._createSession( + path, + error.email, + password, + getKeys, + false + ); + } + throw error; + } + ); + }); + }, + + /** + * Create a new Firefox Account and authenticate + * + * @param email + * The email address for the account (utf8) + * @param password + * The user's password + * @param [getKeys=false] + * If set to true the keyFetchToken will be retrieved + * @return Promise + * Returns a promise that resolves to an object: + * { + * uid: the user's unique ID (hex) + * sessionToken: a session token (hex) + * keyFetchToken: a key fetch token (hex), + * unwrapBKey: used to unwrap kB, derived locally from the + * password (not revealed to the FxA server) + * } + */ + signUp(email, password, getKeys = false) { + return this._createSession( + SIGNUP, + email, + password, + getKeys, + false /* no retry */ + ); + }, + + /** + * Authenticate and create a new session with the Firefox Account API server + * + * @param email + * The email address for the account (utf8) + * @param password + * The user's password + * @param [getKeys=false] + * If set to true the keyFetchToken will be retrieved + * @return Promise + * Returns a promise that resolves to an object: + * { + * authAt: authentication time for the session (seconds since epoch) + * email: the primary email for this account + * keyFetchToken: a key fetch token (hex) + * sessionToken: a session token (hex) + * uid: the user's unique ID (hex) + * unwrapBKey: used to unwrap kB, derived locally from the + * password (not revealed to the FxA server) + * verified: flag indicating verification status of the email + * } + */ + signIn: function signIn(email, password, getKeys = false) { + return this._createSession( + SIGNIN, + email, + password, + getKeys, + true /* retry */ + ); + }, + + /** + * Check the status of a session given a session token + * + * @param sessionTokenHex + * The session token encoded in hex + * @return Promise + * Resolves with a boolean indicating if the session is still valid + */ + async sessionStatus(sessionTokenHex) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + return this._request("/session/status", "GET", credentials).then( + () => Promise.resolve(true), + error => { + if (isInvalidTokenError(error)) { + return Promise.resolve(false); + } + throw error; + } + ); + }, + + /** + * List all the clients connected to the authenticated user's account, + * including devices, OAuth clients, and web sessions. + * + * @param sessionTokenHex + * The session token encoded in hex + * @return Promise + */ + async attachedClients(sessionTokenHex) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + return this._requestWithHeaders( + "/account/attached_clients", + "GET", + credentials + ); + }, + + /** + * Retrieves an OAuth authorization code. + * + * @param String sessionTokenHex + * The session token encoded in hex + * @param {Object} options + * @param options.client_id + * @param options.state + * @param options.scope + * @param options.access_type + * @param options.code_challenge_method + * @param options.code_challenge + * @param [options.keys_jwe] + * @returns {Promise} Object containing `code` and `state`. + */ + async oauthAuthorize(sessionTokenHex, options) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + const body = { + client_id: options.client_id, + response_type: "code", + state: options.state, + scope: options.scope, + access_type: options.access_type, + code_challenge: options.code_challenge, + code_challenge_method: options.code_challenge_method, + }; + if (options.keys_jwe) { + body.keys_jwe = options.keys_jwe; + } + return this._request("/oauth/authorization", "POST", credentials, body); + }, + /** + * Exchanges an OAuth authorization code with a refresh token, access tokens and an optional JWE representing scoped keys + * Takes in the sessionToken to tie the device record associated with the session, with the device record associated with the refreshToken + * + * @param string sessionTokenHex: The session token encoded in hex + * @param String code: OAuth authorization code + * @param String verifier: OAuth PKCE verifier + * @param String clientId: OAuth client ID + * + * @returns { Object } object containing `refresh_token`, `access_token` and `keys_jwe` + **/ + async oauthToken(sessionTokenHex, code, verifier, clientId) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + const body = { + grant_type: "authorization_code", + code, + client_id: clientId, + code_verifier: verifier, + }; + return this._request("/oauth/token", "POST", credentials, body); + }, + /** + * Destroy an OAuth access token or refresh token. + * + * @param String clientId + * @param String token The token to be revoked. + */ + async oauthDestroy(clientId, token) { + const body = { + client_id: clientId, + token, + }; + return this._request("/oauth/destroy", "POST", null, body); + }, + + /** + * Query for the information required to derive + * scoped encryption keys requested by the specified OAuth client. + * + * @param sessionTokenHex + * The session token encoded in hex + * @param clientId + * @param scope + * Space separated list of scopes + * @return Promise + */ + async getScopedKeyData(sessionTokenHex, clientId, scope) { + if (!clientId) { + throw new Error("Missing 'clientId' parameter"); + } + if (!scope) { + throw new Error("Missing 'scope' parameter"); + } + const params = { + client_id: clientId, + scope, + }; + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + return this._request( + "/account/scoped-key-data", + "POST", + credentials, + params + ); + }, + + /** + * Destroy the current session with the Firefox Account API server and its + * associated device. + * + * @param sessionTokenHex + * The session token encoded in hex + * @return Promise + */ + async signOut(sessionTokenHex, options = {}) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + let path = "/session/destroy"; + if (options.service) { + path += "?service=" + encodeURIComponent(options.service); + } + return this._request(path, "POST", credentials); + }, + + /** + * Check the verification status of the user's FxA email address + * + * @param sessionTokenHex + * The current session token encoded in hex + * @return Promise + */ + async recoveryEmailStatus(sessionTokenHex, options = {}) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + let path = "/recovery_email/status"; + if (options.reason) { + path += "?reason=" + encodeURIComponent(options.reason); + } + + return this._request(path, "GET", credentials); + }, + + /** + * Resend the verification email for the user + * + * @param sessionTokenHex + * The current token encoded in hex + * @return Promise + */ + async resendVerificationEmail(sessionTokenHex) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + return this._request("/recovery_email/resend_code", "POST", credentials); + }, + + /** + * Retrieve encryption keys + * + * @param keyFetchTokenHex + * A one-time use key fetch token encoded in hex + * @return Promise + * Returns a promise that resolves to an object: + * { + * kA: an encryption key for recevorable data (bytes) + * wrapKB: an encryption key that requires knowledge of the + * user's password (bytes) + * } + */ + async accountKeys(keyFetchTokenHex) { + let creds = await deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken"); + let keyRequestKey = creds.extra.slice(0, 32); + let morecreds = await CryptoUtils.hkdfLegacy( + keyRequestKey, + undefined, + Credentials.keyWord("account/keys"), + 3 * 32 + ); + let respHMACKey = morecreds.slice(0, 32); + let respXORKey = morecreds.slice(32, 96); + + const resp = await this._request("/account/keys", "GET", creds); + if (!resp.bundle) { + throw new Error("failed to retrieve keys"); + } + + let bundle = CommonUtils.hexToBytes(resp.bundle); + let mac = bundle.slice(-32); + let key = CommonUtils.byteStringToArrayBuffer(respHMACKey); + // CryptoUtils.hmac takes ArrayBuffers as inputs for the key and data and + // returns an ArrayBuffer. + let bundleMAC = await CryptoUtils.hmac( + "SHA-256", + key, + CommonUtils.byteStringToArrayBuffer(bundle.slice(0, -32)) + ); + if (mac !== CommonUtils.arrayBufferToByteString(bundleMAC)) { + throw new Error("error unbundling encryption keys"); + } + + let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64)); + + return { + kA: keyAWrapB.slice(0, 32), + wrapKB: keyAWrapB.slice(32), + }; + }, + + /** + * Obtain an OAuth access token by authenticating using a session token. + * + * @param {String} sessionTokenHex + * The session token encoded in hex + * @param {String} clientId + * @param {String} scope + * List of space-separated scopes. + * @param {Number} ttl + * Token time to live. + * @return {Promise} Object containing an `access_token`. + */ + async accessTokenWithSessionToken(sessionTokenHex, clientId, scope, ttl) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + const body = { + client_id: clientId, + grant_type: "fxa-credentials", + scope, + ttl, + }; + return this._request("/oauth/token", "POST", credentials, body); + }, + + /** + * Determine if an account exists + * + * @param email + * The email address to check + * @return Promise + * The promise resolves to true if the account exists, or false + * if it doesn't. The promise is rejected on other errors. + */ + accountExists(email) { + return this.signIn(email, "").then( + cantHappen => { + throw new Error("How did I sign in with an empty password?"); + }, + expectedError => { + switch (expectedError.errno) { + case ERRNO_ACCOUNT_DOES_NOT_EXIST: + return false; + case ERRNO_INCORRECT_PASSWORD: + return true; + default: + // not so expected, any more ... + throw expectedError; + } + } + ); + }, + + /** + * Given the uid of an existing account (not an arbitrary email), ask + * the server if it still exists via /account/status. + * + * Used for differentiating between password change and account deletion. + */ + accountStatus(uid) { + return this._request("/account/status?uid=" + uid, "GET").then( + result => { + return result.exists; + }, + error => { + log.error("accountStatus failed", error); + return Promise.reject(error); + } + ); + }, + + /** + * Register a new device + * + * @method registerDevice + * @param sessionTokenHex + * Session token obtained from signIn + * @param name + * Device name + * @param type + * Device type (mobile|desktop) + * @param [options] + * Extra device options + * @param [options.availableCommands] + * Available commands for this device + * @param [options.pushCallback] + * `pushCallback` push endpoint callback + * @param [options.pushPublicKey] + * `pushPublicKey` push public key (URLSafe Base64 string) + * @param [options.pushAuthKey] + * `pushAuthKey` push auth secret (URLSafe Base64 string) + * @return Promise + * Resolves to an object: + * { + * id: Device identifier + * createdAt: Creation time (milliseconds since epoch) + * name: Name of device + * type: Type of device (mobile|desktop) + * } + */ + async registerDevice(sessionTokenHex, name, type, options = {}) { + let path = "/account/device"; + + let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken"); + let body = { name, type }; + + if (options.pushCallback) { + body.pushCallback = options.pushCallback; + } + if (options.pushPublicKey && options.pushAuthKey) { + body.pushPublicKey = options.pushPublicKey; + body.pushAuthKey = options.pushAuthKey; + } + body.availableCommands = options.availableCommands; + + return this._request(path, "POST", creds, body); + }, + + /** + * Sends a message to other devices. Must conform with the push payload schema: + * https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json + * + * @method notifyDevice + * @param sessionTokenHex + * Session token obtained from signIn + * @param deviceIds + * Devices to send the message to. If null, will be sent to all devices. + * @param excludedIds + * Devices to exclude when sending to all devices (deviceIds must be null). + * @param payload + * Data to send with the message + * @return Promise + * Resolves to an empty object: + * {} + */ + async notifyDevices( + sessionTokenHex, + deviceIds, + excludedIds, + payload, + TTL = 0 + ) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + if (deviceIds && excludedIds) { + throw new Error( + "You cannot specify excluded devices if deviceIds is set." + ); + } + const body = { + to: deviceIds || "all", + payload, + TTL, + }; + if (excludedIds) { + body.excluded = excludedIds; + } + return this._request("/account/devices/notify", "POST", credentials, body); + }, + + /** + * Retrieves pending commands for our device. + * + * @method getCommands + * @param sessionTokenHex - Session token obtained from signIn + * @param [index] - If specified, only messages received after the one who + * had that index will be retrieved. + * @param [limit] - Maximum number of messages to retrieve. + */ + async getCommands(sessionTokenHex, { index, limit }) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + const params = new URLSearchParams(); + if (index != undefined) { + params.set("index", index); + } + if (limit != undefined) { + params.set("limit", limit); + } + const path = `/account/device/commands?${params.toString()}`; + return this._request(path, "GET", credentials); + }, + + /** + * Invokes a command on another device. + * + * @method invokeCommand + * @param sessionTokenHex - Session token obtained from signIn + * @param command - Name of the command to invoke + * @param target - Recipient device ID. + * @param payload + * @return Promise + * Resolves to the request's response, (which should be an empty object) + */ + async invokeCommand(sessionTokenHex, command, target, payload) { + const credentials = await deriveHawkCredentials( + sessionTokenHex, + "sessionToken" + ); + const body = { + command, + target, + payload, + }; + return this._request( + "/account/devices/invoke_command", + "POST", + credentials, + body + ); + }, + + /** + * Update the session or name for an existing device + * + * @method updateDevice + * @param sessionTokenHex + * Session token obtained from signIn + * @param id + * Device identifier + * @param name + * Device name + * @param [options] + * Extra device options + * @param [options.availableCommands] + * Available commands for this device + * @param [options.pushCallback] + * `pushCallback` push endpoint callback + * @param [options.pushPublicKey] + * `pushPublicKey` push public key (URLSafe Base64 string) + * @param [options.pushAuthKey] + * `pushAuthKey` push auth secret (URLSafe Base64 string) + * @return Promise + * Resolves to an object: + * { + * id: Device identifier + * name: Device name + * } + */ + async updateDevice(sessionTokenHex, id, name, options = {}) { + let path = "/account/device"; + + let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken"); + let body = { id, name }; + if (options.pushCallback) { + body.pushCallback = options.pushCallback; + } + if (options.pushPublicKey && options.pushAuthKey) { + body.pushPublicKey = options.pushPublicKey; + body.pushAuthKey = options.pushAuthKey; + } + body.availableCommands = options.availableCommands; + + return this._request(path, "POST", creds, body); + }, + + /** + * Get a list of currently registered devices that have been accessed + * in the last `DEVICES_FILTER_DAYS` days + * + * @method getDeviceList + * @param sessionTokenHex + * Session token obtained from signIn + * @return Promise + * Resolves to an array of objects: + * [ + * { + * id: Device id + * isCurrentDevice: Boolean indicating whether the item + * represents the current device + * name: Device name + * type: Device type (mobile|desktop) + * }, + * ... + * ] + */ + async getDeviceList(sessionTokenHex) { + let timestamp = Date.now() - 1000 * 60 * 60 * 24 * DEVICES_FILTER_DAYS; + let path = `/account/devices?filterIdleDevicesTimestamp=${timestamp}`; + let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken"); + return this._request(path, "GET", creds, {}); + }, + + _clearBackoff() { + this.backoffError = null; + }, + + /** + * A general method for sending raw API calls to the FxA auth server. + * All request bodies and responses are JSON. + * + * @param path + * API endpoint path + * @param method + * The HTTP request method + * @param credentials + * Hawk credentials + * @param jsonPayload + * A JSON payload + * @return Promise + * Returns a promise that resolves to the JSON response of the API call, + * or is rejected with an error. Error responses have the following properties: + * { + * "code": 400, // matches the HTTP status code + * "errno": 107, // stable application-level error number + * "error": "Bad Request", // string description of the error type + * "message": "the value of salt is not allowed to be undefined", + * "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error + * } + */ + async _requestWithHeaders(path, method, credentials, jsonPayload) { + // We were asked to back off. + if (this.backoffError) { + log.debug("Received new request during backoff, re-rejecting."); + throw this.backoffError; + } + let response; + try { + response = await this.hawk.request( + path, + method, + credentials, + jsonPayload + ); + } catch (error) { + log.error(`error ${method}ing ${path}`, error); + if (error.retryAfter) { + log.debug("Received backoff response; caching error as flag."); + this.backoffError = error; + // Schedule clearing of cached-error-as-flag. + CommonUtils.namedTimer( + this._clearBackoff, + error.retryAfter * 1000, + this, + "fxaBackoffTimer" + ); + } + throw error; + } + try { + return { body: JSON.parse(response.body), headers: response.headers }; + } catch (error) { + log.error("json parse error on response: " + response.body); + // eslint-disable-next-line no-throw-literal + throw { error }; + } + }, + + async _request(path, method, credentials, jsonPayload) { + const response = await this._requestWithHeaders( + path, + method, + credentials, + jsonPayload + ); + return response.body; + }, +}; + +function isInvalidTokenError(error) { + if (error.code != 401) { + return false; + } + switch (error.errno) { + case ERRNO_INVALID_AUTH_TOKEN: + case ERRNO_INVALID_AUTH_TIMESTAMP: + case ERRNO_INVALID_AUTH_NONCE: + return true; + } + return false; +} diff --git a/services/fxaccounts/FxAccountsCommands.sys.mjs b/services/fxaccounts/FxAccountsCommands.sys.mjs new file mode 100644 index 0000000000..40fcc7f925 --- /dev/null +++ b/services/fxaccounts/FxAccountsCommands.sys.mjs @@ -0,0 +1,467 @@ +/* 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 { + COMMAND_SENDTAB, + COMMAND_SENDTAB_TAIL, + SCOPE_OLD_SYNC, + log, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { Observers } from "resource://services-common/observers.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BulkKeyBundle: "resource://services-sync/keys.sys.mjs", + CryptoWrapper: "resource://services-sync/record.sys.mjs", + PushCrypto: "resource://gre/modules/PushCrypto.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "INVALID_SHAREABLE_SCHEMES", + "services.sync.engine.tabs.filteredSchemes", + "", + null, + val => { + return new Set(val.split("|")); + } +); + +export class FxAccountsCommands { + constructor(fxAccountsInternal) { + this._fxai = fxAccountsInternal; + this.sendTab = new SendTab(this, fxAccountsInternal); + this._invokeRateLimitExpiry = 0; + } + + async availableCommands() { + const encryptedSendTabKeys = await this.sendTab.getEncryptedSendTabKeys(); + if (!encryptedSendTabKeys) { + // This will happen if the account is not verified yet. + return {}; + } + return { + [COMMAND_SENDTAB]: encryptedSendTabKeys, + }; + } + + async invoke(command, device, payload) { + const { sessionToken } = await this._fxai.getUserAccountData([ + "sessionToken", + ]); + const client = this._fxai.fxAccountsClient; + const now = Date.now(); + if (now < this._invokeRateLimitExpiry) { + const remaining = (this._invokeRateLimitExpiry - now) / 1000; + throw new Error( + `Invoke for ${command} is rate-limited for ${remaining} seconds.` + ); + } + try { + let info = await client.invokeCommand( + sessionToken, + command, + device.id, + payload + ); + if (!info.enqueued || !info.notified) { + // We want an error log here to help diagnose users who report failure. + log.error("Sending was only partially successful", info); + } else { + log.info("Successfully sent", info); + } + } catch (err) { + if (err.code && err.code === 429 && err.retryAfter) { + this._invokeRateLimitExpiry = Date.now() + err.retryAfter * 1000; + } + throw err; + } + log.info(`Payload sent to device ${device.id}.`); + } + + /** + * Poll and handle device commands for the current device. + * This method can be called either in response to a Push message, + * or by itself as a "commands recovery" mechanism. + * + * @param {Number} notifiedIndex "Command received" push messages include + * the index of the command that triggered the message. We use it as a + * hint when we have no "last command index" stored. + */ + async pollDeviceCommands(notifiedIndex = 0) { + // Whether the call to `pollDeviceCommands` was initiated by a Push message from the FxA + // servers in response to a message being received or simply scheduled in order + // to fetch missed messages. + log.info(`Polling device commands.`); + await this._fxai.withCurrentAccountState(async state => { + const { device } = await state.getUserAccountData(["device"]); + if (!device) { + throw new Error("No device registration."); + } + // We increment lastCommandIndex by 1 because the server response includes the current index. + // If we don't have a `lastCommandIndex` stored, we fall back on the index from the push message we just got. + const lastCommandIndex = device.lastCommandIndex + 1 || notifiedIndex; + // We have already received this message before. + if (notifiedIndex > 0 && notifiedIndex < lastCommandIndex) { + return; + } + const { index, messages } = await this._fetchDeviceCommands( + lastCommandIndex + ); + if (messages.length) { + await state.updateUserAccountData({ + device: { ...device, lastCommandIndex: index }, + }); + log.info(`Handling ${messages.length} messages`); + await this._handleCommands(messages, notifiedIndex); + } + }); + return true; + } + + async _fetchDeviceCommands(index, limit = null) { + const userData = await this._fxai.getUserAccountData(); + if (!userData) { + throw new Error("No user."); + } + const { sessionToken } = userData; + if (!sessionToken) { + throw new Error("No session token."); + } + const client = this._fxai.fxAccountsClient; + const opts = { index }; + if (limit != null) { + opts.limit = limit; + } + return client.getCommands(sessionToken, opts); + } + + _getReason(notifiedIndex, messageIndex) { + // The returned reason value represents an explanation for why the command associated with the + // message of the given `messageIndex` is being handled. If `notifiedIndex` is zero the command + // is a part of a fallback polling process initiated by "Sync Now" ["poll"]. If `notifiedIndex` is + // greater than `messageIndex` this is a push command that was previously missed ["push-missed"], + // otherwise we assume this is a push command with no missed messages ["push"]. + if (notifiedIndex == 0) { + return "poll"; + } else if (notifiedIndex > messageIndex) { + return "push-missed"; + } + // Note: The returned reason might be "push" in the case where a user sends multiple tabs + // in quick succession. We are not attempting to distinguish this from other push cases at + // present. + return "push"; + } + + async _handleCommands(messages, notifiedIndex) { + try { + await this._fxai.device.refreshDeviceList(); + } catch (e) { + log.warn("Error refreshing device list", e); + } + // We debounce multiple incoming tabs so we show a single notification. + const tabsReceived = []; + for (const { index, data } of messages) { + const { command, payload, sender: senderId } = data; + const reason = this._getReason(notifiedIndex, index); + const sender = + senderId && this._fxai.device.recentDeviceList + ? this._fxai.device.recentDeviceList.find(d => d.id == senderId) + : null; + if (!sender) { + log.warn( + "Incoming command is from an unknown device (maybe disconnected?)" + ); + } + switch (command) { + case COMMAND_SENDTAB: + try { + const { title, uri } = await this.sendTab.handle( + senderId, + payload, + reason + ); + log.info( + `Tab received with FxA commands: "${ + title || "" + }" from ${sender ? sender.name : "Unknown device"}.` + ); + // URLs are PII, so only logged at trace. + log.trace(`Tab received URL: ${uri}`); + // This should eventually be rare to hit as all platforms will be using the same + // scheme filter list, but we have this here in the case other platforms + // haven't caught up and/or trying to send invalid uris using older versions + const scheme = Services.io.newURI(uri).scheme; + if (lazy.INVALID_SHAREABLE_SCHEMES.has(scheme)) { + throw new Error("Invalid scheme found for received URI."); + } + tabsReceived.push({ title, uri, sender }); + } catch (e) { + log.error(`Error while handling incoming Send Tab payload.`, e); + } + break; + default: + log.info(`Unknown command: ${command}.`); + } + } + if (tabsReceived.length) { + this._notifyFxATabsReceived(tabsReceived); + } + } + + _notifyFxATabsReceived(tabsReceived) { + Observers.notify("fxaccounts:commands:open-uri", tabsReceived); + } +} + +/** + * Send Tab is built on top of FxA commands. + * + * Devices exchange keys wrapped in the oldsync key between themselves (getEncryptedSendTabKeys) + * during the device registration flow. The FxA server can theoretically never + * retrieve the send tab keys since it doesn't know the oldsync key. + * + * Note about the keys: + * The server has the `pushPublicKey`. The FxA server encrypt the send-tab payload again using the + * push keys - after the client has encrypted the payload using the send-tab keys. + * The push keys are different from the send-tab keys. The FxA server uses + * the push keys to deliver the tabs using same mechanism we use for web-push. + * However, clients use the send-tab keys for end-to-end encryption. + */ +export class SendTab { + constructor(commands, fxAccountsInternal) { + this._commands = commands; + this._fxai = fxAccountsInternal; + } + /** + * @param {Device[]} to - Device objects (typically returned by fxAccounts.getDevicesList()). + * @param {Object} tab + * @param {string} tab.url + * @param {string} tab.title + * @returns A report object, in the shape of + * {succeded: [Device], error: [{device: Device, error: Exception}]} + */ + async send(to, tab) { + log.info(`Sending a tab to ${to.length} devices.`); + const flowID = this._fxai.telemetry.generateFlowID(); + const encoder = new TextEncoder(); + const data = { entries: [{ title: tab.title, url: tab.url }] }; + const report = { + succeeded: [], + failed: [], + }; + for (let device of to) { + try { + const streamID = this._fxai.telemetry.generateFlowID(); + const targetData = Object.assign({ flowID, streamID }, data); + const bytes = encoder.encode(JSON.stringify(targetData)); + const encrypted = await this._encrypt(bytes, device); + // FxA expects an object as the payload, but we only have a single encrypted string; wrap it. + // If you add any plaintext items to this payload, please carefully consider the privacy implications + // of revealing that data to the FxA server. + const payload = { encrypted }; + await this._commands.invoke(COMMAND_SENDTAB, device, payload); + this._fxai.telemetry.recordEvent( + "command-sent", + COMMAND_SENDTAB_TAIL, + this._fxai.telemetry.sanitizeDeviceId(device.id), + { flowID, streamID } + ); + report.succeeded.push(device); + } catch (error) { + log.error("Error while invoking a send tab command.", error); + report.failed.push({ device, error }); + } + } + return report; + } + + // Returns true if the target device is compatible with FxA Commands Send tab. + isDeviceCompatible(device) { + return ( + device.availableCommands && device.availableCommands[COMMAND_SENDTAB] + ); + } + + // Handle incoming send tab payload, called by FxAccountsCommands. + async handle(senderID, { encrypted }, reason) { + const bytes = await this._decrypt(encrypted); + const decoder = new TextDecoder("utf8"); + const data = JSON.parse(decoder.decode(bytes)); + const { flowID, streamID, entries } = data; + const current = data.hasOwnProperty("current") + ? data.current + : entries.length - 1; + const { title, url: uri } = entries[current]; + // `flowID` and `streamID` are in the top-level of the JSON, `entries` is + // an array of "tabs" with `current` being what index is the one we care + // about, or the last one if not specified. + this._fxai.telemetry.recordEvent( + "command-received", + COMMAND_SENDTAB_TAIL, + this._fxai.telemetry.sanitizeDeviceId(senderID), + { flowID, streamID, reason } + ); + + return { + title, + uri, + }; + } + + async _encrypt(bytes, device) { + let bundle = device.availableCommands[COMMAND_SENDTAB]; + if (!bundle) { + throw new Error(`Device ${device.id} does not have send tab keys.`); + } + const oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC); + // Older clients expect this to be hex, due to pre-JWK sync key ids :-( + const ourKid = this._fxai.keys.kidAsHex(oldsyncKey); + const { kid: theirKid } = JSON.parse( + device.availableCommands[COMMAND_SENDTAB] + ); + if (theirKid != ourKid) { + throw new Error("Target Send Tab key ID is different from ours"); + } + const json = JSON.parse(bundle); + const wrapper = new lazy.CryptoWrapper(); + wrapper.deserialize({ payload: json }); + const syncKeyBundle = lazy.BulkKeyBundle.fromJWK(oldsyncKey); + let { publicKey, authSecret } = await wrapper.decrypt(syncKeyBundle); + authSecret = urlsafeBase64Decode(authSecret); + publicKey = urlsafeBase64Decode(publicKey); + + const { ciphertext: encrypted } = await lazy.PushCrypto.encrypt( + bytes, + publicKey, + authSecret + ); + return urlsafeBase64Encode(encrypted); + } + + async _getPersistedSendTabKeys() { + const { device } = await this._fxai.getUserAccountData(["device"]); + return device && device.sendTabKeys; + } + + async _decrypt(ciphertext) { + let { privateKey, publicKey, authSecret } = + await this._getPersistedSendTabKeys(); + publicKey = urlsafeBase64Decode(publicKey); + authSecret = urlsafeBase64Decode(authSecret); + ciphertext = new Uint8Array(urlsafeBase64Decode(ciphertext)); + return lazy.PushCrypto.decrypt( + privateKey, + publicKey, + authSecret, + // The only Push encoding we support. + { encoding: "aes128gcm" }, + ciphertext + ); + } + + async _generateAndPersistSendTabKeys() { + let [publicKey, privateKey] = await lazy.PushCrypto.generateKeys(); + publicKey = urlsafeBase64Encode(publicKey); + let authSecret = lazy.PushCrypto.generateAuthenticationSecret(); + authSecret = urlsafeBase64Encode(authSecret); + const sendTabKeys = { + publicKey, + privateKey, + authSecret, + }; + await this._fxai.withCurrentAccountState(async state => { + const { device } = await state.getUserAccountData(["device"]); + await state.updateUserAccountData({ + device: { + ...device, + sendTabKeys, + }, + }); + }); + return sendTabKeys; + } + + async _getPersistedEncryptedSendTabKey() { + const { encryptedSendTabKeys } = await this._fxai.getUserAccountData([ + "encryptedSendTabKeys", + ]); + return encryptedSendTabKeys; + } + + async _generateAndPersistEncryptedSendTabKey() { + let sendTabKeys = await this._getPersistedSendTabKeys(); + if (!sendTabKeys) { + log.info("Could not find sendtab keys, generating them"); + sendTabKeys = await this._generateAndPersistSendTabKeys(); + } + // Strip the private key from the bundle to encrypt. + const keyToEncrypt = { + publicKey: sendTabKeys.publicKey, + authSecret: sendTabKeys.authSecret, + }; + if (!(await this._fxai.keys.canGetKeyForScope(SCOPE_OLD_SYNC))) { + log.info("Can't fetch keys, so unable to determine sendtab keys"); + return null; + } + let oldsyncKey; + try { + oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC); + } catch (ex) { + log.warn("Failed to fetch keys, so unable to determine sendtab keys", ex); + return null; + } + const wrapper = new lazy.CryptoWrapper(); + wrapper.cleartext = keyToEncrypt; + const keyBundle = lazy.BulkKeyBundle.fromJWK(oldsyncKey); + await wrapper.encrypt(keyBundle); + const encryptedSendTabKeys = JSON.stringify({ + // This is expected in hex, due to pre-JWK sync key ids :-( + kid: this._fxai.keys.kidAsHex(oldsyncKey), + IV: wrapper.IV, + hmac: wrapper.hmac, + ciphertext: wrapper.ciphertext, + }); + await this._fxai.withCurrentAccountState(async state => { + await state.updateUserAccountData({ + encryptedSendTabKeys, + }); + }); + return encryptedSendTabKeys; + } + + async getEncryptedSendTabKeys() { + let encryptedSendTabKeys = await this._getPersistedEncryptedSendTabKey(); + const sendTabKeys = await this._getPersistedSendTabKeys(); + if (!encryptedSendTabKeys || !sendTabKeys) { + log.info("Generating and persisting encrypted sendtab keys"); + // `_generateAndPersistEncryptedKeys` requires the sync key + // which cannot be accessed if the login manager is locked + // (i.e when the primary password is locked) or if the sync keys + // aren't accessible (account isn't verified) + // so this function could fail to retrieve the keys + // however, device registration will trigger when the account + // is verified, so it's OK + // Note that it's okay to persist those keys, because they are + // already persisted in plaintext and the encrypted bundle + // does not include the sync-key (the sync key is used to encrypt + // it though) + encryptedSendTabKeys = + await this._generateAndPersistEncryptedSendTabKey(); + } + return encryptedSendTabKeys; + } +} + +function urlsafeBase64Encode(buffer) { + return ChromeUtils.base64URLEncode(new Uint8Array(buffer), { pad: false }); +} + +function urlsafeBase64Decode(str) { + return ChromeUtils.base64URLDecode(str, { padding: "reject" }); +} diff --git a/services/fxaccounts/FxAccountsCommon.sys.mjs b/services/fxaccounts/FxAccountsCommon.sys.mjs new file mode 100644 index 0000000000..2688fc3c0a --- /dev/null +++ b/services/fxaccounts/FxAccountsCommon.sys.mjs @@ -0,0 +1,393 @@ +/* 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 { Log } from "resource://gre/modules/Log.sys.mjs"; +import { LogManager } from "resource://services-common/logmanager.sys.mjs"; + +// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config", +// "Debug", "Trace" or "All". If none is specified, "Debug" will be used by +// default. Note "Debug" is usually appropriate so that when this log is +// included in the Sync file logs we get verbose output. +const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel"; + +// A pref that can be set so "sensitive" information (eg, personally +// identifiable info, credentials, etc) will be logged. +const PREF_LOG_SENSITIVE_DETAILS = "identity.fxaccounts.log.sensitive"; + +export let log = Log.repository.getLogger("FirefoxAccounts"); +log.manageLevelFromPref(PREF_LOG_LEVEL); + +let logs = [ + "Sync", + "Services.Common", + "FirefoxAccounts", + "Hawk", + "browserwindow.syncui", + "BookmarkSyncUtils", + "addons.xpi", +]; + +// For legacy reasons, the log manager still thinks it's part of sync. +export let logManager = new LogManager("services.sync.", logs, "sync"); + +// A boolean to indicate if personally identifiable information (or anything +// else sensitive, such as credentials) should be logged. +export let logPII = () => + Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS, false); + +export let FXACCOUNTS_PERMISSION = "firefox-accounts"; + +export let DATA_FORMAT_VERSION = 1; +export let DEFAULT_STORAGE_FILENAME = "signedInUser.json"; + +export let OAUTH_TOKEN_FOR_SYNC_LIFETIME_SECONDS = 3600 * 6; // 6 hours + +// After we start polling for account verification, we stop polling when this +// many milliseconds have elapsed. +export let POLL_SESSION = 1000 * 60 * 20; // 20 minutes + +// Observer notifications. +export let ONLOGIN_NOTIFICATION = "fxaccounts:onlogin"; +export let ONVERIFIED_NOTIFICATION = "fxaccounts:onverified"; +export let ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout"; +export let ON_PRELOGOUT_NOTIFICATION = "fxaccounts:on_pre_logout"; +// Internal to services/fxaccounts only +export let ON_DEVICE_CONNECTED_NOTIFICATION = "fxaccounts:device_connected"; +export let ON_DEVICE_DISCONNECTED_NOTIFICATION = + "fxaccounts:device_disconnected"; +export let ON_PROFILE_UPDATED_NOTIFICATION = "fxaccounts:profile_updated"; // Push +export let ON_PASSWORD_CHANGED_NOTIFICATION = "fxaccounts:password_changed"; +export let ON_PASSWORD_RESET_NOTIFICATION = "fxaccounts:password_reset"; +export let ON_ACCOUNT_DESTROYED_NOTIFICATION = "fxaccounts:account_destroyed"; +export let ON_COLLECTION_CHANGED_NOTIFICATION = "sync:collection_changed"; +export let ON_VERIFY_LOGIN_NOTIFICATION = "fxaccounts:verify_login"; +export let ON_COMMAND_RECEIVED_NOTIFICATION = "fxaccounts:command_received"; + +export let FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update"; + +export let ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange"; // WebChannel +export let ON_ACCOUNT_STATE_CHANGE_NOTIFICATION = "fxaccounts:statechange"; +export let ON_NEW_DEVICE_ID = "fxaccounts:new_device_id"; +export let ON_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated"; + +// The common prefix for all commands. +export let COMMAND_PREFIX = "https://identity.mozilla.com/cmd/"; + +// The commands we support - only the _TAIL values are recorded in telemetry. +export let COMMAND_SENDTAB_TAIL = "open-uri"; +export let COMMAND_SENDTAB = COMMAND_PREFIX + COMMAND_SENDTAB_TAIL; + +// OAuth +export let FX_OAUTH_CLIENT_ID = "5882386c6d801776"; +export let SCOPE_PROFILE = "profile"; +export let SCOPE_PROFILE_WRITE = "profile:write"; +export let SCOPE_OLD_SYNC = "https://identity.mozilla.com/apps/oldsync"; + +// This scope was previously used to calculate a telemetry tracking identifier for +// the account, but that system has since been decommissioned. It's here entirely +// so that we can remove the corresponding key from storage if present. We should +// be safe to remove it after some sensible period of time has elapsed to allow +// most clients to update; ref Bug 1697596. +export let DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY = + "https://identity.mozilla.com/ids/ecosystem_telemetry"; + +// OAuth metadata for other Firefox-related services that we might need to know about +// in order to provide an enhanced user experience. +export let FX_MONITOR_OAUTH_CLIENT_ID = "802d56ef2a9af9fa"; +export let FX_RELAY_OAUTH_CLIENT_ID = "9ebfe2c2f9ea3c58"; +export let VPN_OAUTH_CLIENT_ID = "e6eb0d1e856335fc"; + +// UI Requests. +export let UI_REQUEST_SIGN_IN_FLOW = "signInFlow"; +export let UI_REQUEST_REFRESH_AUTH = "refreshAuthentication"; + +// Firefox Accounts WebChannel ID +export let WEBCHANNEL_ID = "account_updates"; + +// WebChannel commands +export let COMMAND_PAIR_HEARTBEAT = "fxaccounts:pair_heartbeat"; +export let COMMAND_PAIR_SUPP_METADATA = "fxaccounts:pair_supplicant_metadata"; +export let COMMAND_PAIR_AUTHORIZE = "fxaccounts:pair_authorize"; +export let COMMAND_PAIR_DECLINE = "fxaccounts:pair_decline"; +export let COMMAND_PAIR_COMPLETE = "fxaccounts:pair_complete"; + +export let COMMAND_PROFILE_CHANGE = "profile:change"; +export let COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account"; +export let COMMAND_LOGIN = "fxaccounts:login"; +export let COMMAND_OAUTH = "fxaccounts:oauth_login"; +export let COMMAND_LOGOUT = "fxaccounts:logout"; +export let COMMAND_DELETE = "fxaccounts:delete"; +export let COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences"; +export let COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password"; +export let COMMAND_FXA_STATUS = "fxaccounts:fxa_status"; +export let COMMAND_PAIR_PREFERENCES = "fxaccounts:pair_preferences"; +export let COMMAND_FIREFOX_VIEW = "fxaccounts:firefox_view"; + +// The pref branch where any prefs which relate to a specific account should +// be stored. This branch will be reset on account signout and signin. +export let PREF_ACCOUNT_ROOT = "identity.fxaccounts.account."; + +export let PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; +export let PREF_REMOTE_PAIRING_URI = "identity.fxaccounts.remote.pairing.uri"; + +// Server errno. +// From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format +export let ERRNO_ACCOUNT_ALREADY_EXISTS = 101; +export let ERRNO_ACCOUNT_DOES_NOT_EXIST = 102; +export let ERRNO_INCORRECT_PASSWORD = 103; +export let ERRNO_UNVERIFIED_ACCOUNT = 104; +export let ERRNO_INVALID_VERIFICATION_CODE = 105; +export let ERRNO_NOT_VALID_JSON_BODY = 106; +export let ERRNO_INVALID_BODY_PARAMETERS = 107; +export let ERRNO_MISSING_BODY_PARAMETERS = 108; +export let ERRNO_INVALID_REQUEST_SIGNATURE = 109; +export let ERRNO_INVALID_AUTH_TOKEN = 110; +export let ERRNO_INVALID_AUTH_TIMESTAMP = 111; +export let ERRNO_MISSING_CONTENT_LENGTH = 112; +export let ERRNO_REQUEST_BODY_TOO_LARGE = 113; +export let ERRNO_TOO_MANY_CLIENT_REQUESTS = 114; +export let ERRNO_INVALID_AUTH_NONCE = 115; +export let ERRNO_ENDPOINT_NO_LONGER_SUPPORTED = 116; +export let ERRNO_INCORRECT_LOGIN_METHOD = 117; +export let ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD = 118; +export let ERRNO_INCORRECT_API_VERSION = 119; +export let ERRNO_INCORRECT_EMAIL_CASE = 120; +export let ERRNO_ACCOUNT_LOCKED = 121; +export let ERRNO_ACCOUNT_UNLOCKED = 122; +export let ERRNO_UNKNOWN_DEVICE = 123; +export let ERRNO_DEVICE_SESSION_CONFLICT = 124; +export let ERRNO_SERVICE_TEMP_UNAVAILABLE = 201; +export let ERRNO_PARSE = 997; +export let ERRNO_NETWORK = 998; +export let ERRNO_UNKNOWN_ERROR = 999; + +// Offset oauth server errnos so they don't conflict with auth server errnos +export let OAUTH_SERVER_ERRNO_OFFSET = 1000; + +// OAuth Server errno. +export let ERRNO_UNKNOWN_CLIENT_ID = 101 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_INCORRECT_CLIENT_SECRET = 102 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_INCORRECT_REDIRECT_URI = 103 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_INVALID_FXA_ASSERTION = 104 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_UNKNOWN_CODE = 105 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_INCORRECT_CODE = 106 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_EXPIRED_CODE = 107 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_OAUTH_INVALID_TOKEN = 108 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_INVALID_REQUEST_PARAM = 109 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_INVALID_RESPONSE_TYPE = 110 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_UNAUTHORIZED = 111 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_FORBIDDEN = 112 + OAUTH_SERVER_ERRNO_OFFSET; +export let ERRNO_INVALID_CONTENT_TYPE = 113 + OAUTH_SERVER_ERRNO_OFFSET; + +// Errors. +export let ERROR_ACCOUNT_ALREADY_EXISTS = "ACCOUNT_ALREADY_EXISTS"; +export let ERROR_ACCOUNT_DOES_NOT_EXIST = "ACCOUNT_DOES_NOT_EXIST "; +export let ERROR_ACCOUNT_LOCKED = "ACCOUNT_LOCKED"; +export let ERROR_ACCOUNT_UNLOCKED = "ACCOUNT_UNLOCKED"; +export let ERROR_ALREADY_SIGNED_IN_USER = "ALREADY_SIGNED_IN_USER"; +export let ERROR_DEVICE_SESSION_CONFLICT = "DEVICE_SESSION_CONFLICT"; +export let ERROR_ENDPOINT_NO_LONGER_SUPPORTED = "ENDPOINT_NO_LONGER_SUPPORTED"; +export let ERROR_INCORRECT_API_VERSION = "INCORRECT_API_VERSION"; +export let ERROR_INCORRECT_EMAIL_CASE = "INCORRECT_EMAIL_CASE"; +export let ERROR_INCORRECT_KEY_RETRIEVAL_METHOD = + "INCORRECT_KEY_RETRIEVAL_METHOD"; +export let ERROR_INCORRECT_LOGIN_METHOD = "INCORRECT_LOGIN_METHOD"; +export let ERROR_INVALID_EMAIL = "INVALID_EMAIL"; +export let ERROR_INVALID_AUDIENCE = "INVALID_AUDIENCE"; +export let ERROR_INVALID_AUTH_TOKEN = "INVALID_AUTH_TOKEN"; +export let ERROR_INVALID_AUTH_TIMESTAMP = "INVALID_AUTH_TIMESTAMP"; +export let ERROR_INVALID_AUTH_NONCE = "INVALID_AUTH_NONCE"; +export let ERROR_INVALID_BODY_PARAMETERS = "INVALID_BODY_PARAMETERS"; +export let ERROR_INVALID_PASSWORD = "INVALID_PASSWORD"; +export let ERROR_INVALID_VERIFICATION_CODE = "INVALID_VERIFICATION_CODE"; +export let ERROR_INVALID_REFRESH_AUTH_VALUE = "INVALID_REFRESH_AUTH_VALUE"; +export let ERROR_INVALID_REQUEST_SIGNATURE = "INVALID_REQUEST_SIGNATURE"; +export let ERROR_INTERNAL_INVALID_USER = "INTERNAL_ERROR_INVALID_USER"; +export let ERROR_MISSING_BODY_PARAMETERS = "MISSING_BODY_PARAMETERS"; +export let ERROR_MISSING_CONTENT_LENGTH = "MISSING_CONTENT_LENGTH"; +export let ERROR_NO_TOKEN_SESSION = "NO_TOKEN_SESSION"; +export let ERROR_NO_SILENT_REFRESH_AUTH = "NO_SILENT_REFRESH_AUTH"; +export let ERROR_NOT_VALID_JSON_BODY = "NOT_VALID_JSON_BODY"; +export let ERROR_OFFLINE = "OFFLINE"; +export let ERROR_PERMISSION_DENIED = "PERMISSION_DENIED"; +export let ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE"; +export let ERROR_SERVER_ERROR = "SERVER_ERROR"; +export let ERROR_SYNC_DISABLED = "SYNC_DISABLED"; +export let ERROR_TOO_MANY_CLIENT_REQUESTS = "TOO_MANY_CLIENT_REQUESTS"; +export let ERROR_SERVICE_TEMP_UNAVAILABLE = "SERVICE_TEMPORARY_UNAVAILABLE"; +export let ERROR_UI_ERROR = "UI_ERROR"; +export let ERROR_UI_REQUEST = "UI_REQUEST"; +export let ERROR_PARSE = "PARSE_ERROR"; +export let ERROR_NETWORK = "NETWORK_ERROR"; +export let ERROR_UNKNOWN = "UNKNOWN_ERROR"; +export let ERROR_UNKNOWN_DEVICE = "UNKNOWN_DEVICE"; +export let ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT"; + +// OAuth errors. +export let ERROR_UNKNOWN_CLIENT_ID = "UNKNOWN_CLIENT_ID"; +export let ERROR_INCORRECT_CLIENT_SECRET = "INCORRECT_CLIENT_SECRET"; +export let ERROR_INCORRECT_REDIRECT_URI = "INCORRECT_REDIRECT_URI"; +export let ERROR_INVALID_FXA_ASSERTION = "INVALID_FXA_ASSERTION"; +export let ERROR_UNKNOWN_CODE = "UNKNOWN_CODE"; +export let ERROR_INCORRECT_CODE = "INCORRECT_CODE"; +export let ERROR_EXPIRED_CODE = "EXPIRED_CODE"; +export let ERROR_OAUTH_INVALID_TOKEN = "OAUTH_INVALID_TOKEN"; +export let ERROR_INVALID_REQUEST_PARAM = "INVALID_REQUEST_PARAM"; +export let ERROR_INVALID_RESPONSE_TYPE = "INVALID_RESPONSE_TYPE"; +export let ERROR_UNAUTHORIZED = "UNAUTHORIZED"; +export let ERROR_FORBIDDEN = "FORBIDDEN"; +export let ERROR_INVALID_CONTENT_TYPE = "INVALID_CONTENT_TYPE"; + +// Additional generic error classes for external consumers +export let ERROR_NO_ACCOUNT = "NO_ACCOUNT"; +export let ERROR_AUTH_ERROR = "AUTH_ERROR"; +export let ERROR_INVALID_PARAMETER = "INVALID_PARAMETER"; + +// Status code errors +export let ERROR_CODE_METHOD_NOT_ALLOWED = 405; +export let ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED"; + +// FxAccounts has the ability to "split" the credentials between a plain-text +// JSON file in the profile dir and in the login manager. +// In order to prevent new fields accidentally ending up in the "wrong" place, +// all fields stored are listed here. + +// The fields we save in the plaintext JSON. +// See bug 1013064 comments 23-25 for why the sessionToken is "safe" +export let FXA_PWDMGR_PLAINTEXT_FIELDS = new Set([ + "email", + "verified", + "authAt", + "sessionToken", + "uid", + "oauthTokens", + "profile", + "device", + "profileCache", + "encryptedSendTabKeys", +]); + +// Fields we store in secure storage if it exists. +export let FXA_PWDMGR_SECURE_FIELDS = new Set([ + "keyFetchToken", + "unwrapBKey", + "scopedKeys", +]); + +// An allowlist of fields that remain in storage when the user needs to +// reauthenticate. All other fields will be removed. +export let FXA_PWDMGR_REAUTH_ALLOWLIST = new Set([ + "email", + "uid", + "profile", + "device", + "verified", +]); + +// The pseudo-host we use in the login manager +export let FXA_PWDMGR_HOST = "chrome://FirefoxAccounts"; +// The realm we use in the login manager. +export let FXA_PWDMGR_REALM = "Firefox Accounts credentials"; + +// Error matching. +export let SERVER_ERRNO_TO_ERROR = { + [ERRNO_ACCOUNT_ALREADY_EXISTS]: ERROR_ACCOUNT_ALREADY_EXISTS, + [ERRNO_ACCOUNT_DOES_NOT_EXIST]: ERROR_ACCOUNT_DOES_NOT_EXIST, + [ERRNO_INCORRECT_PASSWORD]: ERROR_INVALID_PASSWORD, + [ERRNO_UNVERIFIED_ACCOUNT]: ERROR_UNVERIFIED_ACCOUNT, + [ERRNO_INVALID_VERIFICATION_CODE]: ERROR_INVALID_VERIFICATION_CODE, + [ERRNO_NOT_VALID_JSON_BODY]: ERROR_NOT_VALID_JSON_BODY, + [ERRNO_INVALID_BODY_PARAMETERS]: ERROR_INVALID_BODY_PARAMETERS, + [ERRNO_MISSING_BODY_PARAMETERS]: ERROR_MISSING_BODY_PARAMETERS, + [ERRNO_INVALID_REQUEST_SIGNATURE]: ERROR_INVALID_REQUEST_SIGNATURE, + [ERRNO_INVALID_AUTH_TOKEN]: ERROR_INVALID_AUTH_TOKEN, + [ERRNO_INVALID_AUTH_TIMESTAMP]: ERROR_INVALID_AUTH_TIMESTAMP, + [ERRNO_MISSING_CONTENT_LENGTH]: ERROR_MISSING_CONTENT_LENGTH, + [ERRNO_REQUEST_BODY_TOO_LARGE]: ERROR_REQUEST_BODY_TOO_LARGE, + [ERRNO_TOO_MANY_CLIENT_REQUESTS]: ERROR_TOO_MANY_CLIENT_REQUESTS, + [ERRNO_INVALID_AUTH_NONCE]: ERROR_INVALID_AUTH_NONCE, + [ERRNO_ENDPOINT_NO_LONGER_SUPPORTED]: ERROR_ENDPOINT_NO_LONGER_SUPPORTED, + [ERRNO_INCORRECT_LOGIN_METHOD]: ERROR_INCORRECT_LOGIN_METHOD, + [ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD]: ERROR_INCORRECT_KEY_RETRIEVAL_METHOD, + [ERRNO_INCORRECT_API_VERSION]: ERROR_INCORRECT_API_VERSION, + [ERRNO_INCORRECT_EMAIL_CASE]: ERROR_INCORRECT_EMAIL_CASE, + [ERRNO_ACCOUNT_LOCKED]: ERROR_ACCOUNT_LOCKED, + [ERRNO_ACCOUNT_UNLOCKED]: ERROR_ACCOUNT_UNLOCKED, + [ERRNO_UNKNOWN_DEVICE]: ERROR_UNKNOWN_DEVICE, + [ERRNO_DEVICE_SESSION_CONFLICT]: ERROR_DEVICE_SESSION_CONFLICT, + [ERRNO_SERVICE_TEMP_UNAVAILABLE]: ERROR_SERVICE_TEMP_UNAVAILABLE, + [ERRNO_UNKNOWN_ERROR]: ERROR_UNKNOWN, + [ERRNO_NETWORK]: ERROR_NETWORK, + // oauth + [ERRNO_UNKNOWN_CLIENT_ID]: ERROR_UNKNOWN_CLIENT_ID, + [ERRNO_INCORRECT_CLIENT_SECRET]: ERROR_INCORRECT_CLIENT_SECRET, + [ERRNO_INCORRECT_REDIRECT_URI]: ERROR_INCORRECT_REDIRECT_URI, + [ERRNO_INVALID_FXA_ASSERTION]: ERROR_INVALID_FXA_ASSERTION, + [ERRNO_UNKNOWN_CODE]: ERROR_UNKNOWN_CODE, + [ERRNO_INCORRECT_CODE]: ERROR_INCORRECT_CODE, + [ERRNO_EXPIRED_CODE]: ERROR_EXPIRED_CODE, + [ERRNO_OAUTH_INVALID_TOKEN]: ERROR_OAUTH_INVALID_TOKEN, + [ERRNO_INVALID_REQUEST_PARAM]: ERROR_INVALID_REQUEST_PARAM, + [ERRNO_INVALID_RESPONSE_TYPE]: ERROR_INVALID_RESPONSE_TYPE, + [ERRNO_UNAUTHORIZED]: ERROR_UNAUTHORIZED, + [ERRNO_FORBIDDEN]: ERROR_FORBIDDEN, + [ERRNO_INVALID_CONTENT_TYPE]: ERROR_INVALID_CONTENT_TYPE, +}; + +// Map internal errors to more generic error classes for consumers +export let ERROR_TO_GENERAL_ERROR_CLASS = { + [ERROR_ACCOUNT_ALREADY_EXISTS]: ERROR_AUTH_ERROR, + [ERROR_ACCOUNT_DOES_NOT_EXIST]: ERROR_AUTH_ERROR, + [ERROR_ACCOUNT_LOCKED]: ERROR_AUTH_ERROR, + [ERROR_ACCOUNT_UNLOCKED]: ERROR_AUTH_ERROR, + [ERROR_ALREADY_SIGNED_IN_USER]: ERROR_AUTH_ERROR, + [ERROR_DEVICE_SESSION_CONFLICT]: ERROR_AUTH_ERROR, + [ERROR_ENDPOINT_NO_LONGER_SUPPORTED]: ERROR_AUTH_ERROR, + [ERROR_INCORRECT_API_VERSION]: ERROR_AUTH_ERROR, + [ERROR_INCORRECT_EMAIL_CASE]: ERROR_AUTH_ERROR, + [ERROR_INCORRECT_KEY_RETRIEVAL_METHOD]: ERROR_AUTH_ERROR, + [ERROR_INCORRECT_LOGIN_METHOD]: ERROR_AUTH_ERROR, + [ERROR_INVALID_EMAIL]: ERROR_AUTH_ERROR, + [ERROR_INVALID_AUDIENCE]: ERROR_AUTH_ERROR, + [ERROR_INVALID_AUTH_TOKEN]: ERROR_AUTH_ERROR, + [ERROR_INVALID_AUTH_TIMESTAMP]: ERROR_AUTH_ERROR, + [ERROR_INVALID_AUTH_NONCE]: ERROR_AUTH_ERROR, + [ERROR_INVALID_BODY_PARAMETERS]: ERROR_AUTH_ERROR, + [ERROR_INVALID_PASSWORD]: ERROR_AUTH_ERROR, + [ERROR_INVALID_VERIFICATION_CODE]: ERROR_AUTH_ERROR, + [ERROR_INVALID_REFRESH_AUTH_VALUE]: ERROR_AUTH_ERROR, + [ERROR_INVALID_REQUEST_SIGNATURE]: ERROR_AUTH_ERROR, + [ERROR_INTERNAL_INVALID_USER]: ERROR_AUTH_ERROR, + [ERROR_MISSING_BODY_PARAMETERS]: ERROR_AUTH_ERROR, + [ERROR_MISSING_CONTENT_LENGTH]: ERROR_AUTH_ERROR, + [ERROR_NO_TOKEN_SESSION]: ERROR_AUTH_ERROR, + [ERROR_NO_SILENT_REFRESH_AUTH]: ERROR_AUTH_ERROR, + [ERROR_NOT_VALID_JSON_BODY]: ERROR_AUTH_ERROR, + [ERROR_PERMISSION_DENIED]: ERROR_AUTH_ERROR, + [ERROR_REQUEST_BODY_TOO_LARGE]: ERROR_AUTH_ERROR, + [ERROR_UNKNOWN_DEVICE]: ERROR_AUTH_ERROR, + [ERROR_UNVERIFIED_ACCOUNT]: ERROR_AUTH_ERROR, + [ERROR_UI_ERROR]: ERROR_AUTH_ERROR, + [ERROR_UI_REQUEST]: ERROR_AUTH_ERROR, + [ERROR_OFFLINE]: ERROR_NETWORK, + [ERROR_SERVER_ERROR]: ERROR_NETWORK, + [ERROR_TOO_MANY_CLIENT_REQUESTS]: ERROR_NETWORK, + [ERROR_SERVICE_TEMP_UNAVAILABLE]: ERROR_NETWORK, + [ERROR_PARSE]: ERROR_NETWORK, + [ERROR_NETWORK]: ERROR_NETWORK, + + // oauth + [ERROR_INCORRECT_CLIENT_SECRET]: ERROR_AUTH_ERROR, + [ERROR_INCORRECT_REDIRECT_URI]: ERROR_AUTH_ERROR, + [ERROR_INVALID_FXA_ASSERTION]: ERROR_AUTH_ERROR, + [ERROR_UNKNOWN_CODE]: ERROR_AUTH_ERROR, + [ERROR_INCORRECT_CODE]: ERROR_AUTH_ERROR, + [ERROR_EXPIRED_CODE]: ERROR_AUTH_ERROR, + [ERROR_OAUTH_INVALID_TOKEN]: ERROR_AUTH_ERROR, + [ERROR_INVALID_REQUEST_PARAM]: ERROR_AUTH_ERROR, + [ERROR_INVALID_RESPONSE_TYPE]: ERROR_AUTH_ERROR, + [ERROR_UNAUTHORIZED]: ERROR_AUTH_ERROR, + [ERROR_FORBIDDEN]: ERROR_AUTH_ERROR, + [ERROR_INVALID_CONTENT_TYPE]: ERROR_AUTH_ERROR, +}; diff --git a/services/fxaccounts/FxAccountsConfig.sys.mjs b/services/fxaccounts/FxAccountsConfig.sys.mjs new file mode 100644 index 0000000000..cf26704a50 --- /dev/null +++ b/services/fxaccounts/FxAccountsConfig.sys.mjs @@ -0,0 +1,360 @@ +/* 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 { RESTRequest } from "resource://services-common/rest.sys.mjs"; + +import { + log, + SCOPE_OLD_SYNC, + SCOPE_PROFILE, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton(); +}); + +ChromeUtils.defineESModuleGetters(lazy, { + EnsureFxAccountsWebChannel: + "resource://gre/modules/FxAccountsWebChannel.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "ROOT_URL", + "identity.fxaccounts.remote.root" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "CONTEXT_PARAM", + "identity.fxaccounts.contextParam" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "REQUIRES_HTTPS", + "identity.fxaccounts.allowHttp", + false, + null, + val => !val +); + +const CONFIG_PREFS = [ + "identity.fxaccounts.remote.root", + "identity.fxaccounts.auth.uri", + "identity.fxaccounts.remote.oauth.uri", + "identity.fxaccounts.remote.profile.uri", + "identity.fxaccounts.remote.pairing.uri", + "identity.sync.tokenserver.uri", +]; +const SYNC_PARAM = "sync"; + +export var FxAccountsConfig = { + async promiseEmailURI(email, entrypoint, extraParams = {}) { + const authParams = await this._getAuthParams(); + return this._buildURL("", { + extraParams: { + entrypoint, + email, + ...authParams, + ...extraParams, + }, + }); + }, + + async promiseConnectAccountURI(entrypoint, extraParams = {}) { + const authParams = await this._getAuthParams(); + return this._buildURL("", { + extraParams: { + entrypoint, + action: "email", + ...authParams, + ...extraParams, + }, + }); + }, + + async promiseForceSigninURI(entrypoint, extraParams = {}) { + const authParams = await this._getAuthParams(); + return this._buildURL("force_auth", { + extraParams: { entrypoint, ...authParams, ...extraParams }, + addAccountIdentifiers: true, + }); + }, + + async promiseManageURI(entrypoint, extraParams = {}) { + return this._buildURL("settings", { + extraParams: { entrypoint, ...extraParams }, + addAccountIdentifiers: true, + }); + }, + + async promiseChangeAvatarURI(entrypoint, extraParams = {}) { + return this._buildURL("settings/avatar/change", { + extraParams: { entrypoint, ...extraParams }, + addAccountIdentifiers: true, + }); + }, + + async promiseManageDevicesURI(entrypoint, extraParams = {}) { + return this._buildURL("settings/clients", { + extraParams: { entrypoint, ...extraParams }, + addAccountIdentifiers: true, + }); + }, + + async promiseConnectDeviceURI(entrypoint, extraParams = {}) { + return this._buildURL("connect_another_device", { + extraParams: { entrypoint, service: SYNC_PARAM, ...extraParams }, + addAccountIdentifiers: true, + }); + }, + + async promisePairingURI(extraParams = {}) { + return this._buildURL("pair", { + extraParams, + includeDefaultParams: false, + }); + }, + + async promiseOAuthURI(extraParams = {}) { + return this._buildURL("oauth", { + extraParams, + includeDefaultParams: false, + }); + }, + + async promiseMetricsFlowURI(entrypoint, extraParams = {}) { + return this._buildURL("metrics-flow", { + extraParams: { entrypoint, ...extraParams }, + includeDefaultParams: false, + }); + }, + + get defaultParams() { + return { context: lazy.CONTEXT_PARAM }; + }, + + /** + * @param path should be parsable by the URL constructor first parameter. + * @param {bool} [options.includeDefaultParams] If true include the default search params. + * @param {Object.} [options.extraParams] Additionnal search params. + * @param {bool} [options.addAccountIdentifiers] if true we add the current logged-in user uid and email to the search params. + */ + async _buildURL( + path, + { + includeDefaultParams = true, + extraParams = {}, + addAccountIdentifiers = false, + } + ) { + await this.ensureConfigured(); + const url = new URL(path, lazy.ROOT_URL); + if (lazy.REQUIRES_HTTPS && url.protocol != "https:") { + throw new Error("Firefox Accounts server must use HTTPS"); + } + const params = { + ...(includeDefaultParams ? this.defaultParams : null), + ...extraParams, + }; + for (let [k, v] of Object.entries(params)) { + url.searchParams.append(k, v); + } + if (addAccountIdentifiers) { + const accountData = await this.getSignedInUser(); + if (!accountData) { + return null; + } + url.searchParams.append("uid", accountData.uid); + url.searchParams.append("email", accountData.email); + } + return url.href; + }, + + async _buildURLFromString(href, extraParams = {}) { + const url = new URL(href); + for (let [k, v] of Object.entries(extraParams)) { + url.searchParams.append(k, v); + } + return url.href; + }, + + resetConfigURLs() { + let autoconfigURL = this.getAutoConfigURL(); + if (!autoconfigURL) { + return; + } + // They have the autoconfig uri pref set, so we clear all the prefs that we + // will have initialized, which will leave them pointing at production. + for (let pref of CONFIG_PREFS) { + Services.prefs.clearUserPref(pref); + } + // Reset the webchannel. + lazy.EnsureFxAccountsWebChannel(); + }, + + getAutoConfigURL() { + let pref = Services.prefs.getStringPref( + "identity.fxaccounts.autoconfig.uri", + "" + ); + if (!pref) { + // no pref / empty pref means we don't bother here. + return ""; + } + let rootURL = Services.urlFormatter.formatURL(pref); + if (rootURL.endsWith("/")) { + rootURL = rootURL.slice(0, -1); + } + return rootURL; + }, + + async ensureConfigured() { + let isSignedIn = !!(await this.getSignedInUser()); + if (!isSignedIn) { + await this.updateConfigURLs(); + } + }, + + // Returns true if this user is using the FxA "production" systems, false + // if using any other configuration, including self-hosting or the FxA + // non-production systems such as "dev" or "staging". + // It's typically used as a proxy for "is this likely to be a self-hosted + // user?", but it's named this way to make the implementation less + // surprising. As a result, it's fairly conservative and would prefer to have + // a false-negative than a false-position as it determines things which users + // might consider sensitive (notably, telemetry). + // Note also that while it's possible to self-host just sync and not FxA, we + // don't make that distinction - that's a self-hoster from the POV of this + // function. + isProductionConfig() { + // Specifically, if the autoconfig URLs, or *any* of the URLs that + // we consider configurable are modified, we assume self-hosted. + if (this.getAutoConfigURL()) { + return false; + } + for (let pref of CONFIG_PREFS) { + if (Services.prefs.prefHasUserValue(pref)) { + return false; + } + } + return true; + }, + + // Read expected client configuration from the fxa auth server + // (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration) + // and replace all the relevant our prefs with the information found there. + // This is only done before sign-in and sign-up, and even then only if the + // `identity.fxaccounts.autoconfig.uri` preference is set. + async updateConfigURLs() { + let rootURL = this.getAutoConfigURL(); + if (!rootURL) { + return; + } + const config = await this.fetchConfigDocument(rootURL); + try { + // Update the prefs directly specified by the config. + let authServerBase = config.auth_server_base_url; + if (!authServerBase.endsWith("/v1")) { + authServerBase += "/v1"; + } + Services.prefs.setStringPref( + "identity.fxaccounts.auth.uri", + authServerBase + ); + Services.prefs.setStringPref( + "identity.fxaccounts.remote.oauth.uri", + config.oauth_server_base_url + "/v1" + ); + // At the time of landing this, our servers didn't yet answer with pairing_server_base_uri. + // Remove this condition check once Firefox 68 is stable. + if (config.pairing_server_base_uri) { + Services.prefs.setStringPref( + "identity.fxaccounts.remote.pairing.uri", + config.pairing_server_base_uri + ); + } + Services.prefs.setStringPref( + "identity.fxaccounts.remote.profile.uri", + config.profile_server_base_url + "/v1" + ); + Services.prefs.setStringPref( + "identity.sync.tokenserver.uri", + config.sync_tokenserver_base_url + "/1.0/sync/1.5" + ); + Services.prefs.setStringPref("identity.fxaccounts.remote.root", rootURL); + + // Ensure the webchannel is pointed at the correct uri + lazy.EnsureFxAccountsWebChannel(); + } catch (e) { + log.error( + "Failed to initialize configuration preferences from autoconfig object", + e + ); + throw e; + } + }, + + // Read expected client configuration from the fxa auth server + // (or from the provided rootURL, if present) and return it as an object. + async fetchConfigDocument(rootURL = null) { + if (!rootURL) { + rootURL = lazy.ROOT_URL; + } + let configURL = rootURL + "/.well-known/fxa-client-configuration"; + let request = new RESTRequest(configURL); + request.setHeader("Accept", "application/json"); + + // Catch and rethrow the error inline. + let resp = await request.get().catch(e => { + log.error(`Failed to get configuration object from "${configURL}"`, e); + throw e; + }); + if (!resp.success) { + // Note: 'resp.body' is included with the error log below as we are not concerned + // that the body will contain PII, but if that changes it should be excluded. + log.error( + `Received HTTP response code ${resp.status} from configuration object request: + ${resp.body}` + ); + throw new Error( + `HTTP status ${resp.status} from configuration object request` + ); + } + log.debug("Got successful configuration response", resp.body); + try { + return JSON.parse(resp.body); + } catch (e) { + log.error( + `Failed to parse configuration preferences from ${configURL}`, + e + ); + throw e; + } + }, + + // For test purposes, returns a Promise. + getSignedInUser() { + return lazy.fxAccounts.getSignedInUser(); + }, + + _isOAuthFlow() { + return Services.prefs.getBoolPref( + "identity.fxaccounts.oauth.enabled", + false + ); + }, + + async _getAuthParams() { + if (this._isOAuthFlow()) { + const scopes = [SCOPE_OLD_SYNC, SCOPE_PROFILE]; + return lazy.fxAccounts._internal.beginOAuthFlow(scopes); + } + return { service: SYNC_PARAM }; + }, +}; diff --git a/services/fxaccounts/FxAccountsDevice.sys.mjs b/services/fxaccounts/FxAccountsDevice.sys.mjs new file mode 100644 index 0000000000..6b2089739c --- /dev/null +++ b/services/fxaccounts/FxAccountsDevice.sys.mjs @@ -0,0 +1,640 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { + log, + ERRNO_DEVICE_SESSION_CONFLICT, + ERRNO_UNKNOWN_DEVICE, + ON_NEW_DEVICE_ID, + ON_DEVICELIST_UPDATED, + ON_DEVICE_CONNECTED_NOTIFICATION, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + ONVERIFIED_NOTIFICATION, + PREF_ACCOUNT_ROOT, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +import { DEVICE_TYPE_DESKTOP } from "resource://services-sync/constants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CommonUtils: "resource://services-common/utils.sys.mjs", +}); + +const PREF_LOCAL_DEVICE_NAME = PREF_ACCOUNT_ROOT + "device.name"; +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "pref_localDeviceName", + PREF_LOCAL_DEVICE_NAME, + "" +); + +const PREF_DEPRECATED_DEVICE_NAME = "services.sync.client.name"; + +// Sanitizes all characters which the FxA server considers invalid, replacing +// them with the unicode replacement character. +// At time of writing, FxA has a regex DISPLAY_SAFE_UNICODE_WITH_NON_BMP, which +// the regex below is based on. +const INVALID_NAME_CHARS = + // eslint-disable-next-line no-control-regex + /[\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFC\uFFFE-\uFFFF]/g; +const MAX_NAME_LEN = 255; +const REPLACEMENT_CHAR = "\uFFFD"; + +function sanitizeDeviceName(name) { + return name + .substr(0, MAX_NAME_LEN) + .replace(INVALID_NAME_CHARS, REPLACEMENT_CHAR); +} + +// Everything to do with FxA devices. +export class FxAccountsDevice { + constructor(fxai) { + this._fxai = fxai; + this._deviceListCache = null; + this._fetchAndCacheDeviceListPromise = null; + + // The current version of the device registration, we use this to re-register + // devices after we update what we send on device registration. + this.DEVICE_REGISTRATION_VERSION = 2; + + // This is to avoid multiple sequential syncs ending up calling + // this expensive endpoint multiple times in a row. + this.TIME_BETWEEN_FXA_DEVICES_FETCH_MS = 1 * 60 * 1000; // 1 minute + + // Invalidate our cached device list when a device is connected or disconnected. + Services.obs.addObserver(this, ON_DEVICE_CONNECTED_NOTIFICATION, true); + Services.obs.addObserver(this, ON_DEVICE_DISCONNECTED_NOTIFICATION, true); + // A user becoming verified probably means we need to re-register the device + // because we are now able to get the sendtab keys. + Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, true); + } + + async getLocalId() { + return this._withCurrentAccountState(currentState => { + // It turns out _updateDeviceRegistrationIfNecessary() does exactly what we + // need. + return this._updateDeviceRegistrationIfNecessary(currentState); + }); + } + + // Generate a client name if we don't have a useful one yet + getDefaultLocalName() { + let user = Services.env.get("USER") || Services.env.get("USERNAME"); + // Note that we used to fall back to the "services.sync.username" pref here, + // but that's no longer suitable in a world where sync might not be + // configured. However, we almost never *actually* fell back to that, and + // doing so sanely here would mean making this function async, which we don't + // really want to do yet. + + // A little hack for people using the the moz-build environment on Windows + // which sets USER to the literal "%USERNAME%" (yes, really) + if (user == "%USERNAME%" && Services.env.get("USERNAME")) { + user = Services.env.get("USERNAME"); + } + + // The DNS service may fail to provide a hostname in edge-cases we don't + // fully understand - bug 1391488. + let hostname; + try { + // hostname of the system, usually assigned by the user or admin + hostname = Services.dns.myHostName; + } catch (ex) { + console.error(ex); + } + let system = + // 'device' is defined on unix systems + Services.sysinfo.get("device") || + hostname || + // fall back on ua info string + Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ).oscpu; + + const l10n = new Localization( + ["services/accounts.ftl", "branding/brand.ftl"], + true + ); + return sanitizeDeviceName( + l10n.formatValueSync("account-client-name", { user, system }) + ); + } + + getLocalName() { + // We used to store this in services.sync.client.name, but now store it + // under an fxa-specific location. + let deprecated_value = Services.prefs.getStringPref( + PREF_DEPRECATED_DEVICE_NAME, + "" + ); + if (deprecated_value) { + Services.prefs.setStringPref(PREF_LOCAL_DEVICE_NAME, deprecated_value); + Services.prefs.clearUserPref(PREF_DEPRECATED_DEVICE_NAME); + } + let name = lazy.pref_localDeviceName; + if (!name) { + name = this.getDefaultLocalName(); + Services.prefs.setStringPref(PREF_LOCAL_DEVICE_NAME, name); + } + // We need to sanitize here because some names were generated before we + // started sanitizing. + return sanitizeDeviceName(name); + } + + setLocalName(newName) { + Services.prefs.clearUserPref(PREF_DEPRECATED_DEVICE_NAME); + Services.prefs.setStringPref( + PREF_LOCAL_DEVICE_NAME, + sanitizeDeviceName(newName) + ); + // Update the registration in the background. + this.updateDeviceRegistration().catch(error => { + log.warn("failed to update fxa device registration", error); + }); + } + + getLocalType() { + return DEVICE_TYPE_DESKTOP; + } + + /** + * Returns the most recently fetched device list, or `null` if the list + * hasn't been fetched yet. This is synchronous, so that consumers like + * Send Tab can render the device list right away, without waiting for + * it to refresh. + * + * @type {?Array} + */ + get recentDeviceList() { + return this._deviceListCache ? this._deviceListCache.devices : null; + } + + /** + * Refreshes the device list. After this function returns, consumers can + * access the new list using the `recentDeviceList` getter. Note that + * multiple concurrent calls to `refreshDeviceList` will only refresh the + * list once. + * + * @param {Boolean} [options.ignoreCached] + * If `true`, forces a refresh, even if the cached device list is + * still fresh. Defaults to `false`. + * @return {Promise} + * `true` if the list was refreshed, `false` if the cached list is + * fresh. Rejects if an error occurs refreshing the list or device + * push registration. + */ + async refreshDeviceList({ ignoreCached = false } = {}) { + // If we're already refreshing the list in the background, let that finish. + if (this._fetchAndCacheDeviceListPromise) { + log.info("Already fetching device list, return existing promise"); + return this._fetchAndCacheDeviceListPromise; + } + + // If the cache is fresh enough, don't refresh it again. + if (!ignoreCached && this._deviceListCache) { + const ageOfCache = this._fxai.now() - this._deviceListCache.lastFetch; + if (ageOfCache < this.TIME_BETWEEN_FXA_DEVICES_FETCH_MS) { + log.info("Device list cache is fresh, re-using it"); + return false; + } + } + + log.info("fetching updated device list"); + this._fetchAndCacheDeviceListPromise = (async () => { + try { + const devices = await this._withVerifiedAccountState( + async currentState => { + const accountData = await currentState.getUserAccountData([ + "sessionToken", + "device", + ]); + const devices = await this._fxai.fxAccountsClient.getDeviceList( + accountData.sessionToken + ); + log.info( + `Got new device list: ${devices.map(d => d.id).join(", ")}` + ); + + await this._refreshRemoteDevice(currentState, accountData, devices); + return devices; + } + ); + log.info("updating the cache"); + // Be careful to only update the cache once the above has resolved, so + // we know that the current account state didn't change underneath us. + this._deviceListCache = { + lastFetch: this._fxai.now(), + devices, + }; + Services.obs.notifyObservers(null, ON_DEVICELIST_UPDATED); + return true; + } finally { + this._fetchAndCacheDeviceListPromise = null; + } + })(); + return this._fetchAndCacheDeviceListPromise; + } + + async _refreshRemoteDevice(currentState, accountData, remoteDevices) { + // Check if our push registration previously succeeded and is still + // good (although background device registration means it's possible + // we'll be fetching the device list before we've actually + // registered ourself!) + // (For a missing subscription we check for an explicit 'null' - + // both to help tests and as a safety valve - missing might mean + // "no push available" for self-hosters or similar?) + const ourDevice = remoteDevices.find(device => device.isCurrentDevice); + const subscription = await this._fxai.fxaPushService.getSubscription(); + if ( + ourDevice && + (ourDevice.pushCallback === null || // fxa server doesn't know our subscription. + ourDevice.pushEndpointExpired || // fxa server thinks it has expired. + !subscription || // we don't have a local subscription. + subscription.isExpired() || // our local subscription is expired. + ourDevice.pushCallback != subscription.endpoint) // we don't agree with fxa. + ) { + log.warn(`Our push endpoint needs resubscription`); + await this._fxai.fxaPushService.unsubscribe(); + await this._registerOrUpdateDevice(currentState, accountData); + // and there's a reasonable chance there are commands waiting. + await this._fxai.commands.pollDeviceCommands(); + } else if ( + ourDevice && + (await this._checkRemoteCommandsUpdateNeeded(ourDevice.availableCommands)) + ) { + log.warn(`Our commands need to be updated on the server`); + await this._registerOrUpdateDevice(currentState, accountData); + } else { + log.trace(`Our push subscription looks OK`); + } + } + + async updateDeviceRegistration() { + return this._withCurrentAccountState(async currentState => { + const signedInUser = await currentState.getUserAccountData([ + "sessionToken", + "device", + ]); + if (signedInUser) { + await this._registerOrUpdateDevice(currentState, signedInUser); + } + }); + } + + async updateDeviceRegistrationIfNecessary() { + return this._withCurrentAccountState(currentState => { + return this._updateDeviceRegistrationIfNecessary(currentState); + }); + } + + reset() { + this._deviceListCache = null; + this._fetchAndCacheDeviceListPromise = null; + } + + /** + * Here begin our internal helper methods. + * + * Many of these methods take the current account state as first argument, + * in order to avoid racing our state updates with e.g. the uer signing + * out while we're in the middle of an update. If this does happen, the + * resulting promise will be rejected rather than persisting stale state. + * + */ + + _withCurrentAccountState(func) { + return this._fxai.withCurrentAccountState(async currentState => { + try { + return await func(currentState); + } catch (err) { + // `_handleTokenError` always throws, this syntax keeps the linter happy. + // TODO: probably `_handleTokenError` could be done by `_fxai.withCurrentAccountState` + // internally rather than us having to remember to do it here. + throw await this._fxai._handleTokenError(err); + } + }); + } + + _withVerifiedAccountState(func) { + return this._fxai.withVerifiedAccountState(async currentState => { + try { + return await func(currentState); + } catch (err) { + // `_handleTokenError` always throws, this syntax keeps the linter happy. + throw await this._fxai._handleTokenError(err); + } + }); + } + + async _checkDeviceUpdateNeeded(device) { + // There is no device registered or the device registration is outdated. + // Either way, we should register the device with FxA + // before returning the id to the caller. + const availableCommandsKeys = Object.keys( + await this._fxai.commands.availableCommands() + ).sort(); + return ( + !device || + !device.registrationVersion || + device.registrationVersion < this.DEVICE_REGISTRATION_VERSION || + !device.registeredCommandsKeys || + !lazy.CommonUtils.arrayEqual( + device.registeredCommandsKeys, + availableCommandsKeys + ) + ); + } + + async _checkRemoteCommandsUpdateNeeded(remoteAvailableCommands) { + if (!remoteAvailableCommands) { + return true; + } + const remoteAvailableCommandsKeys = Object.keys( + remoteAvailableCommands + ).sort(); + const localAvailableCommands = + await this._fxai.commands.availableCommands(); + const localAvailableCommandsKeys = Object.keys( + localAvailableCommands + ).sort(); + + if ( + !lazy.CommonUtils.arrayEqual( + localAvailableCommandsKeys, + remoteAvailableCommandsKeys + ) + ) { + return true; + } + + for (const key of localAvailableCommandsKeys) { + if (remoteAvailableCommands[key] !== localAvailableCommands[key]) { + return true; + } + } + return false; + } + + async _updateDeviceRegistrationIfNecessary(currentState) { + let data = await currentState.getUserAccountData([ + "sessionToken", + "device", + ]); + if (!data) { + // Can't register a device without a signed-in user. + return null; + } + const { device } = data; + if (await this._checkDeviceUpdateNeeded(device)) { + return this._registerOrUpdateDevice(currentState, data); + } + // Return the device ID we already had. + return device.id; + } + + // If you change what we send to the FxA servers during device registration, + // you'll have to bump the DEVICE_REGISTRATION_VERSION number to force older + // devices to re-register when Firefox updates. + async _registerOrUpdateDevice(currentState, signedInUser) { + // This method has the side-effect of setting some account-related prefs + // (e.g. for caching the device name) so it's important we don't execute it + // if the signed-in state has changed. + if (!currentState.isCurrent) { + throw new Error( + "_registerOrUpdateDevice called after a different user has signed in" + ); + } + + const { sessionToken, device: currentDevice } = signedInUser; + if (!sessionToken) { + throw new Error("_registerOrUpdateDevice called without a session token"); + } + + try { + const subscription = + await this._fxai.fxaPushService.registerPushEndpoint(); + const deviceName = this.getLocalName(); + let deviceOptions = {}; + + // if we were able to obtain a subscription + if (subscription && subscription.endpoint) { + deviceOptions.pushCallback = subscription.endpoint; + let publicKey = subscription.getKey("p256dh"); + let authKey = subscription.getKey("auth"); + if (publicKey && authKey) { + deviceOptions.pushPublicKey = urlsafeBase64Encode(publicKey); + deviceOptions.pushAuthKey = urlsafeBase64Encode(authKey); + } + } + deviceOptions.availableCommands = + await this._fxai.commands.availableCommands(); + const availableCommandsKeys = Object.keys( + deviceOptions.availableCommands + ).sort(); + log.info("registering with available commands", availableCommandsKeys); + + let device; + let is_existing = currentDevice && currentDevice.id; + if (is_existing) { + log.debug("updating existing device details"); + device = await this._fxai.fxAccountsClient.updateDevice( + sessionToken, + currentDevice.id, + deviceName, + deviceOptions + ); + } else { + log.debug("registering new device details"); + device = await this._fxai.fxAccountsClient.registerDevice( + sessionToken, + deviceName, + this.getLocalType(), + deviceOptions + ); + } + + // Get the freshest device props before updating them. + let { device: deviceProps } = await currentState.getUserAccountData([ + "device", + ]); + await currentState.updateUserAccountData({ + device: { + ...deviceProps, // Copy the other properties (e.g. handledCommands). + id: device.id, + registrationVersion: this.DEVICE_REGISTRATION_VERSION, + registeredCommandsKeys: availableCommandsKeys, + }, + }); + // Must send the notification after we've written the storage. + if (!is_existing) { + Services.obs.notifyObservers(null, ON_NEW_DEVICE_ID); + } + return device.id; + } catch (error) { + return this._handleDeviceError(currentState, error, sessionToken); + } + } + + async _handleDeviceError(currentState, error, sessionToken) { + try { + if (error.code === 400) { + if (error.errno === ERRNO_UNKNOWN_DEVICE) { + return this._recoverFromUnknownDevice(currentState); + } + + if (error.errno === ERRNO_DEVICE_SESSION_CONFLICT) { + return this._recoverFromDeviceSessionConflict( + currentState, + error, + sessionToken + ); + } + } + + // `_handleTokenError` always throws, this syntax keeps the linter happy. + // Note that the re-thrown error is immediately caught, logged and ignored + // by the containing scope here, which is why we have to `_handleTokenError` + // ourselves rather than letting it bubble up for handling by the caller. + throw await this._fxai._handleTokenError(error); + } catch (error) { + await this._logErrorAndResetDeviceRegistrationVersion( + currentState, + error + ); + return null; + } + } + + async _recoverFromUnknownDevice(currentState) { + // FxA did not recognise the device id. Handle it by clearing the device + // id on the account data. At next sync or next sign-in, registration is + // retried and should succeed. + log.warn("unknown device id, clearing the local device data"); + try { + await currentState.updateUserAccountData({ + device: null, + encryptedSendTabKeys: null, + }); + } catch (error) { + await this._logErrorAndResetDeviceRegistrationVersion( + currentState, + error + ); + } + return null; + } + + async _recoverFromDeviceSessionConflict(currentState, error, sessionToken) { + // FxA has already associated this session with a different device id. + // Perhaps we were beaten in a race to register. Handle the conflict: + // 1. Fetch the list of devices for the current user from FxA. + // 2. Look for ourselves in the list. + // 3. If we find a match, set the correct device id and device registration + // version on the account data and return the correct device id. At next + // sync or next sign-in, registration is retried and should succeed. + // 4. If we don't find a match, log the original error. + log.warn( + "device session conflict, attempting to ascertain the correct device id" + ); + try { + const devices = await this._fxai.fxAccountsClient.getDeviceList( + sessionToken + ); + const matchingDevices = devices.filter(device => device.isCurrentDevice); + const length = matchingDevices.length; + if (length === 1) { + const deviceId = matchingDevices[0].id; + await currentState.updateUserAccountData({ + device: { + id: deviceId, + registrationVersion: null, + }, + encryptedSendTabKeys: null, + }); + return deviceId; + } + if (length > 1) { + log.error( + "insane server state, " + length + " devices for this session" + ); + } + await this._logErrorAndResetDeviceRegistrationVersion( + currentState, + error + ); + } catch (secondError) { + log.error("failed to recover from device-session conflict", secondError); + await this._logErrorAndResetDeviceRegistrationVersion( + currentState, + error + ); + } + return null; + } + + async _logErrorAndResetDeviceRegistrationVersion(currentState, error) { + // Device registration should never cause other operations to fail. + // If we've reached this point, just log the error and reset the device + // on the account data. At next sync or next sign-in, + // registration will be retried. + log.error("device registration failed", error); + try { + await currentState.updateUserAccountData({ + device: null, + encryptedSendTabKeys: null, + }); + } catch (secondError) { + log.error( + "failed to reset the device registration version, device registration won't be retried", + secondError + ); + } + } + + // Kick off a background refresh when a device is connected or disconnected. + observe(subject, topic, data) { + switch (topic) { + case ON_DEVICE_CONNECTED_NOTIFICATION: + this.refreshDeviceList({ ignoreCached: true }).catch(error => { + log.warn( + "failed to refresh devices after connecting a new device", + error + ); + }); + break; + case ON_DEVICE_DISCONNECTED_NOTIFICATION: + let json = JSON.parse(data); + if (!json.isLocalDevice) { + // If we're the device being disconnected, don't bother fetching a new + // list, since our session token is now invalid. + this.refreshDeviceList({ ignoreCached: true }).catch(error => { + log.warn( + "failed to refresh devices after disconnecting a device", + error + ); + }); + } + break; + case ONVERIFIED_NOTIFICATION: + this.updateDeviceRegistrationIfNecessary().catch(error => { + log.warn( + "updateDeviceRegistrationIfNecessary failed after verification", + error + ); + }); + break; + } + } +} + +FxAccountsDevice.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", +]); + +function urlsafeBase64Encode(buffer) { + return ChromeUtils.base64URLEncode(new Uint8Array(buffer), { pad: false }); +} diff --git a/services/fxaccounts/FxAccountsKeys.sys.mjs b/services/fxaccounts/FxAccountsKeys.sys.mjs new file mode 100644 index 0000000000..ad19df31be --- /dev/null +++ b/services/fxaccounts/FxAccountsKeys.sys.mjs @@ -0,0 +1,649 @@ +/* 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 { CommonUtils } from "resource://services-common/utils.sys.mjs"; + +import { CryptoUtils } from "resource://services-crypto/utils.sys.mjs"; + +import { + SCOPE_OLD_SYNC, + DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY, + FX_OAUTH_CLIENT_ID, + log, + logPII, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +// The following top-level fields have since been deprecated and exist here purely +// to be removed from the account state when seen. After a reasonable period of time +// has passed, where users have been migrated away from those keys they should be safe to be removed +const DEPRECATED_DERIVED_KEYS_NAMES = [ + "kSync", + "kXCS", + "kExtSync", + "kExtKbHash", + "ecosystemUserId", + "ecosystemAnonId", +]; + +// This scope and its associated key material were used by the old Kinto webextension +// storage backend, but has since been decommissioned. It's here entirely so that we +// remove the corresponding key from storage if present. We should be safe to remove it +// after some sensible period of time has elapsed to allow most clients to update. +const DEPRECATED_SCOPE_WEBEXT_SYNC = "sync:addon_storage"; + +// These are the scopes that correspond to new storage for the `LEGACY_DERIVED_KEYS_NAMES`. +// We will, if necessary, migrate storage for those keys so that it's associated with +// these scopes. +const LEGACY_DERIVED_KEY_SCOPES = [SCOPE_OLD_SYNC]; + +// These are scopes that we used to store, but are no longer using, +// and hence should be deleted from storage if present. +const DEPRECATED_KEY_SCOPES = [ + DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY, + DEPRECATED_SCOPE_WEBEXT_SYNC, +]; + +/** + * Utilities for working with key material linked to the user's account. + * + * Each Firefox Account has 32 bytes of root key material called `kB` which is + * linked to the user's password, and which is used to derive purpose-specific + * subkeys for things like encrypting the user's sync data. This class provides + * the interface for working with such key material. + * + * Most recent FxA clients obtain appropriate key material directly as part of + * their sign-in flow, using a special extension of the OAuth2.0 protocol to + * securely deliver the derived keys without revealing `kB`. Keys obtained in + * in this way are called "scoped keys" since each corresponds to a particular + * OAuth scope, and this class provides a `getKeyForScope` method that is the + * preferred method for consumers to work with such keys. + * + * However, since the FxA integration in Firefox Desktop pre-dates the use of + * OAuth2.0, we also have a lot of code for fetching keys via an older flow. + * This flow uses a special `keyFetchToken` to obtain `kB` and then derive various + * sub-keys from it. Consumers should consider this an internal implementation + * detail of the `FxAccountsKeys` class and should prefer `getKeyForScope` where + * possible. We intend to remove support for Firefox ever directly handling `kB` + * at some point in the future. + */ +export class FxAccountsKeys { + constructor(fxAccountsInternal) { + this._fxai = fxAccountsInternal; + } + + /** + * Checks if we currently have the key for a given scope, or if we have enough to + * be able to successfully fetch and unwrap it for the signed-in-user. + * + * Unlike `getKeyForScope`, this will not hit the network to fetch wrapped keys if + * they aren't available locally. + */ + canGetKeyForScope(scope) { + return this._fxai.withCurrentAccountState(async currentState => { + let userData = await currentState.getUserAccountData(); + if (!userData) { + throw new Error("Can't possibly get keys; User is not signed in"); + } + if (!userData.verified) { + log.info("Can't get keys; user is not verified"); + return false; + } + + if (userData.scopedKeys && userData.scopedKeys.hasOwnProperty(scope)) { + return true; + } + + // If we have a `keyFetchToken` we can fetch `kB`. + if (userData.keyFetchToken) { + return true; + } + + log.info("Can't get keys; no key material or tokens available"); + return false; + }); + } + + /** + * Get the key for a specified OAuth scope. + * + * @param {String} scope The OAuth scope whose key should be returned + * + * @return Promise + * If no key is available the promise resolves to `null`. + * If a key is available for the given scope, th promise resolves to a JWK with fields: + * { + * scope: The requested scope + * kid: Key identifier + * k: Derived key material + * kty: Always "oct" for scoped keys + * } + * + */ + async getKeyForScope(scope) { + const { scopedKeys } = await this._loadOrFetchKeys(); + if (!scopedKeys.hasOwnProperty(scope)) { + throw new Error(`Key not available for scope "${scope}"`); + } + return { + scope, + ...scopedKeys[scope], + }; + } + + /** + * Format a JWK kid as hex rather than base64. + * + * This is a backwards-compatibility helper for code that needs a raw key fingerprint + * for use as a key identifier, rather than the timestamp+fingerprint format used by + * FxA scoped keys. + * + * @param {Object} jwk The JWK from which to extract the `kid` field as hex. + */ + kidAsHex(jwk) { + // The kid format is "{timestamp}-{b64url(fingerprint)}", but we have to be careful + // because the fingerprint component may contain "-" as well, and we want to ensure + // the timestamp component was non-empty. + const idx = jwk.kid.indexOf("-") + 1; + if (idx <= 1) { + throw new Error(`Invalid kid: ${jwk.kid}`); + } + return CommonUtils.base64urlToHex(jwk.kid.slice(idx)); + } + + /** + * Fetch encryption keys for the signed-in-user from the FxA API server. + * + * Not for user consumption. Exists to cause the keys to be fetched. + * + * Returns user data so that it can be chained with other methods. + * + * @return Promise + * The promise resolves to the credentials object of the signed-in user: + * { + * email: The user's email address + * uid: The user's unique id + * sessionToken: Session for the FxA server + * scopedKeys: Object mapping OAuth scopes to corresponding derived keys + * verified: email verification status + * } + * @throws If there is no user signed in. + */ + async _loadOrFetchKeys() { + return this._fxai.withCurrentAccountState(async currentState => { + try { + let userData = await currentState.getUserAccountData(); + if (!userData) { + throw new Error("Can't get keys; User is not signed in"); + } + // If we have all the keys in latest storage location, we're good. + if (userData.scopedKeys) { + if ( + LEGACY_DERIVED_KEY_SCOPES.every(scope => + userData.scopedKeys.hasOwnProperty(scope) + ) && + !DEPRECATED_KEY_SCOPES.some(scope => + userData.scopedKeys.hasOwnProperty(scope) + ) && + !DEPRECATED_DERIVED_KEYS_NAMES.some(keyName => + userData.hasOwnProperty(keyName) + ) + ) { + return userData; + } + } + // If not, we've got work to do, and we debounce to avoid duplicating it. + if (!currentState.whenKeysReadyDeferred) { + currentState.whenKeysReadyDeferred = Promise.withResolvers(); + // N.B. we deliberately don't `await` here, and instead use the promise + // to resolve `whenKeysReadyDeferred` (which we then `await` below). + this._migrateOrFetchKeys(currentState, userData).then( + dataWithKeys => { + currentState.whenKeysReadyDeferred.resolve(dataWithKeys); + currentState.whenKeysReadyDeferred = null; + }, + err => { + currentState.whenKeysReadyDeferred.reject(err); + currentState.whenKeysReadyDeferred = null; + } + ); + } + return await currentState.whenKeysReadyDeferred.promise; + } catch (err) { + return this._fxai._handleTokenError(err); + } + }); + } + + /** + * Set externally derived scoped keys in internal storage + * @param { Object } scopedKeys: The scoped keys object derived by the oauth flow + * + * @return { Promise }: A promise that resolves if the keys were successfully stored, + * or rejects if we failed to persist the keys, or if the user is not signed in already + */ + async setScopedKeys(scopedKeys) { + return this._fxai.withCurrentAccountState(async currentState => { + const userData = await currentState.getUserAccountData(); + if (!userData) { + throw new Error("Cannot persist keys, no user signed in"); + } + await currentState.updateUserAccountData({ + scopedKeys, + }); + }); + } + + /** + * Key storage migration or fetching logic. + * + * This method contains the doing-expensive-operations part of the logic of + * _loadOrFetchKeys(), factored out into a separate method so we can debounce it. + * + */ + async _migrateOrFetchKeys(currentState, userData) { + // If the required scopes are present in `scopedKeys`, then we know that we've + // previously applied all earlier migrations + // so we are safe to delete deprecated fields that older migrations + // might have depended on. + if ( + userData.scopedKeys && + LEGACY_DERIVED_KEY_SCOPES.every(scope => + userData.scopedKeys.hasOwnProperty(scope) + ) + ) { + return this._removeDeprecatedKeys(currentState, userData); + } + + // Otherwise, we need to fetch from the network and unwrap. + if (!userData.sessionToken) { + throw new Error("No sessionToken"); + } + if (!userData.keyFetchToken) { + throw new Error("No keyFetchToken"); + } + return this._fetchAndUnwrapAndDeriveKeys( + currentState, + userData.sessionToken, + userData.keyFetchToken + ); + } + + /** + * Removes deprecated keys from storage and returns an + * updated user data object + */ + async _removeDeprecatedKeys(currentState, userData) { + // Bug 1838708: Delete any deprecated high level keys from storage + const keysToRemove = DEPRECATED_DERIVED_KEYS_NAMES.filter(keyName => + userData.hasOwnProperty(keyName) + ); + if (keysToRemove.length) { + const removedKeys = {}; + for (const keyName of keysToRemove) { + removedKeys[keyName] = null; + } + await currentState.updateUserAccountData({ + ...removedKeys, + }); + userData = await currentState.getUserAccountData(); + } + // Bug 1697596 - delete any deprecated scoped keys from storage. + const scopesToRemove = DEPRECATED_KEY_SCOPES.filter(scope => + userData.scopedKeys.hasOwnProperty(scope) + ); + if (scopesToRemove.length) { + const updatedScopedKeys = { + ...userData.scopedKeys, + }; + for (const scope of scopesToRemove) { + delete updatedScopedKeys[scope]; + } + await currentState.updateUserAccountData({ + scopedKeys: updatedScopedKeys, + }); + userData = await currentState.getUserAccountData(); + } + return userData; + } + + /** + * Fetch keys from the server, unwrap them, and derive required sub-keys. + * + * Once the user's email is verified, we can resquest the root key `kB` from the + * FxA server, unwrap it using the client-side secret `unwrapBKey`, and then + * derive all the sub-keys required for operation of the browser. + */ + async _fetchAndUnwrapAndDeriveKeys( + currentState, + sessionToken, + keyFetchToken + ) { + if (logPII()) { + log.debug( + `fetchAndUnwrapKeys: sessionToken: ${sessionToken}, keyFetchToken: ${keyFetchToken}` + ); + } + + // Sign out if we don't have the necessary tokens. + if (!sessionToken || !keyFetchToken) { + // this seems really bad and we should remove this - bug 1572313. + log.warn("improper _fetchAndUnwrapKeys() call: token missing"); + await this._fxai.signOut(); + return null; + } + + // Deriving OAuth scoped keys requires additional metadata from the server. + // We fetch this first, before fetching the actual key material, because the + // keyFetchToken is single-use and we don't want to do a potentially-fallible + // operation after consuming it. + const scopedKeysMetadata = await this._fetchScopedKeysMetadata( + sessionToken + ); + + // Fetch the wrapped keys. + // It would be nice to be able to fetch this in a single operation with fetching + // the metadata above, but that requires server-side changes in FxA. + let { wrapKB } = await this._fetchKeys(keyFetchToken); + + let data = await currentState.getUserAccountData(); + + // Sanity check that the user hasn't changed out from under us (which should + // be impossible given this is called within _withCurrentAccountState, but...) + if (data.keyFetchToken !== keyFetchToken) { + throw new Error("Signed in user changed while fetching keys!"); + } + + let kBbytes = CryptoUtils.xor( + CommonUtils.hexToBytes(data.unwrapBKey), + wrapKB + ); + + if (logPII()) { + log.debug("kBbytes: " + kBbytes); + } + + let updateData = { + ...(await this._deriveKeys(data.uid, kBbytes, scopedKeysMetadata)), + keyFetchToken: null, // null values cause the item to be removed. + unwrapBKey: null, + }; + + if (logPII()) { + log.debug(`Keys Obtained: ${updateData.scopedKeys}`); + } else { + log.debug( + "Keys Obtained: " + Object.keys(updateData.scopedKeys).join(", ") + ); + } + + // Just double-check that scoped keys are there now + if (!updateData.scopedKeys) { + throw new Error(`user data missing: scopedKeys`); + } + + await currentState.updateUserAccountData(updateData); + return currentState.getUserAccountData(); + } + + /** + * Fetch the wrapped root key `wrapKB` from the FxA server. + * + * This consumes the single-use `keyFetchToken`. + */ + _fetchKeys(keyFetchToken) { + let client = this._fxai.fxAccountsClient; + log.debug( + `Fetching keys with token ${!!keyFetchToken} from ${client.host}` + ); + if (logPII()) { + log.debug("fetchKeys - the token is " + keyFetchToken); + } + return client.accountKeys(keyFetchToken); + } + + /** + * Fetch additional metadata required for deriving scoped keys. + * + * This includes timestamps and a server-provided secret to mix in to + * the derived value in order to support key rotation. + */ + async _fetchScopedKeysMetadata(sessionToken) { + // Hard-coded list of scopes that we know about. + // This list will probably grow in future. + const scopes = [SCOPE_OLD_SYNC].join(" "); + const scopedKeysMetadata = + await this._fxai.fxAccountsClient.getScopedKeyData( + sessionToken, + FX_OAUTH_CLIENT_ID, + scopes + ); + // The server may decline us permission for some of those scopes, although it really shouldn't. + // We can live without them...except for the OLDSYNC scope, whose absence would be catastrophic. + if (!scopedKeysMetadata.hasOwnProperty(SCOPE_OLD_SYNC)) { + log.warn( + "The FxA server did not grant Firefox the `oldsync` scope; this is most unexpected!" + + ` scopes were: ${Object.keys(scopedKeysMetadata)}` + ); + throw new Error( + "The FxA server did not grant Firefox the `oldsync` scope" + ); + } + return scopedKeysMetadata; + } + + /** + * Derive purpose-specific keys from the root FxA key `kB`. + * + * Everything that uses an encryption key from FxA uses a purpose-specific derived + * key. For new uses this is derived in a structured way based on OAuth scopes, + * while for legacy uses (mainly Firefox Sync) it is derived in a more ad-hoc fashion. + * This method does all the derivations for the uses that we know about. + * + */ + async _deriveKeys(uid, kBbytes, scopedKeysMetadata) { + const scopedKeys = await this._deriveScopedKeys( + uid, + kBbytes, + scopedKeysMetadata + ); + return { + scopedKeys, + }; + } + + /** + * Derive various scoped keys from the root FxA key `kB`. + * + * The `scopedKeysMetadata` object is additional information fetched from the server that + * that gets mixed in to the key derivation, with each member of the object corresponding + * to an OAuth scope that keys its own scoped key. + * + * As a special case for backwards-compatibility, sync-related scopes get special + * treatment to use a legacy derivation algorithm. + * + */ + async _deriveScopedKeys(uid, kBbytes, scopedKeysMetadata) { + const scopedKeys = {}; + for (const scope in scopedKeysMetadata) { + if (LEGACY_DERIVED_KEY_SCOPES.includes(scope)) { + scopedKeys[scope] = await this._deriveLegacyScopedKey( + uid, + kBbytes, + scope, + scopedKeysMetadata[scope] + ); + } else { + scopedKeys[scope] = await this._deriveScopedKey( + uid, + kBbytes, + scope, + scopedKeysMetadata[scope] + ); + } + } + return scopedKeys; + } + + /** + * Derive a scoped key for an individual OAuth scope. + * + * The derivation here uses HKDF to combine: + * - the root key material kB + * - a unique identifier for this scoped key + * - a server-provided secret that allows for key rotation + * - the account uid as an additional salt + * + * It produces 32 bytes of (secret) key material along with a (potentially public) + * key identifier, formatted as a JWK. + * + * The full details are in the technical docs at + * https://docs.google.com/document/d/1IvQJFEBFz0PnL4uVlIvt8fBS_IPwSK-avK0BRIHucxQ/ + */ + async _deriveScopedKey(uid, kBbytes, scope, scopedKeyMetadata) { + kBbytes = CommonUtils.byteStringToArrayBuffer(kBbytes); + + const FINGERPRINT_LENGTH = 16; + const KEY_LENGTH = 32; + const VALID_UID = /^[0-9a-f]{32}$/i; + const VALID_ROTATION_SECRET = /^[0-9a-f]{64}$/i; + + // Engage paranoia mode for input data. + if (!VALID_UID.test(uid)) { + throw new Error("uid must be a 32-character hex string"); + } + if (kBbytes.length != 32) { + throw new Error("kBbytes must be exactly 32 bytes"); + } + if ( + typeof scopedKeyMetadata.identifier !== "string" || + scopedKeyMetadata.identifier.length < 10 + ) { + throw new Error("identifier must be a string of length >= 10"); + } + if (typeof scopedKeyMetadata.keyRotationTimestamp !== "number") { + throw new Error("keyRotationTimestamp must be a number"); + } + if (!VALID_ROTATION_SECRET.test(scopedKeyMetadata.keyRotationSecret)) { + throw new Error("keyRotationSecret must be a 64-character hex string"); + } + + // The server returns milliseconds, we want seconds as a string. + const keyRotationTimestamp = + "" + Math.round(scopedKeyMetadata.keyRotationTimestamp / 1000); + if (keyRotationTimestamp.length < 10) { + throw new Error("keyRotationTimestamp must round to a 10-digit number"); + } + + const keyRotationSecret = CommonUtils.hexToArrayBuffer( + scopedKeyMetadata.keyRotationSecret + ); + const salt = CommonUtils.hexToArrayBuffer(uid); + const context = new TextEncoder().encode( + "identity.mozilla.com/picl/v1/scoped_key\n" + scopedKeyMetadata.identifier + ); + + const inputKey = new Uint8Array(64); + inputKey.set(kBbytes, 0); + inputKey.set(keyRotationSecret, 32); + + const derivedKeyMaterial = await CryptoUtils.hkdf( + inputKey, + salt, + context, + FINGERPRINT_LENGTH + KEY_LENGTH + ); + const fingerprint = derivedKeyMaterial.slice(0, FINGERPRINT_LENGTH); + const key = derivedKeyMaterial.slice( + FINGERPRINT_LENGTH, + FINGERPRINT_LENGTH + KEY_LENGTH + ); + + return { + kid: + keyRotationTimestamp + + "-" + + ChromeUtils.base64URLEncode(fingerprint, { + pad: false, + }), + k: ChromeUtils.base64URLEncode(key, { + pad: false, + }), + kty: "oct", + }; + } + + /** + * Derive the scoped key for the one of our legacy sync-related scopes. + * + * These uses a different key-derivation algoritm that incorporates less server-provided + * data, for backwards-compatibility reasons. + * + */ + async _deriveLegacyScopedKey(uid, kBbytes, scope, scopedKeyMetadata) { + let kid, key; + if (scope == SCOPE_OLD_SYNC) { + kid = await this._deriveXClientState(kBbytes); + key = await this._deriveSyncKey(kBbytes); + } else { + throw new Error(`Unexpected legacy key-bearing scope: ${scope}`); + } + kid = CommonUtils.byteStringToArrayBuffer(kid); + key = CommonUtils.byteStringToArrayBuffer(key); + return this._formatLegacyScopedKey(kid, key, scope, scopedKeyMetadata); + } + + /** + * Format key material for a legacy scyne-related scope as a JWK. + * + * @param {ArrayBuffer} kid bytes of the key hash to use in the key identifier + * @param {ArrayBuffer} key bytes of the derived sync key + * @param {String} scope the scope with which this key is associated + * @param {Number} keyRotationTimestamp server-provided timestamp of last key rotation + * @returns {Object} key material formatted as a JWK object + */ + _formatLegacyScopedKey(kid, key, scope, { keyRotationTimestamp }) { + kid = ChromeUtils.base64URLEncode(kid, { + pad: false, + }); + key = ChromeUtils.base64URLEncode(key, { + pad: false, + }); + return { + kid: `${keyRotationTimestamp}-${kid}`, + k: key, + kty: "oct", + }; + } + + /** + * Derive the Sync Key given the byte string kB. + * + * @returns Promise + */ + async _deriveSyncKey(kBbytes) { + return CryptoUtils.hkdfLegacy( + kBbytes, + undefined, + "identity.mozilla.com/picl/v1/oldsync", + 2 * 32 + ); + } + + /** + * Derive the X-Client-State header given the byte string kB. + * + * @returns Promise + */ + async _deriveXClientState(kBbytes) { + return this._sha256(kBbytes).slice(0, 16); + } + + _sha256(bytes) { + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(hasher.SHA256); + return CryptoUtils.digestBytes(bytes, hasher); + } +} diff --git a/services/fxaccounts/FxAccountsOAuth.sys.mjs b/services/fxaccounts/FxAccountsOAuth.sys.mjs new file mode 100644 index 0000000000..1935decff2 --- /dev/null +++ b/services/fxaccounts/FxAccountsOAuth.sys.mjs @@ -0,0 +1,224 @@ +/* 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, { + jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs", +}); + +import { + FX_OAUTH_CLIENT_ID, + SCOPE_PROFILE, + SCOPE_PROFILE_WRITE, + SCOPE_OLD_SYNC, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +const VALID_SCOPES = [SCOPE_PROFILE, SCOPE_PROFILE_WRITE, SCOPE_OLD_SYNC]; + +export const ERROR_INVALID_SCOPES = "INVALID_SCOPES"; +export const ERROR_INVALID_STATE = "INVALID_STATE"; +export const ERROR_SYNC_SCOPE_NOT_GRANTED = "ERROR_SYNC_SCOPE_NOT_GRANTED"; +export const ERROR_NO_KEYS_JWE = "ERROR_NO_KEYS_JWE"; +export const ERROR_OAUTH_FLOW_ABANDONED = "ERROR_OAUTH_FLOW_ABANDONED"; + +/** + * Handles all logic and state related to initializing, and completing OAuth flows + * with FxA + * It's possible to start multiple OAuth flow, but only one can be completed, and once one flow is completed + * all the other in-flight flows will be concluded, and attempting to complete those flows will result in errors. + */ +export class FxAccountsOAuth { + #flow; + #fxaClient; + /** + * Creates a new FxAccountsOAuth + * + * @param { Object } fxaClient: The fxa client used to send http request to the oauth server + */ + constructor(fxaClient) { + this.#flow = {}; + this.#fxaClient = fxaClient; + } + + /** + * Stores a flow in-memory + * @param { string } state: A base-64 URL-safe string represnting a random value created at the start of the flow + * @param { Object } value: The data needed to complete a flow, once the oauth code is available. + * in practice, `value` is: + * - `verifier`: A base=64 URL-safe string representing the PKCE code verifier + * - `key`: The private key need to decrypt the JWE we recieve from the auth server + * - `requestedScopes`: The scopes the caller requested, meant to be compared against the scopes the server authorized + */ + addFlow(state, value) { + this.#flow[state] = value; + } + + /** + * Clears all started flows + */ + clearAllFlows() { + this.#flow = {}; + } + + /* + * Gets a stored flow + * @param { string } state: The base-64 URL-safe state string that was created at the start of the flow + * @returns { Object }: The values initially stored when startign th eoauth flow + * in practice, the return value is: + * - `verifier`: A base=64 URL-safe string representing the PKCE code verifier + * - `key`: The private key need to decrypt the JWE we recieve from the auth server + * - ``requestedScopes`: The scopes the caller requested, meant to be compared against the scopes the server authorized + */ + getFlow(state) { + return this.#flow[state]; + } + + /* Returns the number of flows, used by tests + * + */ + numOfFlows() { + return Object.keys(this.#flow).length; + } + + /** + * Begins an OAuth flow, to be completed with a an OAuth code and state. + * + * This function stores needed information to complete the flow. You must call `completeOAuthFlow` + * on the same instance of `FxAccountsOAuth`, otherwise the completing of the oauth flow will fail. + * + * @param { string[] } scopes: The OAuth scopes the client should request from FxA + * + * @returns { Object }: Returns an object representing the query parameters that should be + * added to the FxA authorization URL to initialize an oAuth flow. + * In practice, the query parameters are: + * - `client_id`: The OAuth client ID for Firefox Desktop + * - `scope`: The scopes given by the caller, space seperated + * - `action`: This will always be `email` + * - `response_type`: This will always be `code` + * - `access_type`: This will always be `offline` + * - `state`: A URL-safe base-64 string randomly generated + * - `code_challenge`: A URL-safe base-64 string representing the PKCE challenge + * - `code_challenge_method`: This will always be `S256` + * For more informatio about PKCE, read https://datatracker.ietf.org/doc/html/rfc7636 + * - `keys_jwk`: A URL-safe base-64 representing a JWK to be used as a public key by the server + * to generate a JWE + */ + async beginOAuthFlow(scopes) { + if ( + !Array.isArray(scopes) || + scopes.some(scope => !VALID_SCOPES.includes(scope)) + ) { + throw new Error(ERROR_INVALID_SCOPES); + } + const queryParams = { + client_id: FX_OAUTH_CLIENT_ID, + action: "email", + response_type: "code", + access_type: "offline", + scope: scopes.join(" "), + }; + + // Generate a random, 16 byte value to represent a state that we verify + // once we complete the oauth flow, to ensure that we only conclude + // an oauth flow that we started + const state = new Uint8Array(16); + crypto.getRandomValues(state); + const stateB64 = ChromeUtils.base64URLEncode(state, { pad: false }); + queryParams.state = stateB64; + + // Generate a 43 byte code verifier for PKCE, in accordance with + // https://datatracker.ietf.org/doc/html/rfc7636#section-7.1 which recommends a + // 43-octet URL safe string + const codeVerifier = new Uint8Array(43); + crypto.getRandomValues(codeVerifier); + const codeVerifierB64 = ChromeUtils.base64URLEncode(codeVerifier, { + pad: false, + }); + const challenge = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(codeVerifierB64) + ); + const challengeB64 = ChromeUtils.base64URLEncode(challenge, { pad: false }); + queryParams.code_challenge = challengeB64; + queryParams.code_challenge_method = "S256"; + + // Generate a public, private key pair to be used during the oauth flow + // to encrypt scoped-keys as they roundtrip through the auth server + const ECDH_KEY = { name: "ECDH", namedCurve: "P-256" }; + const key = await crypto.subtle.generateKey(ECDH_KEY, true, ["deriveKey"]); + const publicKey = await crypto.subtle.exportKey("jwk", key.publicKey); + const privateKey = key.privateKey; + + // We encode the public key as URL-safe base64 to be included in the query parameters + const encodedPublicKey = ChromeUtils.base64URLEncode( + new TextEncoder().encode(JSON.stringify(publicKey)), + { pad: false } + ); + queryParams.keys_jwk = encodedPublicKey; + + // We store the state in-memory, to verify once the oauth flow is completed + this.addFlow(stateB64, { + key: privateKey, + verifier: codeVerifierB64, + requestedScopes: scopes.join(" "), + }); + return queryParams; + } + + /** Completes an OAuth flow and invalidates any other ongoing flows + * @param { string } sessionTokenHex: The session token encoded in hexadecimal + * @param { string } code: OAuth authorization code provided by running an OAuth flow + * @param { string } state: The state first provided by `beginOAuthFlow`, then roundtripped through the server + * + * @returns { Object }: Returns an object representing the result of completing the oauth flow. + * The object includes the following: + * - 'scopedKeys': The encryption keys provided by the server, already decrypted + * - 'refreshToken': The refresh token provided by the server + * - 'accessToken': The access token provided by the server + * */ + async completeOAuthFlow(sessionTokenHex, code, state) { + const flow = this.getFlow(state); + if (!flow) { + throw new Error(ERROR_INVALID_STATE); + } + const { key, verifier, requestedScopes } = flow; + const { keys_jwe, refresh_token, access_token, scope } = + await this.#fxaClient.oauthToken( + sessionTokenHex, + code, + verifier, + FX_OAUTH_CLIENT_ID + ); + if ( + requestedScopes.includes(SCOPE_OLD_SYNC) && + !scope.includes(SCOPE_OLD_SYNC) + ) { + throw new Error(ERROR_SYNC_SCOPE_NOT_GRANTED); + } + if (scope.includes(SCOPE_OLD_SYNC) && !keys_jwe) { + throw new Error(ERROR_NO_KEYS_JWE); + } + let scopedKeys; + if (keys_jwe) { + scopedKeys = JSON.parse( + new TextDecoder().decode(await lazy.jwcrypto.decryptJWE(keys_jwe, key)) + ); + } + + // We make sure no other flow snuck in, and completed before we did + if (!this.getFlow(state)) { + throw new Error(ERROR_OAUTH_FLOW_ABANDONED); + } + + // Clear all flows, so any in-flight or future flows trigger an error as the browser + // would have been signed in + this.clearAllFlows(); + return { + scopedKeys, + refreshToken: refresh_token, + accessToken: access_token, + }; + } +} diff --git a/services/fxaccounts/FxAccountsPairing.sys.mjs b/services/fxaccounts/FxAccountsPairing.sys.mjs new file mode 100644 index 0000000000..e68554f7ab --- /dev/null +++ b/services/fxaccounts/FxAccountsPairing.sys.mjs @@ -0,0 +1,511 @@ +// 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 { + log, + PREF_REMOTE_PAIRING_URI, + COMMAND_PAIR_SUPP_METADATA, + COMMAND_PAIR_AUTHORIZE, + COMMAND_PAIR_DECLINE, + COMMAND_PAIR_HEARTBEAT, + COMMAND_PAIR_COMPLETE, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +import { + getFxAccountsSingleton, + FxAccounts, +} from "resource://gre/modules/FxAccounts.sys.mjs"; + +const fxAccounts = getFxAccountsSingleton(); +import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +ChromeUtils.importESModule("resource://services-common/utils.sys.mjs"); +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + FxAccountsPairingChannel: + "resource://gre/modules/FxAccountsPairingChannel.sys.mjs", + + Weave: "resource://services-sync/main.sys.mjs", + jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs", +}); + +const PAIRING_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob:pair-auth-webchannel"; +// A pairing flow is not tied to a specific browser window, can also finish in +// various ways and subsequently might leak a Web Socket, so just in case we +// time out and free-up the resources after a specified amount of time. +const FLOW_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes. + +class PairingStateMachine { + constructor(emitter) { + this._emitter = emitter; + this._transition(SuppConnectionPending); + } + + get currentState() { + return this._currentState; + } + + _transition(StateCtor, ...args) { + const state = new StateCtor(this, ...args); + this._currentState = state; + } + + assertState(RequiredStates, messagePrefix = null) { + if (!(RequiredStates instanceof Array)) { + RequiredStates = [RequiredStates]; + } + if ( + !RequiredStates.some( + RequiredState => this._currentState instanceof RequiredState + ) + ) { + const msg = `${ + messagePrefix ? `${messagePrefix}. ` : "" + }Valid expected states: ${RequiredStates.map(({ name }) => name).join( + ", " + )}. Current state: ${this._currentState.label}.`; + throw new Error(msg); + } + } +} + +/** + * The pairing flow can be modeled by a finite state machine: + * We start by connecting to a WebSocket channel (SuppConnectionPending). + * Then the other party connects and requests some metadata from us (PendingConfirmations). + * A confirmation happens locally first (PendingRemoteConfirmation) + * or the oppposite (PendingLocalConfirmation). + * Any side can decline this confirmation (Aborted). + * Once both sides have confirmed, the pairing flow is finished (Completed). + * During this flow errors can happen and should be handled (Errored). + */ +class State { + constructor(stateMachine, ...args) { + this._transition = (...args) => stateMachine._transition(...args); + this._notify = (...args) => stateMachine._emitter.emit(...args); + this.init(...args); + } + + init() { + /* Does nothing by default but can be re-implemented. */ + } + + get label() { + return this.constructor.name; + } + + hasErrored(error) { + this._notify("view:Error", error); + this._transition(Errored, error); + } + + hasAborted() { + this._transition(Aborted); + } +} +class SuppConnectionPending extends State { + suppConnected(sender, oauthOptions) { + this._transition(PendingConfirmations, sender, oauthOptions); + } +} +class PendingConfirmationsState extends State { + localConfirmed() { + throw new Error("Subclasses must implement this method."); + } + remoteConfirmed() { + throw new Error("Subclasses must implement this method."); + } +} +class PendingConfirmations extends PendingConfirmationsState { + init(sender, oauthOptions) { + this.sender = sender; + this.oauthOptions = oauthOptions; + } + + localConfirmed() { + this._transition(PendingRemoteConfirmation); + } + + remoteConfirmed() { + this._transition(PendingLocalConfirmation, this.sender, this.oauthOptions); + } +} +class PendingLocalConfirmation extends PendingConfirmationsState { + init(sender, oauthOptions) { + this.sender = sender; + this.oauthOptions = oauthOptions; + } + + localConfirmed() { + this._transition(Completed); + } + + remoteConfirmed() { + throw new Error( + "Insane state! Remote has already been confirmed at this point." + ); + } +} +class PendingRemoteConfirmation extends PendingConfirmationsState { + localConfirmed() { + throw new Error( + "Insane state! Local has already been confirmed at this point." + ); + } + + remoteConfirmed() { + this._transition(Completed); + } +} +class Completed extends State {} +class Aborted extends State {} +class Errored extends State { + init(error) { + this.error = error; + } +} + +const flows = new Map(); + +export class FxAccountsPairingFlow { + static get(channelId) { + return flows.get(channelId); + } + + static finalizeAll() { + for (const flow of flows) { + flow.finalize(); + } + } + + static async start(options) { + const { emitter } = options; + const fxaConfig = options.fxaConfig || FxAccounts.config; + const fxa = options.fxAccounts || fxAccounts; + const weave = options.weave || lazy.Weave; + const flowTimeout = options.flowTimeout || FLOW_TIMEOUT_MS; + + const contentPairingURI = await fxaConfig.promisePairingURI(); + const wsUri = Services.urlFormatter.formatURLPref(PREF_REMOTE_PAIRING_URI); + const pairingChannel = + options.pairingChannel || + (await lazy.FxAccountsPairingChannel.create(wsUri)); + const { channelId, channelKey } = pairingChannel; + const channelKeyB64 = ChromeUtils.base64URLEncode(channelKey, { + pad: false, + }); + const pairingFlow = new FxAccountsPairingFlow({ + channelId, + pairingChannel, + emitter, + fxa, + fxaConfig, + flowTimeout, + weave, + }); + flows.set(channelId, pairingFlow); + + return `${contentPairingURI}#channel_id=${channelId}&channel_key=${channelKeyB64}`; + } + + constructor(options) { + this._channelId = options.channelId; + this._pairingChannel = options.pairingChannel; + this._emitter = options.emitter; + this._fxa = options.fxa; + this._fxai = options.fxai || this._fxa._internal; + this._fxaConfig = options.fxaConfig; + this._weave = options.weave; + this._stateMachine = new PairingStateMachine(this._emitter); + this._setupListeners(); + this._flowTimeoutId = setTimeout( + () => this._onFlowTimeout(), + options.flowTimeout + ); + } + + _onFlowTimeout() { + log.warn(`The pairing flow ${this._channelId} timed out.`); + this._onError(new Error("Timeout")); + this.finalize(); + } + + _closeChannel() { + if (!this._closed && !this._pairingChannel.closed) { + this._pairingChannel.close(); + this._closed = true; + } + } + + finalize() { + this._closeChannel(); + clearTimeout(this._flowTimeoutId); + // Free up resources and let the GC do its thing. + flows.delete(this._channelId); + } + + _setupListeners() { + this._pairingChannel.addEventListener( + "message", + ({ detail: { sender, data } }) => + this.onPairingChannelMessage(sender, data) + ); + this._pairingChannel.addEventListener("error", event => + this._onPairingChannelError(event.detail.error) + ); + this._emitter.on("view:Closed", () => this.onPrefViewClosed()); + } + + _onAbort() { + this._stateMachine.currentState.hasAborted(); + this.finalize(); + } + + _onError(error) { + this._stateMachine.currentState.hasErrored(error); + this._closeChannel(); + } + + _onPairingChannelError(error) { + log.error("Pairing channel error", error); + this._onError(error); + } + + // Any non-falsy returned value is sent back through WebChannel. + async onWebChannelMessage(command) { + const stateMachine = this._stateMachine; + const curState = stateMachine.currentState; + try { + switch (command) { + case COMMAND_PAIR_SUPP_METADATA: + stateMachine.assertState( + [PendingConfirmations, PendingLocalConfirmation], + `Wrong state for ${command}` + ); + const { + ua, + city, + region, + country, + remote: ipAddress, + } = curState.sender; + return { ua, city, region, country, ipAddress }; + case COMMAND_PAIR_AUTHORIZE: + stateMachine.assertState( + [PendingConfirmations, PendingLocalConfirmation], + `Wrong state for ${command}` + ); + const { + client_id, + state, + scope, + code_challenge, + code_challenge_method, + keys_jwk, + } = curState.oauthOptions; + const authorizeParams = { + client_id, + access_type: "offline", + state, + scope, + code_challenge, + code_challenge_method, + keys_jwk, + }; + const codeAndState = await this._authorizeOAuthCode(authorizeParams); + if (codeAndState.state != state) { + throw new Error(`OAuth state mismatch`); + } + await this._pairingChannel.send({ + message: "pair:auth:authorize", + data: { + ...codeAndState, + }, + }); + curState.localConfirmed(); + break; + case COMMAND_PAIR_DECLINE: + this._onAbort(); + break; + case COMMAND_PAIR_HEARTBEAT: + if (curState instanceof Errored || this._pairingChannel.closed) { + return { err: curState.error.message || "Pairing channel closed" }; + } + const suppAuthorized = !( + curState instanceof PendingConfirmations || + curState instanceof PendingRemoteConfirmation + ); + return { suppAuthorized }; + case COMMAND_PAIR_COMPLETE: + this.finalize(); + break; + default: + throw new Error(`Received unknown WebChannel command: ${command}`); + } + } catch (e) { + log.error(e); + curState.hasErrored(e); + } + return {}; + } + + async onPairingChannelMessage(sender, payload) { + const { message } = payload; + const stateMachine = this._stateMachine; + const curState = stateMachine.currentState; + try { + switch (message) { + case "pair:supp:request": + stateMachine.assertState( + SuppConnectionPending, + `Wrong state for ${message}` + ); + const oauthUri = await this._fxaConfig.promiseOAuthURI(); + const { uid, email, avatar, displayName } = + await this._fxa.getSignedInUser(); + const deviceName = this._weave.Service.clientsEngine.localName; + await this._pairingChannel.send({ + message: "pair:auth:metadata", + data: { + email, + avatar, + displayName, + deviceName, + }, + }); + const { + client_id, + state, + scope, + code_challenge, + code_challenge_method, + keys_jwk, + } = payload.data; + const url = new URL(oauthUri); + url.searchParams.append("client_id", client_id); + url.searchParams.append("scope", scope); + url.searchParams.append("email", email); + url.searchParams.append("uid", uid); + url.searchParams.append("channel_id", this._channelId); + url.searchParams.append("redirect_uri", PAIRING_REDIRECT_URI); + this._emitter.emit("view:SwitchToWebContent", url.href); + curState.suppConnected(sender, { + client_id, + state, + scope, + code_challenge, + code_challenge_method, + keys_jwk, + }); + break; + case "pair:supp:authorize": + stateMachine.assertState( + [PendingConfirmations, PendingRemoteConfirmation], + `Wrong state for ${message}` + ); + curState.remoteConfirmed(); + break; + default: + throw new Error( + `Received unknown Pairing Channel message: ${message}` + ); + } + } catch (e) { + log.error(e); + curState.hasErrored(e); + } + } + + onPrefViewClosed() { + const curState = this._stateMachine.currentState; + // We don't want to stop the pairing process in the later stages. + if ( + curState instanceof SuppConnectionPending || + curState instanceof Aborted || + curState instanceof Errored + ) { + this.finalize(); + } + } + + /** + * Grant an OAuth authorization code for the connecting client. + * + * @param {Object} options + * @param options.client_id + * @param options.state + * @param options.scope + * @param options.access_type + * @param options.code_challenge_method + * @param options.code_challenge + * @param [options.keys_jwe] + * @returns {Promise} Object containing "code" and "state" properties. + */ + _authorizeOAuthCode(options) { + return this._fxa._withVerifiedAccountState(async state => { + const { sessionToken } = await state.getUserAccountData(["sessionToken"]); + const params = { ...options }; + if (params.keys_jwk) { + const jwk = JSON.parse( + new TextDecoder().decode( + ChromeUtils.base64URLDecode(params.keys_jwk, { padding: "reject" }) + ) + ); + params.keys_jwe = await this._createKeysJWE( + sessionToken, + params.client_id, + params.scope, + jwk + ); + delete params.keys_jwk; + } + try { + return await this._fxai.fxAccountsClient.oauthAuthorize( + sessionToken, + params + ); + } catch (err) { + throw this._fxai._errorToErrorClass(err); + } + }); + } + + /** + * Create a JWE to deliver keys to another client via the OAuth scoped-keys flow. + * + * This method is used to transfer key material to another client, by providing + * an appropriately-encrypted value for the `keys_jwe` OAuth response parameter. + * Since we're transferring keys from one client to another, two things must be + * true: + * + * * This client must actually have the key. + * * The other client must be allowed to request that key. + * + * @param {String} sessionToken the sessionToken to use when fetching key metadata + * @param {String} clientId the client requesting access to our keys + * @param {String} scopes Space separated requested scopes being requested + * @param {Object} jwk Ephemeral JWK provided by the client for secure key transfer + */ + async _createKeysJWE(sessionToken, clientId, scopes, jwk) { + // This checks with the FxA server about what scopes the client is allowed. + // Note that we pass the requesting client_id here, not our own client_id. + const clientKeyData = await this._fxai.fxAccountsClient.getScopedKeyData( + sessionToken, + clientId, + scopes + ); + const scopedKeys = {}; + for (const scope of Object.keys(clientKeyData)) { + const key = await this._fxai.keys.getKeyForScope(scope); + if (!key) { + throw new Error(`Key not available for scope "${scope}"`); + } + scopedKeys[scope] = key; + } + return lazy.jwcrypto.generateJWE( + jwk, + new TextEncoder().encode(JSON.stringify(scopedKeys)) + ); + } +} diff --git a/services/fxaccounts/FxAccountsPairingChannel.sys.mjs b/services/fxaccounts/FxAccountsPairingChannel.sys.mjs new file mode 100644 index 0000000000..cb6d3fdb91 --- /dev/null +++ b/services/fxaccounts/FxAccountsPairingChannel.sys.mjs @@ -0,0 +1,3693 @@ +/*! + * + * 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/. + * + * The following bundle is from an external repository at github.com/mozilla/fxa-pairing-channel, + * it implements a shared library for two javascript environments to create an encrypted and authenticated + * communication channel by sharing a secret key and by relaying messages through a websocket server. + * + * It is used by the Firefox Accounts pairing flow, with one side of the channel being web + * content from https://accounts.firefox.com and the other side of the channel being chrome native code. + * + * This uses the event-target-shim node library published under the MIT license: + * https://github.com/mysticatea/event-target-shim/blob/master/LICENSE + * + * Bundle generated from https://github.com/mozilla/fxa-pairing-channel.git. Hash:c8ec3119920b4ffa833b, Chunkhash:378a5f51445e7aa7630e. + * + */ + +// This header provides a little bit of plumbing to use `FxAccountsPairingChannel` +// from Firefox browser code, hence the presence of these privileged browser APIs. +// If you're trying to use this from ordinary web content you're in for a bad time. + +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +// We cannot use WebSocket from chrome code without a window, +// see https://bugzilla.mozilla.org/show_bug.cgi?id=784686 +const browser = Services.appShell.createWindowlessBrowser(true); +const {WebSocket} = browser.document.ownerGlobal; + +export var FxAccountsPairingChannel = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "PairingChannel", function() { return /* binding */ src_PairingChannel; }); +__webpack_require__.d(__webpack_exports__, "base64urlToBytes", function() { return /* reexport */ base64urlToBytes; }); +__webpack_require__.d(__webpack_exports__, "bytesToBase64url", function() { return /* reexport */ bytesToBase64url; }); +__webpack_require__.d(__webpack_exports__, "bytesToHex", function() { return /* reexport */ bytesToHex; }); +__webpack_require__.d(__webpack_exports__, "bytesToUtf8", function() { return /* reexport */ bytesToUtf8; }); +__webpack_require__.d(__webpack_exports__, "hexToBytes", function() { return /* reexport */ hexToBytes; }); +__webpack_require__.d(__webpack_exports__, "TLSCloseNotify", function() { return /* reexport */ TLSCloseNotify; }); +__webpack_require__.d(__webpack_exports__, "TLSError", function() { return /* reexport */ TLSError; }); +__webpack_require__.d(__webpack_exports__, "utf8ToBytes", function() { return /* reexport */ utf8ToBytes; }); +__webpack_require__.d(__webpack_exports__, "_internals", function() { return /* binding */ _internals; }); + +// CONCATENATED MODULE: ./src/alerts.js +/* 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/. */ + +/* eslint-disable sorting/sort-object-props */ +const ALERT_LEVEL = { + WARNING: 1, + FATAL: 2 +}; + +const ALERT_DESCRIPTION = { + CLOSE_NOTIFY: 0, + UNEXPECTED_MESSAGE: 10, + BAD_RECORD_MAC: 20, + RECORD_OVERFLOW: 22, + HANDSHAKE_FAILURE: 40, + ILLEGAL_PARAMETER: 47, + DECODE_ERROR: 50, + DECRYPT_ERROR: 51, + PROTOCOL_VERSION: 70, + INTERNAL_ERROR: 80, + MISSING_EXTENSION: 109, + UNSUPPORTED_EXTENSION: 110, + UNKNOWN_PSK_IDENTITY: 115, + NO_APPLICATION_PROTOCOL: 120, +}; +/* eslint-enable sorting/sort-object-props */ + +function alertTypeToName(type) { + for (const name in ALERT_DESCRIPTION) { + if (ALERT_DESCRIPTION[name] === type) { + return `${name} (${type})`; + } + } + return `UNKNOWN (${type})`; +} + +class TLSAlert extends Error { + constructor(description, level) { + super(`TLS Alert: ${alertTypeToName(description)}`); + this.description = description; + this.level = level; + } + + static fromBytes(bytes) { + if (bytes.byteLength !== 2) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + switch (bytes[1]) { + case ALERT_DESCRIPTION.CLOSE_NOTIFY: + if (bytes[0] !== ALERT_LEVEL.WARNING) { + // Close notifications should be fatal. + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + return new TLSCloseNotify(); + default: + return new TLSError(bytes[1]); + } + } + + toBytes() { + return new Uint8Array([this.level, this.description]); + } +} + +class TLSCloseNotify extends TLSAlert { + constructor() { + super(ALERT_DESCRIPTION.CLOSE_NOTIFY, ALERT_LEVEL.WARNING); + } +} + +class TLSError extends TLSAlert { + constructor(description = ALERT_DESCRIPTION.INTERNAL_ERROR) { + super(description, ALERT_LEVEL.FATAL); + } +} + +// CONCATENATED MODULE: ./src/utils.js +/* 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/. */ + + + +// +// Various low-level utility functions. +// +// These are mostly conveniences for working with Uint8Arrays as +// the primitive "bytes" type. +// + +const UTF8_ENCODER = new TextEncoder(); +const UTF8_DECODER = new TextDecoder(); + +function noop() {} + +function assert(cond, msg) { + if (! cond) { + throw new Error('assert failed: ' + msg); + } +} + +function assertIsBytes(value, msg = 'value must be a Uint8Array') { + // Using `value instanceof Uint8Array` seems to fail in Firefox chrome code + // for inscrutable reasons, so we do a less direct check. + assert(ArrayBuffer.isView(value), msg); + assert(value.BYTES_PER_ELEMENT === 1, msg); + return value; +} + +const EMPTY = new Uint8Array(0); + +function zeros(n) { + return new Uint8Array(n); +} + +function arrayToBytes(value) { + return new Uint8Array(value); +} + +function bytesToHex(bytes) { + return Array.prototype.map.call(bytes, byte => { + let s = byte.toString(16); + if (s.length === 1) { + s = '0' + s; + } + return s; + }).join(''); +} + +function hexToBytes(hexstr) { + assert(hexstr.length % 2 === 0, 'hexstr.length must be even'); + return new Uint8Array(Array.prototype.map.call(hexstr, (c, n) => { + if (n % 2 === 1) { + return hexstr[n - 1] + c; + } else { + return ''; + } + }).filter(s => { + return !! s; + }).map(s => { + return parseInt(s, 16); + })); +} + +function bytesToUtf8(bytes) { + return UTF8_DECODER.decode(bytes); +} + +function utf8ToBytes(str) { + return UTF8_ENCODER.encode(str); +} + +function bytesToBase64url(bytes) { + // XXX TODO: try to use something constant-time, in case calling code + // uses it to encode secrets? + const charCodes = String.fromCharCode.apply(String, bytes); + return btoa(charCodes).replace(/\+/g, '-').replace(/\//g, '_'); +} + +function base64urlToBytes(str) { + // XXX TODO: try to use something constant-time, in case calling code + // uses it to decode secrets? + str = atob(str.replace(/-/g, '+').replace(/_/g, '/')); + const bytes = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes; +} + +function bytesAreEqual(v1, v2) { + assertIsBytes(v1); + assertIsBytes(v2); + if (v1.length !== v2.length) { + return false; + } + for (let i = 0; i < v1.length; i++) { + if (v1[i] !== v2[i]) { + return false; + } + } + return true; +} + +// The `BufferReader` and `BufferWriter` classes are helpers for dealing with the +// binary struct format that's used for various TLS message. Think of them as a +// buffer with a pointer to the "current position" and a bunch of helper methods +// to read/write structured data and advance said pointer. + +class utils_BufferWithPointer { + constructor(buf) { + this._buffer = buf; + this._dataview = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + this._pos = 0; + } + + length() { + return this._buffer.byteLength; + } + + tell() { + return this._pos; + } + + seek(pos) { + if (pos < 0) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + if (pos > this.length()) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + this._pos = pos; + } + + incr(offset) { + this.seek(this._pos + offset); + } +} + +// The `BufferReader` class helps you read structured data from a byte array. +// It offers methods for reading both primitive values, and the variable-length +// vector structures defined in https://tools.ietf.org/html/rfc8446#section-3.4. +// +// Such vectors are represented as a length followed by the concatenated +// bytes of each item, and the size of the length field is determined by +// the maximum allowed number of bytes in the vector. For example +// to read a vector that may contain up to 65535 bytes, use `readVector16`. +// +// To read a variable-length vector of between 1 and 100 uint16 values, +// defined in the RFC like this: +// +// uint16 items<2..200>; +// +// You would do something like this: +// +// const items = [] +// buf.readVector8(buf => { +// items.push(buf.readUint16()) +// }) +// +// The various `read` will throw `DECODE_ERROR` if you attempt to read path +// the end of the buffer, or past the end of a variable-length list. +// +class utils_BufferReader extends utils_BufferWithPointer { + + hasMoreBytes() { + return this.tell() < this.length(); + } + + readBytes(length) { + // This avoids copies by returning a view onto the existing buffer. + const start = this._buffer.byteOffset + this.tell(); + this.incr(length); + return new Uint8Array(this._buffer.buffer, start, length); + } + + _rangeErrorToAlert(cb) { + try { + return cb(this); + } catch (err) { + if (err instanceof RangeError) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + throw err; + } + } + + readUint8() { + return this._rangeErrorToAlert(() => { + const n = this._dataview.getUint8(this._pos); + this.incr(1); + return n; + }); + } + + readUint16() { + return this._rangeErrorToAlert(() => { + const n = this._dataview.getUint16(this._pos); + this.incr(2); + return n; + }); + } + + readUint24() { + return this._rangeErrorToAlert(() => { + let n = this._dataview.getUint16(this._pos); + n = (n << 8) | this._dataview.getUint8(this._pos + 2); + this.incr(3); + return n; + }); + } + + readUint32() { + return this._rangeErrorToAlert(() => { + const n = this._dataview.getUint32(this._pos); + this.incr(4); + return n; + }); + } + + _readVector(length, cb) { + const contentsBuf = new utils_BufferReader(this.readBytes(length)); + const expectedEnd = this.tell(); + // Keep calling the callback until we've consumed the expected number of bytes. + let n = 0; + while (contentsBuf.hasMoreBytes()) { + const prevPos = contentsBuf.tell(); + cb(contentsBuf, n); + // Check that the callback made forward progress, otherwise we'll infinite loop. + if (contentsBuf.tell() <= prevPos) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + n += 1; + } + // Check that the callback correctly consumed the vector's entire contents. + if (this.tell() !== expectedEnd) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + } + + readVector8(cb) { + const length = this.readUint8(); + return this._readVector(length, cb); + } + + readVector16(cb) { + const length = this.readUint16(); + return this._readVector(length, cb); + } + + readVector24(cb) { + const length = this.readUint24(); + return this._readVector(length, cb); + } + + readVectorBytes8() { + return this.readBytes(this.readUint8()); + } + + readVectorBytes16() { + return this.readBytes(this.readUint16()); + } + + readVectorBytes24() { + return this.readBytes(this.readUint24()); + } +} + + +class utils_BufferWriter extends utils_BufferWithPointer { + constructor(size = 1024) { + super(new Uint8Array(size)); + } + + _maybeGrow(n) { + const curSize = this._buffer.byteLength; + const newPos = this._pos + n; + const shortfall = newPos - curSize; + if (shortfall > 0) { + // Classic grow-by-doubling, up to 4kB max increment. + // This formula was not arrived at by any particular science. + const incr = Math.min(curSize, 4 * 1024); + const newbuf = new Uint8Array(curSize + Math.ceil(shortfall / incr) * incr); + newbuf.set(this._buffer, 0); + this._buffer = newbuf; + this._dataview = new DataView(newbuf.buffer, newbuf.byteOffset, newbuf.byteLength); + } + } + + slice(start = 0, end = this.tell()) { + if (end < 0) { + end = this.tell() + end; + } + if (start < 0) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + if (end < 0) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + if (end > this.length()) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + return this._buffer.slice(start, end); + } + + flush() { + const slice = this.slice(); + this.seek(0); + return slice; + } + + writeBytes(data) { + this._maybeGrow(data.byteLength); + this._buffer.set(data, this.tell()); + this.incr(data.byteLength); + } + + writeUint8(n) { + this._maybeGrow(1); + this._dataview.setUint8(this._pos, n); + this.incr(1); + } + + writeUint16(n) { + this._maybeGrow(2); + this._dataview.setUint16(this._pos, n); + this.incr(2); + } + + writeUint24(n) { + this._maybeGrow(3); + this._dataview.setUint16(this._pos, n >> 8); + this._dataview.setUint8(this._pos + 2, n & 0xFF); + this.incr(3); + } + + writeUint32(n) { + this._maybeGrow(4); + this._dataview.setUint32(this._pos, n); + this.incr(4); + } + + // These are helpers for writing the variable-length vector structure + // defined in https://tools.ietf.org/html/rfc8446#section-3.4. + // + // Such vectors are represented as a length followed by the concatenated + // bytes of each item, and the size of the length field is determined by + // the maximum allowed size of the vector. For example to write a vector + // that may contain up to 65535 bytes, use `writeVector16`. + // + // To write a variable-length vector of between 1 and 100 uint16 values, + // defined in the RFC like this: + // + // uint16 items<2..200>; + // + // You would do something like this: + // + // buf.writeVector8(buf => { + // for (let item of items) { + // buf.writeUint16(item) + // } + // }) + // + // The helper will automatically take care of writing the appropriate + // length field once the callback completes. + + _writeVector(maxLength, writeLength, cb) { + // Initially, write the length field as zero. + const lengthPos = this.tell(); + writeLength(0); + // Call the callback to write the vector items. + const bodyPos = this.tell(); + cb(this); + const length = this.tell() - bodyPos; + if (length >= maxLength) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + // Backfill the actual length field. + this.seek(lengthPos); + writeLength(length); + this.incr(length); + return length; + } + + writeVector8(cb) { + return this._writeVector(Math.pow(2, 8), len => this.writeUint8(len), cb); + } + + writeVector16(cb) { + return this._writeVector(Math.pow(2, 16), len => this.writeUint16(len), cb); + } + + writeVector24(cb) { + return this._writeVector(Math.pow(2, 24), len => this.writeUint24(len), cb); + } + + writeVectorBytes8(bytes) { + return this.writeVector8(buf => { + buf.writeBytes(bytes); + }); + } + + writeVectorBytes16(bytes) { + return this.writeVector16(buf => { + buf.writeBytes(bytes); + }); + } + + writeVectorBytes24(bytes) { + return this.writeVector24(buf => { + buf.writeBytes(bytes); + }); + } +} + +// CONCATENATED MODULE: ./src/crypto.js +/* 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/. */ + +// +// Low-level crypto primitives. +// +// This file implements the AEAD encrypt/decrypt and hashing routines +// for the TLS_AES_128_GCM_SHA256 ciphersuite. They are (thankfully) +// fairly light-weight wrappers around what's available via the WebCrypto +// API. +// + + + + +const AEAD_SIZE_INFLATION = 16; +const KEY_LENGTH = 16; +const IV_LENGTH = 12; +const HASH_LENGTH = 32; + +async function prepareKey(key, mode) { + return crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, [mode]); +} + +async function encrypt(key, iv, plaintext, additionalData) { + const ciphertext = await crypto.subtle.encrypt({ + additionalData, + iv, + name: 'AES-GCM', + tagLength: AEAD_SIZE_INFLATION * 8 + }, key, plaintext); + return new Uint8Array(ciphertext); +} + +async function decrypt(key, iv, ciphertext, additionalData) { + try { + const plaintext = await crypto.subtle.decrypt({ + additionalData, + iv, + name: 'AES-GCM', + tagLength: AEAD_SIZE_INFLATION * 8 + }, key, ciphertext); + return new Uint8Array(plaintext); + } catch (err) { + // Yes, we really do throw 'decrypt_error' when failing to verify a HMAC, + // and a 'bad_record_mac' error when failing to decrypt. + throw new TLSError(ALERT_DESCRIPTION.BAD_RECORD_MAC); + } +} + +async function hash(message) { + return new Uint8Array(await crypto.subtle.digest({ name: 'SHA-256' }, message)); +} + +async function hmac(keyBytes, message) { + const key = await crypto.subtle.importKey('raw', keyBytes, { + hash: { name: 'SHA-256' }, + name: 'HMAC', + }, false, ['sign']); + const sig = await crypto.subtle.sign({ name: 'HMAC' }, key, message); + return new Uint8Array(sig); +} + +async function verifyHmac(keyBytes, signature, message) { + const key = await crypto.subtle.importKey('raw', keyBytes, { + hash: { name: 'SHA-256' }, + name: 'HMAC', + }, false, ['verify']); + if (! (await crypto.subtle.verify({ name: 'HMAC' }, key, signature, message))) { + // Yes, we really do throw 'decrypt_error' when failing to verify a HMAC, + // and a 'bad_record_mac' error when failing to decrypt. + throw new TLSError(ALERT_DESCRIPTION.DECRYPT_ERROR); + } +} + +async function hkdfExtract(salt, ikm) { + // Ref https://tools.ietf.org/html/rfc5869#section-2.2 + return await hmac(salt, ikm); +} + +async function hkdfExpand(prk, info, length) { + // Ref https://tools.ietf.org/html/rfc5869#section-2.3 + const N = Math.ceil(length / HASH_LENGTH); + if (N <= 0) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + if (N >= 255) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + const input = new utils_BufferWriter(); + const output = new utils_BufferWriter(); + let T = new Uint8Array(0); + for (let i = 1; i <= N; i++) { + input.writeBytes(T); + input.writeBytes(info); + input.writeUint8(i); + T = await hmac(prk, input.flush()); + output.writeBytes(T); + } + return output.slice(0, length); +} + +async function hkdfExpandLabel(secret, label, context, length) { + // struct { + // uint16 length = Length; + // opaque label < 7..255 > = "tls13 " + Label; + // opaque context < 0..255 > = Context; + // } HkdfLabel; + const hkdfLabel = new utils_BufferWriter(); + hkdfLabel.writeUint16(length); + hkdfLabel.writeVectorBytes8(utf8ToBytes('tls13 ' + label)); + hkdfLabel.writeVectorBytes8(context); + return hkdfExpand(secret, hkdfLabel.flush(), length); +} + +async function getRandomBytes(size) { + const bytes = new Uint8Array(size); + crypto.getRandomValues(bytes); + return bytes; +} + +// CONCATENATED MODULE: ./src/extensions.js +/* 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/. */ + +// +// Extension parsing. +// +// This file contains some helpers for reading/writing the various kinds +// of Extension that might appear in a HandshakeMessage. +// +// "Extensions" are how TLS signals the presence of particular bits of optional +// functionality in the protocol. Lots of parts of TLS1.3 that don't seem like +// they're optional are implemented in terms of an extension, IIUC because that's +// what was needed for a clean deployment in amongst earlier versions of the protocol. +// + + + + + +/* eslint-disable sorting/sort-object-props */ +const EXTENSION_TYPE = { + PRE_SHARED_KEY: 41, + SUPPORTED_VERSIONS: 43, + PSK_KEY_EXCHANGE_MODES: 45, +}; +/* eslint-enable sorting/sort-object-props */ + +// Base class for generic reading/writing of extensions, +// which are all uniformly formatted as: +// +// struct { +// ExtensionType extension_type; +// opaque extension_data<0..2^16-1>; +// } Extension; +// +// Extensions always appear inside of a handshake message, +// and their internal structure may differ based on the +// type of that message. + +class extensions_Extension { + + get TYPE_TAG() { + throw new Error('not implemented'); + } + + static read(messageType, buf) { + const type = buf.readUint16(); + let ext = { + TYPE_TAG: type, + }; + buf.readVector16(buf => { + switch (type) { + case EXTENSION_TYPE.PRE_SHARED_KEY: + ext = extensions_PreSharedKeyExtension._read(messageType, buf); + break; + case EXTENSION_TYPE.SUPPORTED_VERSIONS: + ext = extensions_SupportedVersionsExtension._read(messageType, buf); + break; + case EXTENSION_TYPE.PSK_KEY_EXCHANGE_MODES: + ext = extensions_PskKeyExchangeModesExtension._read(messageType, buf); + break; + default: + // Skip over unrecognised extensions. + buf.incr(buf.length()); + } + if (buf.hasMoreBytes()) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + }); + return ext; + } + + write(messageType, buf) { + buf.writeUint16(this.TYPE_TAG); + buf.writeVector16(buf => { + this._write(messageType, buf); + }); + } + + static _read(messageType, buf) { + throw new Error('not implemented'); + } + + static _write(messageType, buf) { + throw new Error('not implemented'); + } +} + +// The PreSharedKey extension: +// +// struct { +// opaque identity<1..2^16-1>; +// uint32 obfuscated_ticket_age; +// } PskIdentity; +// opaque PskBinderEntry<32..255>; +// struct { +// PskIdentity identities<7..2^16-1>; +// PskBinderEntry binders<33..2^16-1>; +// } OfferedPsks; +// struct { +// select(Handshake.msg_type) { +// case client_hello: OfferedPsks; +// case server_hello: uint16 selected_identity; +// }; +// } PreSharedKeyExtension; + +class extensions_PreSharedKeyExtension extends extensions_Extension { + constructor(identities, binders, selectedIdentity) { + super(); + this.identities = identities; + this.binders = binders; + this.selectedIdentity = selectedIdentity; + } + + get TYPE_TAG() { + return EXTENSION_TYPE.PRE_SHARED_KEY; + } + + static _read(messageType, buf) { + let identities = null, binders = null, selectedIdentity = null; + switch (messageType) { + case HANDSHAKE_TYPE.CLIENT_HELLO: + identities = []; binders = []; + buf.readVector16(buf => { + const identity = buf.readVectorBytes16(); + buf.readBytes(4); // Skip over the ticket age. + identities.push(identity); + }); + buf.readVector16(buf => { + const binder = buf.readVectorBytes8(); + if (binder.byteLength < HASH_LENGTH) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + binders.push(binder); + }); + if (identities.length !== binders.length) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + break; + case HANDSHAKE_TYPE.SERVER_HELLO: + selectedIdentity = buf.readUint16(); + break; + default: + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + return new this(identities, binders, selectedIdentity); + } + + _write(messageType, buf) { + switch (messageType) { + case HANDSHAKE_TYPE.CLIENT_HELLO: + buf.writeVector16(buf => { + this.identities.forEach(pskId => { + buf.writeVectorBytes16(pskId); + buf.writeUint32(0); // Zero for "tag age" field. + }); + }); + buf.writeVector16(buf => { + this.binders.forEach(pskBinder => { + buf.writeVectorBytes8(pskBinder); + }); + }); + break; + case HANDSHAKE_TYPE.SERVER_HELLO: + buf.writeUint16(this.selectedIdentity); + break; + default: + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + } +} + + +// The SupportedVersions extension: +// +// struct { +// select(Handshake.msg_type) { +// case client_hello: +// ProtocolVersion versions < 2..254 >; +// case server_hello: +// ProtocolVersion selected_version; +// }; +// } SupportedVersions; + +class extensions_SupportedVersionsExtension extends extensions_Extension { + constructor(versions, selectedVersion) { + super(); + this.versions = versions; + this.selectedVersion = selectedVersion; + } + + get TYPE_TAG() { + return EXTENSION_TYPE.SUPPORTED_VERSIONS; + } + + static _read(messageType, buf) { + let versions = null, selectedVersion = null; + switch (messageType) { + case HANDSHAKE_TYPE.CLIENT_HELLO: + versions = []; + buf.readVector8(buf => { + versions.push(buf.readUint16()); + }); + break; + case HANDSHAKE_TYPE.SERVER_HELLO: + selectedVersion = buf.readUint16(); + break; + default: + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + return new this(versions, selectedVersion); + } + + _write(messageType, buf) { + switch (messageType) { + case HANDSHAKE_TYPE.CLIENT_HELLO: + buf.writeVector8(buf => { + this.versions.forEach(version => { + buf.writeUint16(version); + }); + }); + break; + case HANDSHAKE_TYPE.SERVER_HELLO: + buf.writeUint16(this.selectedVersion); + break; + default: + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + } +} + + +class extensions_PskKeyExchangeModesExtension extends extensions_Extension { + constructor(modes) { + super(); + this.modes = modes; + } + + get TYPE_TAG() { + return EXTENSION_TYPE.PSK_KEY_EXCHANGE_MODES; + } + + static _read(messageType, buf) { + const modes = []; + switch (messageType) { + case HANDSHAKE_TYPE.CLIENT_HELLO: + buf.readVector8(buf => { + modes.push(buf.readUint8()); + }); + break; + default: + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + return new this(modes); + } + + _write(messageType, buf) { + switch (messageType) { + case HANDSHAKE_TYPE.CLIENT_HELLO: + buf.writeVector8(buf => { + this.modes.forEach(mode => { + buf.writeUint8(mode); + }); + }); + break; + default: + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + } +} + +// CONCATENATED MODULE: ./src/constants.js +/* 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 VERSION_TLS_1_0 = 0x0301; +const VERSION_TLS_1_2 = 0x0303; +const VERSION_TLS_1_3 = 0x0304; +const TLS_AES_128_GCM_SHA256 = 0x1301; +const PSK_MODE_KE = 0; + +// CONCATENATED MODULE: ./src/messages.js +/* 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/. */ + +// +// Message parsing. +// +// Herein we have code for reading and writing the various Handshake +// messages involved in the TLS protocol. +// + + + + + + + +/* eslint-disable sorting/sort-object-props */ +const HANDSHAKE_TYPE = { + CLIENT_HELLO: 1, + SERVER_HELLO: 2, + NEW_SESSION_TICKET: 4, + ENCRYPTED_EXTENSIONS: 8, + FINISHED: 20, +}; +/* eslint-enable sorting/sort-object-props */ + +// Base class for generic reading/writing of handshake messages, +// which are all uniformly formatted as: +// +// struct { +// HandshakeType msg_type; /* handshake type */ +// uint24 length; /* bytes in message */ +// select(Handshake.msg_type) { +// ... type specific cases here ... +// }; +// } Handshake; + +class messages_HandshakeMessage { + + get TYPE_TAG() { + throw new Error('not implemented'); + } + + static fromBytes(bytes) { + // Each handshake message has a type and length prefix, per + // https://tools.ietf.org/html/rfc8446#appendix-B.3 + const buf = new utils_BufferReader(bytes); + const msg = this.read(buf); + if (buf.hasMoreBytes()) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + return msg; + } + + toBytes() { + const buf = new utils_BufferWriter(); + this.write(buf); + return buf.flush(); + } + + static read(buf) { + const type = buf.readUint8(); + let msg = null; + buf.readVector24(buf => { + switch (type) { + case HANDSHAKE_TYPE.CLIENT_HELLO: + msg = messages_ClientHello._read(buf); + break; + case HANDSHAKE_TYPE.SERVER_HELLO: + msg = messages_ServerHello._read(buf); + break; + case HANDSHAKE_TYPE.NEW_SESSION_TICKET: + msg = messages_NewSessionTicket._read(buf); + break; + case HANDSHAKE_TYPE.ENCRYPTED_EXTENSIONS: + msg = EncryptedExtensions._read(buf); + break; + case HANDSHAKE_TYPE.FINISHED: + msg = messages_Finished._read(buf); + break; + } + if (buf.hasMoreBytes()) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + }); + if (msg === null) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + return msg; + } + + write(buf) { + buf.writeUint8(this.TYPE_TAG); + buf.writeVector24(buf => { + this._write(buf); + }); + } + + static _read(buf) { + throw new Error('not implemented'); + } + + _write(buf) { + throw new Error('not implemented'); + } + + // Some little helpers for reading a list of extensions, + // which is uniformly represented as: + // + // Extension extensions<8..2^16-1>; + // + // Recognized extensions are returned as a Map from extension type + // to extension data object, with a special `lastSeenExtension` + // property to make it easy to check which one came last. + + static _readExtensions(messageType, buf) { + const extensions = new Map(); + buf.readVector16(buf => { + const ext = extensions_Extension.read(messageType, buf); + if (extensions.has(ext.TYPE_TAG)) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + extensions.set(ext.TYPE_TAG, ext); + extensions.lastSeenExtension = ext.TYPE_TAG; + }); + return extensions; + } + + _writeExtensions(buf, extensions) { + buf.writeVector16(buf => { + extensions.forEach(ext => { + ext.write(this.TYPE_TAG, buf); + }); + }); + } +} + + +// The ClientHello message: +// +// struct { +// ProtocolVersion legacy_version = 0x0303; +// Random random; +// opaque legacy_session_id<0..32>; +// CipherSuite cipher_suites<2..2^16-2>; +// opaque legacy_compression_methods<1..2^8-1>; +// Extension extensions<8..2^16-1>; +// } ClientHello; + +class messages_ClientHello extends messages_HandshakeMessage { + + constructor(random, sessionId, extensions) { + super(); + this.random = random; + this.sessionId = sessionId; + this.extensions = extensions; + } + + get TYPE_TAG() { + return HANDSHAKE_TYPE.CLIENT_HELLO; + } + + static _read(buf) { + // The legacy_version field may indicate an earlier version of TLS + // for backwards compatibility, but must not predate TLS 1.0! + if (buf.readUint16() < VERSION_TLS_1_0) { + throw new TLSError(ALERT_DESCRIPTION.PROTOCOL_VERSION); + } + // The random bytes provided by the peer. + const random = buf.readBytes(32); + // Read legacy_session_id, so the server can echo it. + const sessionId = buf.readVectorBytes8(); + // We only support a single ciphersuite, but the peer may offer several. + // Scan the list to confirm that the one we want is present. + let found = false; + buf.readVector16(buf => { + const cipherSuite = buf.readUint16(); + if (cipherSuite === TLS_AES_128_GCM_SHA256) { + found = true; + } + }); + if (! found) { + throw new TLSError(ALERT_DESCRIPTION.HANDSHAKE_FAILURE); + } + // legacy_compression_methods must be a single zero byte for TLS1.3 ClientHellos. + // It can be non-zero in previous versions of TLS, but we're not going to + // make a successful handshake with such versions, so better to just bail out now. + const legacyCompressionMethods = buf.readVectorBytes8(); + if (legacyCompressionMethods.byteLength !== 1) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + if (legacyCompressionMethods[0] !== 0x00) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + // Read and check the extensions. + const extensions = this._readExtensions(HANDSHAKE_TYPE.CLIENT_HELLO, buf); + if (! extensions.has(EXTENSION_TYPE.SUPPORTED_VERSIONS)) { + throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION); + } + if (extensions.get(EXTENSION_TYPE.SUPPORTED_VERSIONS).versions.indexOf(VERSION_TLS_1_3) === -1) { + throw new TLSError(ALERT_DESCRIPTION.PROTOCOL_VERSION); + } + // Was the PreSharedKey extension the last one? + if (extensions.has(EXTENSION_TYPE.PRE_SHARED_KEY)) { + if (extensions.lastSeenExtension !== EXTENSION_TYPE.PRE_SHARED_KEY) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + } + return new this(random, sessionId, extensions); + } + + _write(buf) { + buf.writeUint16(VERSION_TLS_1_2); + buf.writeBytes(this.random); + buf.writeVectorBytes8(this.sessionId); + // Our single supported ciphersuite + buf.writeVector16(buf => { + buf.writeUint16(TLS_AES_128_GCM_SHA256); + }); + // A single zero byte for legacy_compression_methods + buf.writeVectorBytes8(new Uint8Array(1)); + this._writeExtensions(buf, this.extensions); + } +} + + +// The ServerHello message: +// +// struct { +// ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */ +// Random random; +// opaque legacy_session_id_echo<0..32>; +// CipherSuite cipher_suite; +// uint8 legacy_compression_method = 0; +// Extension extensions < 6..2 ^ 16 - 1 >; +// } ServerHello; + +class messages_ServerHello extends messages_HandshakeMessage { + + constructor(random, sessionId, extensions) { + super(); + this.random = random; + this.sessionId = sessionId; + this.extensions = extensions; + } + + get TYPE_TAG() { + return HANDSHAKE_TYPE.SERVER_HELLO; + } + + static _read(buf) { + // Fixed value for legacy_version. + if (buf.readUint16() !== VERSION_TLS_1_2) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + // Random bytes from the server. + const random = buf.readBytes(32); + // It should have echoed our vector for legacy_session_id. + const sessionId = buf.readVectorBytes8(); + // It should have selected our single offered ciphersuite. + if (buf.readUint16() !== TLS_AES_128_GCM_SHA256) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + // legacy_compression_method must be zero. + if (buf.readUint8() !== 0) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + const extensions = this._readExtensions(HANDSHAKE_TYPE.SERVER_HELLO, buf); + if (! extensions.has(EXTENSION_TYPE.SUPPORTED_VERSIONS)) { + throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION); + } + if (extensions.get(EXTENSION_TYPE.SUPPORTED_VERSIONS).selectedVersion !== VERSION_TLS_1_3) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + return new this(random, sessionId, extensions); + } + + _write(buf) { + buf.writeUint16(VERSION_TLS_1_2); + buf.writeBytes(this.random); + buf.writeVectorBytes8(this.sessionId); + // Our single supported ciphersuite + buf.writeUint16(TLS_AES_128_GCM_SHA256); + // A single zero byte for legacy_compression_method + buf.writeUint8(0); + this._writeExtensions(buf, this.extensions); + } +} + + +// The EncryptedExtensions message: +// +// struct { +// Extension extensions < 0..2 ^ 16 - 1 >; +// } EncryptedExtensions; +// +// We don't actually send any EncryptedExtensions, +// but still have to send an empty message. + +class EncryptedExtensions extends messages_HandshakeMessage { + constructor(extensions) { + super(); + this.extensions = extensions; + } + + get TYPE_TAG() { + return HANDSHAKE_TYPE.ENCRYPTED_EXTENSIONS; + } + + static _read(buf) { + const extensions = this._readExtensions(HANDSHAKE_TYPE.ENCRYPTED_EXTENSIONS, buf); + return new this(extensions); + } + + _write(buf) { + this._writeExtensions(buf, this.extensions); + } +} + + +// The Finished message: +// +// struct { +// opaque verify_data[Hash.length]; +// } Finished; + +class messages_Finished extends messages_HandshakeMessage { + + constructor(verifyData) { + super(); + this.verifyData = verifyData; + } + + get TYPE_TAG() { + return HANDSHAKE_TYPE.FINISHED; + } + + static _read(buf) { + const verifyData = buf.readBytes(HASH_LENGTH); + return new this(verifyData); + } + + _write(buf) { + buf.writeBytes(this.verifyData); + } +} + + +// The NewSessionTicket message: +// +// struct { +// uint32 ticket_lifetime; +// uint32 ticket_age_add; +// opaque ticket_nonce < 0..255 >; +// opaque ticket < 1..2 ^ 16 - 1 >; +// Extension extensions < 0..2 ^ 16 - 2 >; +// } NewSessionTicket; +// +// We don't actually make use of these, but we need to be able +// to accept them and do basic validation. + +class messages_NewSessionTicket extends messages_HandshakeMessage { + constructor(ticketLifetime, ticketAgeAdd, ticketNonce, ticket, extensions) { + super(); + this.ticketLifetime = ticketLifetime; + this.ticketAgeAdd = ticketAgeAdd; + this.ticketNonce = ticketNonce; + this.ticket = ticket; + this.extensions = extensions; + } + + get TYPE_TAG() { + return HANDSHAKE_TYPE.NEW_SESSION_TICKET; + } + + static _read(buf) { + const ticketLifetime = buf.readUint32(); + const ticketAgeAdd = buf.readUint32(); + const ticketNonce = buf.readVectorBytes8(); + const ticket = buf.readVectorBytes16(); + if (ticket.byteLength < 1) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + const extensions = this._readExtensions(HANDSHAKE_TYPE.NEW_SESSION_TICKET, buf); + return new this(ticketLifetime, ticketAgeAdd, ticketNonce, ticket, extensions); + } + + _write(buf) { + buf.writeUint32(this.ticketLifetime); + buf.writeUint32(this.ticketAgeAdd); + buf.writeVectorBytes8(this.ticketNonce); + buf.writeVectorBytes16(this.ticket); + this._writeExtensions(buf, this.extensions); + } +} + +// CONCATENATED MODULE: ./src/states.js +/* 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/. */ + + + + + + + + +// +// State-machine for TLS Handshake Management. +// +// Internally, we manage the TLS connection by explicitly modelling the +// client and server state-machines from RFC8446. You can think of +// these `State` objects as little plugins for the `Connection` class +// that provide different behaviours of `send` and `receive` depending +// on the state of the connection. +// + +class states_State { + + constructor(conn) { + this.conn = conn; + } + + async initialize() { + // By default, nothing to do when entering the state. + } + + async sendApplicationData(bytes) { + // By default, assume we're not ready to send yet and the caller + // should be blocking on the connection promise before reaching here. + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + + async recvApplicationData(bytes) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + + async recvHandshakeMessage(msg) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + + async recvAlertMessage(alert) { + switch (alert.description) { + case ALERT_DESCRIPTION.CLOSE_NOTIFY: + this.conn._closeForRecv(alert); + throw alert; + default: + return await this.handleErrorAndRethrow(alert); + } + } + + async recvChangeCipherSpec(bytes) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + + async handleErrorAndRethrow(err) { + let alert = err; + if (! (alert instanceof TLSAlert)) { + alert = new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + // Try to send error alert to the peer, but we may not + // be able to if the outgoing connection was already closed. + try { + await this.conn._sendAlertMessage(alert); + } catch (_) { } + await this.conn._transition(ERROR, err); + throw err; + } + + async close() { + const alert = new TLSCloseNotify(); + await this.conn._sendAlertMessage(alert); + this.conn._closeForSend(alert); + } + +} + +// A special "guard" state to prevent us from using +// an improperly-initialized Connection. + +class UNINITIALIZED extends states_State { + async initialize() { + throw new Error('uninitialized state'); + } + async sendApplicationData(bytes) { + throw new Error('uninitialized state'); + } + async recvApplicationData(bytes) { + throw new Error('uninitialized state'); + } + async recvHandshakeMessage(msg) { + throw new Error('uninitialized state'); + } + async recvChangeCipherSpec(bytes) { + throw new Error('uninitialized state'); + } + async handleErrorAndRethrow(err) { + throw err; + } + async close() { + throw new Error('uninitialized state'); + } +} + +// A special "error" state for when something goes wrong. +// This state never transitions to another state, effectively +// terminating the connection. + +class ERROR extends states_State { + async initialize(err) { + this.error = err; + this.conn._setConnectionFailure(err); + // Unceremoniously shut down the record layer on error. + this.conn._recordlayer.setSendError(err); + this.conn._recordlayer.setRecvError(err); + } + async sendApplicationData(bytes) { + throw this.error; + } + async recvApplicationData(bytes) { + throw this.error; + } + async recvHandshakeMessage(msg) { + throw this.error; + } + async recvAlertMessage(err) { + throw this.error; + } + async recvChangeCipherSpec(bytes) { + throw this.error; + } + async handleErrorAndRethrow(err) { + throw err; + } + async close() { + throw this.error; + } +} + +// The "connected" state, for when the handshake is complete +// and we're ready to send application-level data. +// The logic for this is largely symmetric between client and server. + +class states_CONNECTED extends states_State { + async initialize() { + this.conn._setConnectionSuccess(); + } + async sendApplicationData(bytes) { + await this.conn._sendApplicationData(bytes); + } + async recvApplicationData(bytes) { + return bytes; + } + async recvChangeCipherSpec(bytes) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } +} + +// A base class for states that occur in the middle of the handshake +// (that is, between ClientHello and Finished). These states may receive +// CHANGE_CIPHER_SPEC records for b/w compat reasons, which must contain +// exactly a single 0x01 byte and must otherwise be ignored. + +class states_MidHandshakeState extends states_State { + async recvChangeCipherSpec(bytes) { + if (this.conn._hasSeenChangeCipherSpec) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + if (bytes.byteLength !== 1 || bytes[0] !== 1) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + this.conn._hasSeenChangeCipherSpec = true; + } +} + +// These states implement (part of) the client state-machine from +// https://tools.ietf.org/html/rfc8446#appendix-A.1 +// +// Since we're only implementing a small subset of TLS1.3, +// we only need a small subset of the handshake. It basically goes: +// +// * send ClientHello +// * receive ServerHello +// * receive EncryptedExtensions +// * receive server Finished +// * send client Finished +// +// We include some unused states for completeness, so that it's easier +// to check the implementation against the diagrams in the RFC. + +class states_CLIENT_START extends states_State { + async initialize() { + const keyschedule = this.conn._keyschedule; + await keyschedule.addPSK(this.conn.psk); + // Construct a ClientHello message with our single PSK. + // We can't know the PSK binder value yet, so we initially write zeros. + const clientHello = new messages_ClientHello( + // Client random salt. + await getRandomBytes(32), + // Random legacy_session_id; we *could* send an empty string here, + // but sending a random one makes it easier to be compatible with + // the data emitted by tlslite-ng for test-case generation. + await getRandomBytes(32), + [ + new extensions_SupportedVersionsExtension([VERSION_TLS_1_3]), + new extensions_PskKeyExchangeModesExtension([PSK_MODE_KE]), + new extensions_PreSharedKeyExtension([this.conn.pskId], [zeros(HASH_LENGTH)]), + ], + ); + const buf = new utils_BufferWriter(); + clientHello.write(buf); + // Now that we know what the ClientHello looks like, + // go back and calculate the appropriate PSK binder value. + // We only support a single PSK, so the length of the binders field is the + // length of the hash plus one for rendering it as a variable-length byte array, + // plus two for rendering the variable-length list of PSK binders. + const PSK_BINDERS_SIZE = HASH_LENGTH + 1 + 2; + const truncatedTranscript = buf.slice(0, buf.tell() - PSK_BINDERS_SIZE); + const pskBinder = await keyschedule.calculateFinishedMAC(keyschedule.extBinderKey, truncatedTranscript); + buf.incr(-HASH_LENGTH); + buf.writeBytes(pskBinder); + await this.conn._sendHandshakeMessageBytes(buf.flush()); + await this.conn._transition(states_CLIENT_WAIT_SH, clientHello.sessionId); + } +} + +class states_CLIENT_WAIT_SH extends states_State { + async initialize(sessionId) { + this._sessionId = sessionId; + } + async recvHandshakeMessage(msg) { + if (! (msg instanceof messages_ServerHello)) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + if (! bytesAreEqual(msg.sessionId, this._sessionId)) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + const pskExt = msg.extensions.get(EXTENSION_TYPE.PRE_SHARED_KEY); + if (! pskExt) { + throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION); + } + // We expect only the SUPPORTED_VERSIONS and PRE_SHARED_KEY extensions. + if (msg.extensions.size !== 2) { + throw new TLSError(ALERT_DESCRIPTION.UNSUPPORTED_EXTENSION); + } + if (pskExt.selectedIdentity !== 0) { + throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER); + } + await this.conn._keyschedule.addECDHE(null); + await this.conn._setSendKey(this.conn._keyschedule.clientHandshakeTrafficSecret); + await this.conn._setRecvKey(this.conn._keyschedule.serverHandshakeTrafficSecret); + await this.conn._transition(states_CLIENT_WAIT_EE); + } +} + +class states_CLIENT_WAIT_EE extends states_MidHandshakeState { + async recvHandshakeMessage(msg) { + // We don't make use of any encrypted extensions, but we still + // have to wait for the server to send the (empty) list of them. + if (! (msg instanceof EncryptedExtensions)) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + // We do not support any EncryptedExtensions. + if (msg.extensions.size !== 0) { + throw new TLSError(ALERT_DESCRIPTION.UNSUPPORTED_EXTENSION); + } + const keyschedule = this.conn._keyschedule; + const serverFinishedTranscript = keyschedule.getTranscript(); + await this.conn._transition(states_CLIENT_WAIT_FINISHED, serverFinishedTranscript); + } +} + +class states_CLIENT_WAIT_FINISHED extends states_State { + async initialize(serverFinishedTranscript) { + this._serverFinishedTranscript = serverFinishedTranscript; + } + async recvHandshakeMessage(msg) { + if (! (msg instanceof messages_Finished)) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + // Verify server Finished MAC. + const keyschedule = this.conn._keyschedule; + await keyschedule.verifyFinishedMAC(keyschedule.serverHandshakeTrafficSecret, msg.verifyData, this._serverFinishedTranscript); + // Send our own Finished message in return. + // This must be encrypted with the handshake traffic key, + // but must not appear in the transcript used to calculate the application keys. + const clientFinishedMAC = await keyschedule.calculateFinishedMAC(keyschedule.clientHandshakeTrafficSecret); + await keyschedule.finalize(); + await this.conn._sendHandshakeMessage(new messages_Finished(clientFinishedMAC)); + await this.conn._setSendKey(keyschedule.clientApplicationTrafficSecret); + await this.conn._setRecvKey(keyschedule.serverApplicationTrafficSecret); + await this.conn._transition(states_CLIENT_CONNECTED); + } +} + +class states_CLIENT_CONNECTED extends states_CONNECTED { + async recvHandshakeMessage(msg) { + // A connected client must be prepared to accept NewSessionTicket + // messages. We never use them, but other server implementations + // might send them. + if (! (msg instanceof messages_NewSessionTicket)) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + } +} + +// These states implement (part of) the server state-machine from +// https://tools.ietf.org/html/rfc8446#appendix-A.2 +// +// Since we're only implementing a small subset of TLS1.3, +// we only need a small subset of the handshake. It basically goes: +// +// * receive ClientHello +// * send ServerHello +// * send empty EncryptedExtensions +// * send server Finished +// * receive client Finished +// +// We include some unused states for completeness, so that it's easier +// to check the implementation against the diagrams in the RFC. + +class states_SERVER_START extends states_State { + async recvHandshakeMessage(msg) { + if (! (msg instanceof messages_ClientHello)) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + // In the spec, this is where we select connection parameters, and maybe + // tell the client to try again if we can't find a compatible set. + // Since we only support a fixed cipherset, the only thing to "negotiate" + // is whether they provided an acceptable PSK. + const pskExt = msg.extensions.get(EXTENSION_TYPE.PRE_SHARED_KEY); + const pskModesExt = msg.extensions.get(EXTENSION_TYPE.PSK_KEY_EXCHANGE_MODES); + if (! pskExt || ! pskModesExt) { + throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION); + } + if (pskModesExt.modes.indexOf(PSK_MODE_KE) === -1) { + throw new TLSError(ALERT_DESCRIPTION.HANDSHAKE_FAILURE); + } + const pskIndex = pskExt.identities.findIndex(pskId => bytesAreEqual(pskId, this.conn.pskId)); + if (pskIndex === -1) { + throw new TLSError(ALERT_DESCRIPTION.UNKNOWN_PSK_IDENTITY); + } + await this.conn._keyschedule.addPSK(this.conn.psk); + // Validate the PSK binder. + const keyschedule = this.conn._keyschedule; + const transcript = keyschedule.getTranscript(); + // Calculate size occupied by the PSK binders. + let pskBindersSize = 2; // Vector16 representation overhead. + for (const binder of pskExt.binders) { + pskBindersSize += binder.byteLength + 1; // Vector8 representation overhead. + } + await keyschedule.verifyFinishedMAC(keyschedule.extBinderKey, pskExt.binders[pskIndex], transcript.slice(0, -pskBindersSize)); + await this.conn._transition(states_SERVER_NEGOTIATED, msg.sessionId, pskIndex); + } +} + +class states_SERVER_NEGOTIATED extends states_MidHandshakeState { + async initialize(sessionId, pskIndex) { + await this.conn._sendHandshakeMessage(new messages_ServerHello( + // Server random + await getRandomBytes(32), + sessionId, + [ + new extensions_SupportedVersionsExtension(null, VERSION_TLS_1_3), + new extensions_PreSharedKeyExtension(null, null, pskIndex), + ] + )); + // If the client sent a non-empty sessionId, the server *must* send a change-cipher-spec for b/w compat. + if (sessionId.byteLength > 0) { + await this.conn._sendChangeCipherSpec(); + } + // We can now transition to the encrypted part of the handshake. + const keyschedule = this.conn._keyschedule; + await keyschedule.addECDHE(null); + await this.conn._setSendKey(keyschedule.serverHandshakeTrafficSecret); + await this.conn._setRecvKey(keyschedule.clientHandshakeTrafficSecret); + // Send an empty EncryptedExtensions message. + await this.conn._sendHandshakeMessage(new EncryptedExtensions([])); + // Send the Finished message. + const serverFinishedMAC = await keyschedule.calculateFinishedMAC(keyschedule.serverHandshakeTrafficSecret); + await this.conn._sendHandshakeMessage(new messages_Finished(serverFinishedMAC)); + // We can now *send* using the application traffic key, + // but have to wait to receive the client Finished before receiving under that key. + // We need to remember the handshake state from before the client Finished + // in order to successfully verify the client Finished. + const clientFinishedTranscript = await keyschedule.getTranscript(); + const clientHandshakeTrafficSecret = keyschedule.clientHandshakeTrafficSecret; + await keyschedule.finalize(); + await this.conn._setSendKey(keyschedule.serverApplicationTrafficSecret); + await this.conn._transition(states_SERVER_WAIT_FINISHED, clientHandshakeTrafficSecret, clientFinishedTranscript); + } +} + +class states_SERVER_WAIT_FINISHED extends states_MidHandshakeState { + async initialize(clientHandshakeTrafficSecret, clientFinishedTranscript) { + this._clientHandshakeTrafficSecret = clientHandshakeTrafficSecret; + this._clientFinishedTranscript = clientFinishedTranscript; + } + async recvHandshakeMessage(msg) { + if (! (msg instanceof messages_Finished)) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + const keyschedule = this.conn._keyschedule; + await keyschedule.verifyFinishedMAC(this._clientHandshakeTrafficSecret, msg.verifyData, this._clientFinishedTranscript); + this._clientHandshakeTrafficSecret = this._clientFinishedTranscript = null; + await this.conn._setRecvKey(keyschedule.clientApplicationTrafficSecret); + await this.conn._transition(states_CONNECTED); + } +} + +// CONCATENATED MODULE: ./src/keyschedule.js +/* 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/. */ + +// TLS1.3 Key Schedule. +// +// In this file we implement the "key schedule" from +// https://tools.ietf.org/html/rfc8446#section-7.1, which +// defines how to calculate various keys as the handshake +// state progresses. + + + + + + + +// The `KeySchedule` class progresses through three stages corresponding +// to the three phases of the TLS1.3 key schedule: +// +// UNINITIALIZED +// | +// | addPSK() +// v +// EARLY_SECRET +// | +// | addECDHE() +// v +// HANDSHAKE_SECRET +// | +// | finalize() +// v +// MASTER_SECRET +// +// It will error out if the calling code attempts to add key material +// in the wrong order. + +const STAGE_UNINITIALIZED = 0; +const STAGE_EARLY_SECRET = 1; +const STAGE_HANDSHAKE_SECRET = 2; +const STAGE_MASTER_SECRET = 3; + +class keyschedule_KeySchedule { + constructor() { + this.stage = STAGE_UNINITIALIZED; + // WebCrypto doesn't support a rolling hash construct, so we have to + // keep the entire message transcript in memory. + this.transcript = new utils_BufferWriter(); + // This tracks the main secret from with other keys are derived at each stage. + this.secret = null; + // And these are all the various keys we'll derive as the handshake progresses. + this.extBinderKey = null; + this.clientHandshakeTrafficSecret = null; + this.serverHandshakeTrafficSecret = null; + this.clientApplicationTrafficSecret = null; + this.serverApplicationTrafficSecret = null; + } + + async addPSK(psk) { + // Use the selected PSK (if any) to calculate the "early secret". + if (psk === null) { + psk = zeros(HASH_LENGTH); + } + if (this.stage !== STAGE_UNINITIALIZED) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + this.stage = STAGE_EARLY_SECRET; + this.secret = await hkdfExtract(zeros(HASH_LENGTH), psk); + this.extBinderKey = await this.deriveSecret('ext binder', EMPTY); + this.secret = await this.deriveSecret('derived', EMPTY); + } + + async addECDHE(ecdhe) { + // Mix in the ECDHE output (if any) to calculate the "handshake secret". + if (ecdhe === null) { + ecdhe = zeros(HASH_LENGTH); + } + if (this.stage !== STAGE_EARLY_SECRET) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + this.stage = STAGE_HANDSHAKE_SECRET; + this.extBinderKey = null; + this.secret = await hkdfExtract(this.secret, ecdhe); + this.clientHandshakeTrafficSecret = await this.deriveSecret('c hs traffic'); + this.serverHandshakeTrafficSecret = await this.deriveSecret('s hs traffic'); + this.secret = await this.deriveSecret('derived', EMPTY); + } + + async finalize() { + if (this.stage !== STAGE_HANDSHAKE_SECRET) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + this.stage = STAGE_MASTER_SECRET; + this.clientHandshakeTrafficSecret = null; + this.serverHandshakeTrafficSecret = null; + this.secret = await hkdfExtract(this.secret, zeros(HASH_LENGTH)); + this.clientApplicationTrafficSecret = await this.deriveSecret('c ap traffic'); + this.serverApplicationTrafficSecret = await this.deriveSecret('s ap traffic'); + this.secret = null; + } + + addToTranscript(bytes) { + this.transcript.writeBytes(bytes); + } + + getTranscript() { + return this.transcript.slice(); + } + + async deriveSecret(label, transcript = undefined) { + transcript = transcript || this.getTranscript(); + return await hkdfExpandLabel(this.secret, label, await hash(transcript), HASH_LENGTH); + } + + async calculateFinishedMAC(baseKey, transcript = undefined) { + transcript = transcript || this.getTranscript(); + const finishedKey = await hkdfExpandLabel(baseKey, 'finished', EMPTY, HASH_LENGTH); + return await hmac(finishedKey, await hash(transcript)); + } + + async verifyFinishedMAC(baseKey, mac, transcript = undefined) { + transcript = transcript || this.getTranscript(); + const finishedKey = await hkdfExpandLabel(baseKey, 'finished', EMPTY, HASH_LENGTH); + await verifyHmac(finishedKey, mac, await hash(transcript)); + } +} + +// CONCATENATED MODULE: ./src/recordlayer.js +/* 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/. */ + +// +// This file implements the "record layer" for TLS1.3, as defined in +// https://tools.ietf.org/html/rfc8446#section-5. +// +// The record layer is responsible for encrypting/decrypting bytes to be +// sent over the wire, including stateful management of sequence numbers +// for the incoming and outgoing stream. +// +// The main interface is the RecordLayer class, which takes a callback function +// sending data and can be used like so: +// +// rl = new RecordLayer(async function send_encrypted_data(data) { +// // application-specific sending logic here. +// }); +// +// // Records are sent and received in plaintext by default, +// // until you specify the key to use. +// await rl.setSendKey(key) +// +// // Send some data by specifying the record type and the bytes. +// // Where allowed by the record type, it will be buffered until +// // explicitly flushed, and then sent by calling the callback. +// await rl.send(RECORD_TYPE.HANDSHAKE, ) +// await rl.send(RECORD_TYPE.HANDSHAKE, ) +// await rl.flush() +// +// // Separate keys are used for sending and receiving. +// rl.setRecvKey(key); +// +// // When data is received, push it into the RecordLayer +// // and pass a callback that will be called with a [type, bytes] +// // pair for each message parsed from the data. +// rl.recv(dataReceivedFromPeer, async (type, bytes) => { +// switch (type) { +// case RECORD_TYPE.APPLICATION_DATA: +// // do something with application data +// case RECORD_TYPE.HANDSHAKE: +// // do something with a handshake message +// default: +// // etc... +// } +// }); +// + + + + + + + +/* eslint-disable sorting/sort-object-props */ +const RECORD_TYPE = { + CHANGE_CIPHER_SPEC: 20, + ALERT: 21, + HANDSHAKE: 22, + APPLICATION_DATA: 23, +}; +/* eslint-enable sorting/sort-object-props */ + +// Encrypting at most 2^24 records will force us to stay +// below data limits on AES-GCM encryption key use, and also +// means we can accurately represent the sequence number as +// a javascript double. +const MAX_SEQUENCE_NUMBER = Math.pow(2, 24); +const MAX_RECORD_SIZE = Math.pow(2, 14); +const MAX_ENCRYPTED_RECORD_SIZE = MAX_RECORD_SIZE + 256; +const RECORD_HEADER_SIZE = 5; + +// These are some helper classes to manage the encryption/decryption state +// for a particular key. + +class recordlayer_CipherState { + constructor(key, iv) { + this.key = key; + this.iv = iv; + this.seqnum = 0; + } + + static async create(baseKey, mode) { + // Derive key and iv per https://tools.ietf.org/html/rfc8446#section-7.3 + const key = await prepareKey(await hkdfExpandLabel(baseKey, 'key', EMPTY, KEY_LENGTH), mode); + const iv = await hkdfExpandLabel(baseKey, 'iv', EMPTY, IV_LENGTH); + return new this(key, iv); + } + + nonce() { + // Ref https://tools.ietf.org/html/rfc8446#section-5.3: + // * left-pad the sequence number with zeros to IV_LENGTH + // * xor with the provided iv + // Our sequence numbers are always less than 2^24, so fit in a Uint32 + // in the last 4 bytes of the nonce. + const nonce = this.iv.slice(); + const dv = new DataView(nonce.buffer, nonce.byteLength - 4, 4); + dv.setUint32(0, dv.getUint32(0) ^ this.seqnum); + this.seqnum += 1; + if (this.seqnum > MAX_SEQUENCE_NUMBER) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + return nonce; + } +} + +class recordlayer_EncryptionState extends recordlayer_CipherState { + static async create(key) { + return super.create(key, 'encrypt'); + } + + async encrypt(plaintext, additionalData) { + return await encrypt(this.key, this.nonce(), plaintext, additionalData); + } +} + +class recordlayer_DecryptionState extends recordlayer_CipherState { + static async create(key) { + return super.create(key, 'decrypt'); + } + + async decrypt(ciphertext, additionalData) { + return await decrypt(this.key, this.nonce(), ciphertext, additionalData); + } +} + +// The main RecordLayer class. + +class recordlayer_RecordLayer { + constructor(sendCallback) { + this.sendCallback = sendCallback; + this._sendEncryptState = null; + this._sendError = null; + this._recvDecryptState = null; + this._recvError = null; + this._pendingRecordType = 0; + this._pendingRecordBuf = null; + } + + async setSendKey(key) { + await this.flush(); + this._sendEncryptState = await recordlayer_EncryptionState.create(key); + } + + async setRecvKey(key) { + this._recvDecryptState = await recordlayer_DecryptionState.create(key); + } + + async setSendError(err) { + this._sendError = err; + } + + async setRecvError(err) { + this._recvError = err; + } + + async send(type, data) { + if (this._sendError !== null) { + throw this._sendError; + } + // Forbid sending data that doesn't fit into a single record. + // We do not support fragmentation over multiple records. + if (data.byteLength > MAX_RECORD_SIZE) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + // Flush if we're switching to a different record type. + if (this._pendingRecordType && this._pendingRecordType !== type) { + await this.flush(); + } + // Flush if we would overflow the max size of a record. + if (this._pendingRecordBuf !== null) { + if (this._pendingRecordBuf.tell() + data.byteLength > MAX_RECORD_SIZE) { + await this.flush(); + } + } + // Start a new pending record if necessary. + // We reserve space at the start of the buffer for the record header, + // which is conveniently always a fixed size. + if (this._pendingRecordBuf === null) { + this._pendingRecordType = type; + this._pendingRecordBuf = new utils_BufferWriter(); + this._pendingRecordBuf.incr(RECORD_HEADER_SIZE); + } + this._pendingRecordBuf.writeBytes(data); + } + + async flush() { + // If there's nothing to flush, bail out early. + // Don't throw `_sendError` if we're not sending anything, because `flush()` + // can be called when we're trying to transition into an error state. + const buf = this._pendingRecordBuf; + let type = this._pendingRecordType; + if (! type) { + if (buf !== null) { + throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR); + } + return; + } + if (this._sendError !== null) { + throw this._sendError; + } + // If we're encrypting, turn the existing buffer contents into a `TLSInnerPlaintext` by + // appending the type. We don't do any zero-padding, although the spec allows it. + let inflation = 0, innerPlaintext = null; + if (this._sendEncryptState !== null) { + buf.writeUint8(type); + innerPlaintext = buf.slice(RECORD_HEADER_SIZE); + inflation = AEAD_SIZE_INFLATION; + type = RECORD_TYPE.APPLICATION_DATA; + } + // Write the common header for either `TLSPlaintext` or `TLSCiphertext` record. + const length = buf.tell() - RECORD_HEADER_SIZE + inflation; + buf.seek(0); + buf.writeUint8(type); + buf.writeUint16(VERSION_TLS_1_2); + buf.writeUint16(length); + // Followed by different payload depending on encryption status. + if (this._sendEncryptState !== null) { + const additionalData = buf.slice(0, RECORD_HEADER_SIZE); + const ciphertext = await this._sendEncryptState.encrypt(innerPlaintext, additionalData); + buf.writeBytes(ciphertext); + } else { + buf.incr(length); + } + this._pendingRecordBuf = null; + this._pendingRecordType = 0; + await this.sendCallback(buf.flush()); + } + + async recv(data) { + if (this._recvError !== null) { + throw this._recvError; + } + // For simplicity, we assume that the given data contains exactly one record. + // Peers using this library will send one record at a time over the websocket + // connection, and we can assume that the server-side websocket bridge will split + // up any traffic into individual records if we ever start interoperating with + // peers using a different TLS implementation. + // Similarly, we assume that handshake messages will not be fragmented across + // multiple records. This should be trivially true for the PSK-only mode used + // by this library, but we may want to relax it in future for interoperability + // with e.g. large ClientHello messages that contain lots of different options. + const buf = new utils_BufferReader(data); + // The data to read is either a TLSPlaintext or TLSCiphertext struct, + // depending on whether record protection has been enabled yet: + // + // struct { + // ContentType type; + // ProtocolVersion legacy_record_version; + // uint16 length; + // opaque fragment[TLSPlaintext.length]; + // } TLSPlaintext; + // + // struct { + // ContentType opaque_type = application_data; /* 23 */ + // ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */ + // uint16 length; + // opaque encrypted_record[TLSCiphertext.length]; + // } TLSCiphertext; + // + let type = buf.readUint8(); + // The spec says legacy_record_version "MUST be ignored for all purposes", + // but we know TLS1.3 implementations will only ever emit two possible values, + // so it seems useful to bail out early if we receive anything else. + const version = buf.readUint16(); + if (version !== VERSION_TLS_1_2) { + // TLS1.0 is only acceptable on initial plaintext records. + if (this._recvDecryptState !== null || version !== VERSION_TLS_1_0) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + } + const length = buf.readUint16(); + let plaintext; + if (this._recvDecryptState === null || type === RECORD_TYPE.CHANGE_CIPHER_SPEC) { + [type, plaintext] = await this._readPlaintextRecord(type, length, buf); + } else { + [type, plaintext] = await this._readEncryptedRecord(type, length, buf); + } + // Sanity-check that we received exactly one record. + if (buf.hasMoreBytes()) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + return [type, plaintext]; + } + + // Helper to read an unencrypted `TLSPlaintext` struct + + async _readPlaintextRecord(type, length, buf) { + if (length > MAX_RECORD_SIZE) { + throw new TLSError(ALERT_DESCRIPTION.RECORD_OVERFLOW); + } + return [type, buf.readBytes(length)]; + } + + // Helper to read an encrypted `TLSCiphertext` struct, + // decrypting it into plaintext. + + async _readEncryptedRecord(type, length, buf) { + if (length > MAX_ENCRYPTED_RECORD_SIZE) { + throw new TLSError(ALERT_DESCRIPTION.RECORD_OVERFLOW); + } + // The outer type for encrypted records is always APPLICATION_DATA. + if (type !== RECORD_TYPE.APPLICATION_DATA) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + // Decrypt and decode the contained `TLSInnerPlaintext` struct: + // + // struct { + // opaque content[TLSPlaintext.length]; + // ContentType type; + // uint8 zeros[length_of_padding]; + // } TLSInnerPlaintext; + // + // The additional data for the decryption is the `TLSCiphertext` record + // header, which is a fixed size and immediately prior to current buffer position. + buf.incr(-RECORD_HEADER_SIZE); + const additionalData = buf.readBytes(RECORD_HEADER_SIZE); + const ciphertext = buf.readBytes(length); + const paddedPlaintext = await this._recvDecryptState.decrypt(ciphertext, additionalData); + // We have to scan backwards over the zero padding at the end of the struct + // in order to find the non-zero `type` byte. + let i; + for (i = paddedPlaintext.byteLength - 1; i >= 0; i--) { + if (paddedPlaintext[i] !== 0) { + break; + } + } + if (i < 0) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + type = paddedPlaintext[i]; + // `change_cipher_spec` records must always be plaintext. + if (type === RECORD_TYPE.CHANGE_CIPHER_SPEC) { + throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR); + } + return [type, paddedPlaintext.slice(0, i)]; + } +} + +// CONCATENATED MODULE: ./src/tlsconnection.js +/* 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/. */ + +// The top-level APIs offered by this module are `ClientConnection` and +// `ServerConnection` classes, which provide authenticated and encrypted +// communication via the "externally-provisioned PSK" mode of TLS1.3. +// They each take a callback to be used for sending data to the remote peer, +// and operate like this: +// +// conn = await ClientConnection.create(psk, pskId, async function send_data_to_server(data) { +// // application-specific sending logic here. +// }) +// +// // Send data to the server by calling `send`, +// // which will use the callback provided in the constructor. +// // A single `send()` by the application may result in multiple +// // invokations of the callback. +// +// await conn.send('application-level data') +// +// // When data is received from the server, push it into +// // the connection and let it return any decrypted app-level data. +// // There might not be any app-level data if it was a protocol control +// // message, and the receipt of the data might trigger additional calls +// // to the send callback for protocol control purposes. +// +// serverSocket.on('data', async encrypted_data => { +// const plaintext = await conn.recv(data) +// if (plaintext !== null) { +// do_something_with_app_level_data(plaintext) +// } +// }) +// +// // It's good practice to explicitly close the connection +// // when finished. This will send a "closed" notification +// // to the server. +// +// await conn.close() +// +// // When the peer sends a "closed" notification it will show up +// // as a `TLSCloseNotify` exception from recv: +// +// try { +// data = await conn.recv(data); +// } catch (err) { +// if (! (err instanceof TLSCloseNotify) { throw err } +// do_something_to_cleanly_close_data_connection(); +// } +// +// The `ServerConnection` API operates similarly; the distinction is mainly +// in which side is expected to send vs receieve during the protocol handshake. + + + + + + + + + + +class tlsconnection_Connection { + constructor(psk, pskId, sendCallback) { + this.psk = assertIsBytes(psk); + this.pskId = assertIsBytes(pskId); + this.connected = new Promise((resolve, reject) => { + this._onConnectionSuccess = resolve; + this._onConnectionFailure = reject; + }); + this._state = new UNINITIALIZED(this); + this._handshakeRecvBuffer = null; + this._hasSeenChangeCipherSpec = false; + this._recordlayer = new recordlayer_RecordLayer(sendCallback); + this._keyschedule = new keyschedule_KeySchedule(); + this._lastPromise = Promise.resolve(); + } + + // Subclasses will override this with some async initialization logic. + static async create(psk, pskId, sendCallback) { + return new this(psk, pskId, sendCallback); + } + + // These are the three public API methods that consumers can use + // to send and receive data encrypted with TLS1.3. + + async send(data) { + assertIsBytes(data); + await this.connected; + await this._synchronized(async () => { + await this._state.sendApplicationData(data); + }); + } + + async recv(data) { + assertIsBytes(data); + return await this._synchronized(async () => { + // Decrypt the data using the record layer. + // We expect to receive precisely one record at a time. + const [type, bytes] = await this._recordlayer.recv(data); + // Dispatch based on the type of the record. + switch (type) { + case RECORD_TYPE.CHANGE_CIPHER_SPEC: + await this._state.recvChangeCipherSpec(bytes); + return null; + case RECORD_TYPE.ALERT: + await this._state.recvAlertMessage(TLSAlert.fromBytes(bytes)); + return null; + case RECORD_TYPE.APPLICATION_DATA: + return await this._state.recvApplicationData(bytes); + case RECORD_TYPE.HANDSHAKE: + // Multiple handshake messages may be coalesced into a single record. + // Store the in-progress record buffer on `this` so that we can guard + // against handshake messages that span a change in keys. + this._handshakeRecvBuffer = new utils_BufferReader(bytes); + if (! this._handshakeRecvBuffer.hasMoreBytes()) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + do { + // Each handshake messages has a type and length prefix, per + // https://tools.ietf.org/html/rfc8446#appendix-B.3 + this._handshakeRecvBuffer.incr(1); + const mlength = this._handshakeRecvBuffer.readUint24(); + this._handshakeRecvBuffer.incr(-4); + const messageBytes = this._handshakeRecvBuffer.readBytes(mlength + 4); + this._keyschedule.addToTranscript(messageBytes); + await this._state.recvHandshakeMessage(messages_HandshakeMessage.fromBytes(messageBytes)); + } while (this._handshakeRecvBuffer.hasMoreBytes()); + this._handshakeRecvBuffer = null; + return null; + default: + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + }); + } + + async close() { + await this._synchronized(async () => { + await this._state.close(); + }); + } + + // Ensure that async functions execute one at a time, + // by waiting for the previous call to `_synchronized()` to complete + // before starting a new one. This helps ensure that we complete + // one state-machine transition before starting to do the next. + // It's also a convenient place to catch and alert on errors. + + _synchronized(cb) { + const nextPromise = this._lastPromise.then(() => { + return cb(); + }).catch(async err => { + if (err instanceof TLSCloseNotify) { + throw err; + } + await this._state.handleErrorAndRethrow(err); + }); + // We don't want to hold on to the return value or error, + // just synchronize on the fact that it completed. + this._lastPromise = nextPromise.then(noop, noop); + return nextPromise; + } + + // This drives internal transition of the state-machine, + // ensuring that the new state is properly initialized. + + async _transition(State, ...args) { + this._state = new State(this); + await this._state.initialize(...args); + await this._recordlayer.flush(); + } + + // These are helpers to allow the State to manipulate the recordlayer + // and send out various types of data. + + async _sendApplicationData(bytes) { + await this._recordlayer.send(RECORD_TYPE.APPLICATION_DATA, bytes); + await this._recordlayer.flush(); + } + + async _sendHandshakeMessage(msg) { + await this._sendHandshakeMessageBytes(msg.toBytes()); + } + + async _sendHandshakeMessageBytes(bytes) { + this._keyschedule.addToTranscript(bytes); + await this._recordlayer.send(RECORD_TYPE.HANDSHAKE, bytes); + // Don't flush after each handshake message, since we can probably + // coalesce multiple messages into a single record. + } + + async _sendAlertMessage(err) { + await this._recordlayer.send(RECORD_TYPE.ALERT, err.toBytes()); + await this._recordlayer.flush(); + } + + async _sendChangeCipherSpec() { + await this._recordlayer.send(RECORD_TYPE.CHANGE_CIPHER_SPEC, new Uint8Array([0x01])); + await this._recordlayer.flush(); + } + + async _setSendKey(key) { + return await this._recordlayer.setSendKey(key); + } + + async _setRecvKey(key) { + // Handshake messages that change keys must be on a record boundary. + if (this._handshakeRecvBuffer && this._handshakeRecvBuffer.hasMoreBytes()) { + throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE); + } + return await this._recordlayer.setRecvKey(key); + } + + _setConnectionSuccess() { + if (this._onConnectionSuccess !== null) { + this._onConnectionSuccess(); + this._onConnectionSuccess = null; + this._onConnectionFailure = null; + } + } + + _setConnectionFailure(err) { + if (this._onConnectionFailure !== null) { + this._onConnectionFailure(err); + this._onConnectionSuccess = null; + this._onConnectionFailure = null; + } + } + + _closeForSend(alert) { + this._recordlayer.setSendError(alert); + } + + _closeForRecv(alert) { + this._recordlayer.setRecvError(alert); + } +} + +class tlsconnection_ClientConnection extends tlsconnection_Connection { + static async create(psk, pskId, sendCallback) { + const instance = await super.create(psk, pskId, sendCallback); + await instance._transition(states_CLIENT_START); + return instance; + } +} + +class tlsconnection_ServerConnection extends tlsconnection_Connection { + static async create(psk, pskId, sendCallback) { + const instance = await super.create(psk, pskId, sendCallback); + await instance._transition(states_SERVER_START); + return instance; + } +} + +// CONCATENATED MODULE: ./node_modules/event-target-shim/dist/event-target-shim.mjs +/** + * @author Toru Nagashima + * @copyright 2015 Toru Nagashima. All rights reserved. + * See LICENSE file in root directory for full license. + */ +/** + * @typedef {object} PrivateData + * @property {EventTarget} eventTarget The event target. + * @property {{type:string}} event The original event object. + * @property {number} eventPhase The current event phase. + * @property {EventTarget|null} currentTarget The current event target. + * @property {boolean} canceled The flag to prevent default. + * @property {boolean} stopped The flag to stop propagation. + * @property {boolean} immediateStopped The flag to stop propagation immediately. + * @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null. + * @property {number} timeStamp The unix time. + * @private + */ + +/** + * Private data for event wrappers. + * @type {WeakMap} + * @private + */ +const privateData = new WeakMap(); + +/** + * Cache for wrapper classes. + * @type {WeakMap} + * @private + */ +const wrappers = new WeakMap(); + +/** + * Get private data. + * @param {Event} event The event object to get private data. + * @returns {PrivateData} The private data of the event. + * @private + */ +function pd(event) { + const retv = privateData.get(event); + console.assert( + retv != null, + "'this' is expected an Event object, but got", + event + ); + return retv +} + +/** + * https://dom.spec.whatwg.org/#set-the-canceled-flag + * @param data {PrivateData} private data. + */ +function setCancelFlag(data) { + if (data.passiveListener != null) { + if ( + typeof console !== "undefined" && + typeof console.error === "function" + ) { + console.error( + "Unable to preventDefault inside passive event listener invocation.", + data.passiveListener + ); + } + return + } + if (!data.event.cancelable) { + return + } + + data.canceled = true; + if (typeof data.event.preventDefault === "function") { + data.event.preventDefault(); + } +} + +/** + * @see https://dom.spec.whatwg.org/#interface-event + * @private + */ +/** + * The event wrapper. + * @constructor + * @param {EventTarget} eventTarget The event target of this dispatching. + * @param {Event|{type:string}} event The original event to wrap. + */ +function Event(eventTarget, event) { + privateData.set(this, { + eventTarget, + event, + eventPhase: 2, + currentTarget: eventTarget, + canceled: false, + stopped: false, + immediateStopped: false, + passiveListener: null, + timeStamp: event.timeStamp || Date.now(), + }); + + // https://heycam.github.io/webidl/#Unforgeable + Object.defineProperty(this, "isTrusted", { value: false, enumerable: true }); + + // Define accessors + const keys = Object.keys(event); + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (!(key in this)) { + Object.defineProperty(this, key, defineRedirectDescriptor(key)); + } + } +} + +// Should be enumerable, but class methods are not enumerable. +Event.prototype = { + /** + * The type of this event. + * @type {string} + */ + get type() { + return pd(this).event.type + }, + + /** + * The target of this event. + * @type {EventTarget} + */ + get target() { + return pd(this).eventTarget + }, + + /** + * The target of this event. + * @type {EventTarget} + */ + get currentTarget() { + return pd(this).currentTarget + }, + + /** + * @returns {EventTarget[]} The composed path of this event. + */ + composedPath() { + const currentTarget = pd(this).currentTarget; + if (currentTarget == null) { + return [] + } + return [currentTarget] + }, + + /** + * Constant of NONE. + * @type {number} + */ + get NONE() { + return 0 + }, + + /** + * Constant of CAPTURING_PHASE. + * @type {number} + */ + get CAPTURING_PHASE() { + return 1 + }, + + /** + * Constant of AT_TARGET. + * @type {number} + */ + get AT_TARGET() { + return 2 + }, + + /** + * Constant of BUBBLING_PHASE. + * @type {number} + */ + get BUBBLING_PHASE() { + return 3 + }, + + /** + * The target of this event. + * @type {number} + */ + get eventPhase() { + return pd(this).eventPhase + }, + + /** + * Stop event bubbling. + * @returns {void} + */ + stopPropagation() { + const data = pd(this); + + data.stopped = true; + if (typeof data.event.stopPropagation === "function") { + data.event.stopPropagation(); + } + }, + + /** + * Stop event bubbling. + * @returns {void} + */ + stopImmediatePropagation() { + const data = pd(this); + + data.stopped = true; + data.immediateStopped = true; + if (typeof data.event.stopImmediatePropagation === "function") { + data.event.stopImmediatePropagation(); + } + }, + + /** + * The flag to be bubbling. + * @type {boolean} + */ + get bubbles() { + return Boolean(pd(this).event.bubbles) + }, + + /** + * The flag to be cancelable. + * @type {boolean} + */ + get cancelable() { + return Boolean(pd(this).event.cancelable) + }, + + /** + * Cancel this event. + * @returns {void} + */ + preventDefault() { + setCancelFlag(pd(this)); + }, + + /** + * The flag to indicate cancellation state. + * @type {boolean} + */ + get defaultPrevented() { + return pd(this).canceled + }, + + /** + * The flag to be composed. + * @type {boolean} + */ + get composed() { + return Boolean(pd(this).event.composed) + }, + + /** + * The unix time of this event. + * @type {number} + */ + get timeStamp() { + return pd(this).timeStamp + }, + + /** + * The target of this event. + * @type {EventTarget} + * @deprecated + */ + get srcElement() { + return pd(this).eventTarget + }, + + /** + * The flag to stop event bubbling. + * @type {boolean} + * @deprecated + */ + get cancelBubble() { + return pd(this).stopped + }, + set cancelBubble(value) { + if (!value) { + return + } + const data = pd(this); + + data.stopped = true; + if (typeof data.event.cancelBubble === "boolean") { + data.event.cancelBubble = true; + } + }, + + /** + * The flag to indicate cancellation state. + * @type {boolean} + * @deprecated + */ + get returnValue() { + return !pd(this).canceled + }, + set returnValue(value) { + if (!value) { + setCancelFlag(pd(this)); + } + }, + + /** + * Initialize this event object. But do nothing under event dispatching. + * @param {string} type The event type. + * @param {boolean} [bubbles=false] The flag to be possible to bubble up. + * @param {boolean} [cancelable=false] The flag to be possible to cancel. + * @deprecated + */ + initEvent() { + // Do nothing. + }, +}; + +// `constructor` is not enumerable. +Object.defineProperty(Event.prototype, "constructor", { + value: Event, + configurable: true, + writable: true, +}); + +// Ensure `event instanceof window.Event` is `true`. +if (typeof window !== "undefined" && typeof window.Event !== "undefined") { + Object.setPrototypeOf(Event.prototype, window.Event.prototype); + + // Make association for wrappers. + wrappers.set(window.Event.prototype, Event); +} + +/** + * Get the property descriptor to redirect a given property. + * @param {string} key Property name to define property descriptor. + * @returns {PropertyDescriptor} The property descriptor to redirect the property. + * @private + */ +function defineRedirectDescriptor(key) { + return { + get() { + return pd(this).event[key] + }, + set(value) { + pd(this).event[key] = value; + }, + configurable: true, + enumerable: true, + } +} + +/** + * Get the property descriptor to call a given method property. + * @param {string} key Property name to define property descriptor. + * @returns {PropertyDescriptor} The property descriptor to call the method property. + * @private + */ +function defineCallDescriptor(key) { + return { + value() { + const event = pd(this).event; + return event[key].apply(event, arguments) + }, + configurable: true, + enumerable: true, + } +} + +/** + * Define new wrapper class. + * @param {Function} BaseEvent The base wrapper class. + * @param {Object} proto The prototype of the original event. + * @returns {Function} The defined wrapper class. + * @private + */ +function defineWrapper(BaseEvent, proto) { + const keys = Object.keys(proto); + if (keys.length === 0) { + return BaseEvent + } + + /** CustomEvent */ + function CustomEvent(eventTarget, event) { + BaseEvent.call(this, eventTarget, event); + } + + CustomEvent.prototype = Object.create(BaseEvent.prototype, { + constructor: { value: CustomEvent, configurable: true, writable: true }, + }); + + // Define accessors. + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (!(key in BaseEvent.prototype)) { + const descriptor = Object.getOwnPropertyDescriptor(proto, key); + const isFunc = typeof descriptor.value === "function"; + Object.defineProperty( + CustomEvent.prototype, + key, + isFunc + ? defineCallDescriptor(key) + : defineRedirectDescriptor(key) + ); + } + } + + return CustomEvent +} + +/** + * Get the wrapper class of a given prototype. + * @param {Object} proto The prototype of the original event to get its wrapper. + * @returns {Function} The wrapper class. + * @private + */ +function getWrapper(proto) { + if (proto == null || proto === Object.prototype) { + return Event + } + + let wrapper = wrappers.get(proto); + if (wrapper == null) { + wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto); + wrappers.set(proto, wrapper); + } + return wrapper +} + +/** + * Wrap a given event to management a dispatching. + * @param {EventTarget} eventTarget The event target of this dispatching. + * @param {Object} event The event to wrap. + * @returns {Event} The wrapper instance. + * @private + */ +function wrapEvent(eventTarget, event) { + const Wrapper = getWrapper(Object.getPrototypeOf(event)); + return new Wrapper(eventTarget, event) +} + +/** + * Get the immediateStopped flag of a given event. + * @param {Event} event The event to get. + * @returns {boolean} The flag to stop propagation immediately. + * @private + */ +function isStopped(event) { + return pd(event).immediateStopped +} + +/** + * Set the current event phase of a given event. + * @param {Event} event The event to set current target. + * @param {number} eventPhase New event phase. + * @returns {void} + * @private + */ +function setEventPhase(event, eventPhase) { + pd(event).eventPhase = eventPhase; +} + +/** + * Set the current target of a given event. + * @param {Event} event The event to set current target. + * @param {EventTarget|null} currentTarget New current target. + * @returns {void} + * @private + */ +function setCurrentTarget(event, currentTarget) { + pd(event).currentTarget = currentTarget; +} + +/** + * Set a passive listener of a given event. + * @param {Event} event The event to set current target. + * @param {Function|null} passiveListener New passive listener. + * @returns {void} + * @private + */ +function setPassiveListener(event, passiveListener) { + pd(event).passiveListener = passiveListener; +} + +/** + * @typedef {object} ListenerNode + * @property {Function} listener + * @property {1|2|3} listenerType + * @property {boolean} passive + * @property {boolean} once + * @property {ListenerNode|null} next + * @private + */ + +/** + * @type {WeakMap>} + * @private + */ +const listenersMap = new WeakMap(); + +// Listener types +const CAPTURE = 1; +const BUBBLE = 2; +const ATTRIBUTE = 3; + +/** + * Check whether a given value is an object or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if the value is an object. + */ +function isObject(x) { + return x !== null && typeof x === "object" //eslint-disable-line no-restricted-syntax +} + +/** + * Get listeners. + * @param {EventTarget} eventTarget The event target to get. + * @returns {Map} The listeners. + * @private + */ +function getListeners(eventTarget) { + const listeners = listenersMap.get(eventTarget); + if (listeners == null) { + throw new TypeError( + "'this' is expected an EventTarget object, but got another value." + ) + } + return listeners +} + +/** + * Get the property descriptor for the event attribute of a given event. + * @param {string} eventName The event name to get property descriptor. + * @returns {PropertyDescriptor} The property descriptor. + * @private + */ +function defineEventAttributeDescriptor(eventName) { + return { + get() { + const listeners = getListeners(this); + let node = listeners.get(eventName); + while (node != null) { + if (node.listenerType === ATTRIBUTE) { + return node.listener + } + node = node.next; + } + return null + }, + + set(listener) { + if (typeof listener !== "function" && !isObject(listener)) { + listener = null; // eslint-disable-line no-param-reassign + } + const listeners = getListeners(this); + + // Traverse to the tail while removing old value. + let prev = null; + let node = listeners.get(eventName); + while (node != null) { + if (node.listenerType === ATTRIBUTE) { + // Remove old value. + if (prev !== null) { + prev.next = node.next; + } else if (node.next !== null) { + listeners.set(eventName, node.next); + } else { + listeners.delete(eventName); + } + } else { + prev = node; + } + + node = node.next; + } + + // Add new value. + if (listener !== null) { + const newNode = { + listener, + listenerType: ATTRIBUTE, + passive: false, + once: false, + next: null, + }; + if (prev === null) { + listeners.set(eventName, newNode); + } else { + prev.next = newNode; + } + } + }, + configurable: true, + enumerable: true, + } +} + +/** + * Define an event attribute (e.g. `eventTarget.onclick`). + * @param {Object} eventTargetPrototype The event target prototype to define an event attrbite. + * @param {string} eventName The event name to define. + * @returns {void} + */ +function defineEventAttribute(eventTargetPrototype, eventName) { + Object.defineProperty( + eventTargetPrototype, + `on${eventName}`, + defineEventAttributeDescriptor(eventName) + ); +} + +/** + * Define a custom EventTarget with event attributes. + * @param {string[]} eventNames Event names for event attributes. + * @returns {EventTarget} The custom EventTarget. + * @private + */ +function defineCustomEventTarget(eventNames) { + /** CustomEventTarget */ + function CustomEventTarget() { + EventTarget.call(this); + } + + CustomEventTarget.prototype = Object.create(EventTarget.prototype, { + constructor: { + value: CustomEventTarget, + configurable: true, + writable: true, + }, + }); + + for (let i = 0; i < eventNames.length; ++i) { + defineEventAttribute(CustomEventTarget.prototype, eventNames[i]); + } + + return CustomEventTarget +} + +/** + * EventTarget. + * + * - This is constructor if no arguments. + * - This is a function which returns a CustomEventTarget constructor if there are arguments. + * + * For example: + * + * class A extends EventTarget {} + * class B extends EventTarget("message") {} + * class C extends EventTarget("message", "error") {} + * class D extends EventTarget(["message", "error"]) {} + */ +function EventTarget() { + /*eslint-disable consistent-return */ + if (this instanceof EventTarget) { + listenersMap.set(this, new Map()); + return + } + if (arguments.length === 1 && Array.isArray(arguments[0])) { + return defineCustomEventTarget(arguments[0]) + } + if (arguments.length > 0) { + const types = new Array(arguments.length); + for (let i = 0; i < arguments.length; ++i) { + types[i] = arguments[i]; + } + return defineCustomEventTarget(types) + } + throw new TypeError("Cannot call a class as a function") + /*eslint-enable consistent-return */ +} + +// Should be enumerable, but class methods are not enumerable. +EventTarget.prototype = { + /** + * Add a given listener to this event target. + * @param {string} eventName The event name to add. + * @param {Function} listener The listener to add. + * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. + * @returns {void} + */ + addEventListener(eventName, listener, options) { + if (listener == null) { + return + } + if (typeof listener !== "function" && !isObject(listener)) { + throw new TypeError("'listener' should be a function or an object.") + } + + const listeners = getListeners(this); + const optionsIsObj = isObject(options); + const capture = optionsIsObj + ? Boolean(options.capture) + : Boolean(options); + const listenerType = capture ? CAPTURE : BUBBLE; + const newNode = { + listener, + listenerType, + passive: optionsIsObj && Boolean(options.passive), + once: optionsIsObj && Boolean(options.once), + next: null, + }; + + // Set it as the first node if the first node is null. + let node = listeners.get(eventName); + if (node === undefined) { + listeners.set(eventName, newNode); + return + } + + // Traverse to the tail while checking duplication.. + let prev = null; + while (node != null) { + if ( + node.listener === listener && + node.listenerType === listenerType + ) { + // Should ignore duplication. + return + } + prev = node; + node = node.next; + } + + // Add it. + prev.next = newNode; + }, + + /** + * Remove a given listener from this event target. + * @param {string} eventName The event name to remove. + * @param {Function} listener The listener to remove. + * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. + * @returns {void} + */ + removeEventListener(eventName, listener, options) { + if (listener == null) { + return + } + + const listeners = getListeners(this); + const capture = isObject(options) + ? Boolean(options.capture) + : Boolean(options); + const listenerType = capture ? CAPTURE : BUBBLE; + + let prev = null; + let node = listeners.get(eventName); + while (node != null) { + if ( + node.listener === listener && + node.listenerType === listenerType + ) { + if (prev !== null) { + prev.next = node.next; + } else if (node.next !== null) { + listeners.set(eventName, node.next); + } else { + listeners.delete(eventName); + } + return + } + + prev = node; + node = node.next; + } + }, + + /** + * Dispatch a given event. + * @param {Event|{type:string}} event The event to dispatch. + * @returns {boolean} `false` if canceled. + */ + dispatchEvent(event) { + if (event == null || typeof event.type !== "string") { + throw new TypeError('"event.type" should be a string.') + } + + // If listeners aren't registered, terminate. + const listeners = getListeners(this); + const eventName = event.type; + let node = listeners.get(eventName); + if (node == null) { + return true + } + + // Since we cannot rewrite several properties, so wrap object. + const wrappedEvent = wrapEvent(this, event); + + // This doesn't process capturing phase and bubbling phase. + // This isn't participating in a tree. + let prev = null; + while (node != null) { + // Remove this listener if it's once + if (node.once) { + if (prev !== null) { + prev.next = node.next; + } else if (node.next !== null) { + listeners.set(eventName, node.next); + } else { + listeners.delete(eventName); + } + } else { + prev = node; + } + + // Call this listener + setPassiveListener( + wrappedEvent, + node.passive ? node.listener : null + ); + if (typeof node.listener === "function") { + try { + node.listener.call(this, wrappedEvent); + } catch (err) { + if ( + typeof console !== "undefined" && + typeof console.error === "function" + ) { + console.error(err); + } + } + } else if ( + node.listenerType !== ATTRIBUTE && + typeof node.listener.handleEvent === "function" + ) { + node.listener.handleEvent(wrappedEvent); + } + + // Break if `event.stopImmediatePropagation` was called. + if (isStopped(wrappedEvent)) { + break + } + + node = node.next; + } + setPassiveListener(wrappedEvent, null); + setEventPhase(wrappedEvent, 0); + setCurrentTarget(wrappedEvent, null); + + return !wrappedEvent.defaultPrevented + }, +}; + +// `constructor` is not enumerable. +Object.defineProperty(EventTarget.prototype, "constructor", { + value: EventTarget, + configurable: true, + writable: true, +}); + +// Ensure `eventTarget instanceof window.EventTarget` is `true`. +if ( + typeof window !== "undefined" && + typeof window.EventTarget !== "undefined" +) { + Object.setPrototypeOf(EventTarget.prototype, window.EventTarget.prototype); +} + +/* harmony default export */ var event_target_shim = (EventTarget); + + +// CONCATENATED MODULE: ./src/index.js +/* 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/. */ + +// A wrapper that combines a WebSocket to the channelserver +// with some client-side encryption for securing the channel. +// +// This code is responsible for the event handling and the consumer API. +// All the details of encrypting the messages are delegated to`./tlsconnection.js`. + + + + + + + +const CLOSE_FLUSH_BUFFER_INTERVAL_MS = 200; +const CLOSE_FLUSH_BUFFER_MAX_TRIES = 5; + +class src_PairingChannel extends EventTarget { + constructor(channelId, channelKey, socket, connection) { + super(); + this._channelId = channelId; + this._channelKey = channelKey; + this._socket = socket; + this._connection = connection; + this._selfClosed = false; + this._peerClosed = false; + this._setupListeners(); + } + + /** + * Create a new pairing channel. + * + * This will open a channel on the channelserver, and generate a random client-side + * encryption key. When the promise resolves, `this.channelId` and `this.channelKey` + * can be transferred to another client to allow it to securely connect to the channel. + * + * @returns Promise + */ + static create(channelServerURI) { + const wsURI = new URL('/v1/ws/', channelServerURI).href; + const channelKey = crypto.getRandomValues(new Uint8Array(32)); + // The one who creates the channel plays the role of 'server' in the underlying TLS exchange. + return this._makePairingChannel(wsURI, tlsconnection_ServerConnection, channelKey); + } + + /** + * Connect to an existing pairing channel. + * + * This will connect to a channel on the channelserver previously established by + * another client calling `create`. The `channelId` and `channelKey` must have been + * obtained via some out-of-band mechanism (such as by scanning from a QR code). + * + * @returns Promise + */ + static connect(channelServerURI, channelId, channelKey) { + const wsURI = new URL(`/v1/ws/${channelId}`, channelServerURI).href; + // The one who connects to an existing channel plays the role of 'client' + // in the underlying TLS exchange. + return this._makePairingChannel(wsURI, tlsconnection_ClientConnection, channelKey); + } + + static _makePairingChannel(wsUri, ConnectionClass, psk) { + const socket = new WebSocket(wsUri); + return new Promise((resolve, reject) => { + // eslint-disable-next-line prefer-const + let stopListening; + const onConnectionError = async () => { + stopListening(); + reject(new Error('Error while creating the pairing channel')); + }; + const onFirstMessage = async event => { + stopListening(); + try { + // The channelserver echos back the channel id, and we use it as an + // additional input to the TLS handshake via the "psk id" field. + const {channelid: channelId} = JSON.parse(event.data); + const pskId = utf8ToBytes(channelId); + const connection = await ConnectionClass.create(psk, pskId, data => { + // Send data by forwarding it via the channelserver websocket. + // The TLS connection gives us `data` as raw bytes, but channelserver + // expects b64urlsafe strings, because it wraps them in a JSON object envelope. + socket.send(bytesToBase64url(data)); + }); + const instance = new this(channelId, psk, socket, connection); + resolve(instance); + } catch (err) { + reject(err); + } + }; + stopListening = () => { + socket.removeEventListener('close', onConnectionError); + socket.removeEventListener('error', onConnectionError); + socket.removeEventListener('message', onFirstMessage); + }; + socket.addEventListener('close', onConnectionError); + socket.addEventListener('error', onConnectionError); + socket.addEventListener('message', onFirstMessage); + }); + } + + _setupListeners() { + this._socket.addEventListener('message', async event => { + try { + // When we receive data from the channelserver, pump it through the TLS connection + // to decrypt it, then echo it back out to consumers as an event. + const channelServerEnvelope = JSON.parse(event.data); + const payload = await this._connection.recv(base64urlToBytes(channelServerEnvelope.message)); + if (payload !== null) { + const data = JSON.parse(bytesToUtf8(payload)); + this.dispatchEvent(new CustomEvent('message', { + detail: { + data, + sender: channelServerEnvelope.sender, + }, + })); + } + } catch (error) { + let event; + // The underlying TLS connection will signal a clean shutdown of the channel + // by throwing a special error, because it doesn't really have a better + // signally mechanism available. + if (error instanceof TLSCloseNotify) { + this._peerClosed = true; + if (this._selfClosed) { + this._shutdown(); + } + event = new CustomEvent('close'); + } else { + event = new CustomEvent('error', { + detail: { + error, + } + }); + } + this.dispatchEvent(event); + } + }); + // Relay the WebSocket events. + this._socket.addEventListener('error', () => { + this._shutdown(); + // The dispatched event that we receive has no useful information. + this.dispatchEvent(new CustomEvent('error', { + detail: { + error: new Error('WebSocket error.'), + }, + })); + }); + // In TLS, the peer has to explicitly send a close notification, + // which we dispatch above. Unexpected socket close is an error. + this._socket.addEventListener('close', () => { + this._shutdown(); + if (! this._peerClosed) { + this.dispatchEvent(new CustomEvent('error', { + detail: { + error: new Error('WebSocket unexpectedly closed'), + } + })); + } + }); + } + + /** + * @param {Object} data + */ + async send(data) { + const payload = utf8ToBytes(JSON.stringify(data)); + await this._connection.send(payload); + } + + async close() { + this._selfClosed = true; + await this._connection.close(); + try { + // Ensure all queued bytes have been sent before closing the connection. + let tries = 0; + while (this._socket.bufferedAmount > 0) { + if (++tries > CLOSE_FLUSH_BUFFER_MAX_TRIES) { + throw new Error('Could not flush the outgoing buffer in time.'); + } + await new Promise(res => setTimeout(res, CLOSE_FLUSH_BUFFER_INTERVAL_MS)); + } + } finally { + // If the peer hasn't closed, we might still receive some data. + if (this._peerClosed) { + this._shutdown(); + } + } + } + + _shutdown() { + if (this._socket) { + this._socket.close(); + this._socket = null; + this._connection = null; + } + } + + get closed() { + return (! this._socket) || (this._socket.readyState === 3); + } + + get channelId() { + return this._channelId; + } + + get channelKey() { + return this._channelKey; + } +} + +// Re-export helpful utilities for calling code to use. + + +// For running tests using the built bundle, +// expose a bunch of implementation details. + + + + + + + +const _internals = { + arrayToBytes: arrayToBytes, + BufferReader: utils_BufferReader, + BufferWriter: utils_BufferWriter, + bytesAreEqual: bytesAreEqual, + bytesToHex: bytesToHex, + bytesToUtf8: bytesToUtf8, + ClientConnection: tlsconnection_ClientConnection, + Connection: tlsconnection_Connection, + DecryptionState: recordlayer_DecryptionState, + EncryptedExtensions: EncryptedExtensions, + EncryptionState: recordlayer_EncryptionState, + Finished: messages_Finished, + HASH_LENGTH: HASH_LENGTH, + hexToBytes: hexToBytes, + hkdfExpand: hkdfExpand, + KeySchedule: keyschedule_KeySchedule, + NewSessionTicket: messages_NewSessionTicket, + RecordLayer: recordlayer_RecordLayer, + ServerConnection: tlsconnection_ServerConnection, + utf8ToBytes: utf8ToBytes, + zeros: zeros, +}; + + +/***/ }) +/******/ ])["PairingChannel"]; diff --git a/services/fxaccounts/FxAccountsProfile.sys.mjs b/services/fxaccounts/FxAccountsProfile.sys.mjs new file mode 100644 index 0000000000..de8bdb2f0e --- /dev/null +++ b/services/fxaccounts/FxAccountsProfile.sys.mjs @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Firefox Accounts Profile helper. + * + * This class abstracts interaction with the profile server for an account. + * It will handle things like fetching profile data, listening for updates to + * the user's profile in open browser tabs, and cacheing/invalidating profile data. + */ + +import { + ON_PROFILE_CHANGE_NOTIFICATION, + log, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs"; + +const fxAccounts = getFxAccountsSingleton(); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FxAccountsProfileClient: + "resource://gre/modules/FxAccountsProfileClient.sys.mjs", +}); + +export var FxAccountsProfile = function (options = {}) { + this._currentFetchPromise = null; + this._cachedAt = 0; // when we saved the cached version. + this._isNotifying = false; // are we sending a notification? + this.fxai = options.fxai || fxAccounts._internal; + this.client = + options.profileClient || + new lazy.FxAccountsProfileClient({ + fxai: this.fxai, + serverURL: options.profileServerUrl, + }); + + // An observer to invalidate our _cachedAt optimization. We use a weak-ref + // just incase this.tearDown isn't called in some cases. + Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true); + // for testing + if (options.channel) { + this.channel = options.channel; + } +}; + +FxAccountsProfile.prototype = { + // If we get subsequent requests for a profile within this period, don't bother + // making another request to determine if it is fresh or not. + PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes + + observe(subject, topic, data) { + // If we get a profile change notification from our webchannel it means + // the user has just changed their profile via the web, so we want to + // ignore our "freshness threshold" + if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) { + log.debug("FxAccountsProfile observed profile change"); + this._cachedAt = 0; + } + }, + + tearDown() { + this.fxai = null; + this.client = null; + Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION); + }, + + _notifyProfileChange(uid) { + this._isNotifying = true; + Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid); + this._isNotifying = false; + }, + + // Cache fetched data and send out a notification so that UI can update. + _cacheProfile(response) { + return this.fxai.withCurrentAccountState(async state => { + const profile = response.body; + const userData = await state.getUserAccountData(); + if (profile.uid != userData.uid) { + throw new Error( + "The fetched profile does not correspond with the current account." + ); + } + let profileCache = { + profile, + etag: response.etag, + }; + await state.updateUserAccountData({ profileCache }); + if (profile.email != userData.email) { + await this.fxai._handleEmailUpdated(profile.email); + } + log.debug("notifying profile changed for user ${uid}", userData); + this._notifyProfileChange(userData.uid); + return profile; + }); + }, + + async _getProfileCache() { + let data = await this.fxai.currentAccountState.getUserAccountData([ + "profileCache", + ]); + return data ? data.profileCache : null; + }, + + async _fetchAndCacheProfileInternal() { + try { + const profileCache = await this._getProfileCache(); + const etag = profileCache ? profileCache.etag : null; + let response; + try { + response = await this.client.fetchProfile(etag); + } catch (err) { + await this.fxai._handleTokenError(err); + // _handleTokenError always re-throws. + throw new Error("not reached!"); + } + + // response may be null if the profile was not modified (same ETag). + if (!response) { + return null; + } + return await this._cacheProfile(response); + } finally { + this._cachedAt = Date.now(); + this._currentFetchPromise = null; + } + }, + + _fetchAndCacheProfile() { + if (!this._currentFetchPromise) { + this._currentFetchPromise = this._fetchAndCacheProfileInternal(); + } + return this._currentFetchPromise; + }, + + // Returns cached data right away if available, otherwise returns null - if + // it returns null, or if the profile is possibly stale, it attempts to + // fetch the latest profile data in the background. After data is fetched a + // notification will be sent out if the profile has changed. + async getProfile() { + const profileCache = await this._getProfileCache(); + if (!profileCache) { + // fetch and cache it in the background. + this._fetchAndCacheProfile().catch(err => { + log.error("Background refresh of initial profile failed", err); + }); + return null; + } + if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) { + // Note that _fetchAndCacheProfile isn't returned, so continues + // in the background. + this._fetchAndCacheProfile().catch(err => { + log.error("Background refresh of profile failed", err); + }); + } else { + log.trace("not checking freshness of profile as it remains recent"); + } + return profileCache.profile; + }, + + // Get the user's profile data, fetching from the network if necessary. + // Most callers should instead use `getProfile()`; this methods exists to support + // callers who need to await the underlying network request. + async ensureProfile({ staleOk = false, forceFresh = false } = {}) { + if (staleOk && forceFresh) { + throw new Error("contradictory options specified"); + } + const profileCache = await this._getProfileCache(); + if ( + forceFresh || + !profileCache || + (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD && + !staleOk) + ) { + const profile = await this._fetchAndCacheProfile().catch(err => { + log.error("Background refresh of profile failed", err); + }); + if (profile) { + return profile; + } + } + log.trace("not checking freshness of profile as it remains recent"); + return profileCache ? profileCache.profile : null; + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), +}; diff --git a/services/fxaccounts/FxAccountsProfileClient.sys.mjs b/services/fxaccounts/FxAccountsProfileClient.sys.mjs new file mode 100644 index 0000000000..7ae1bd95db --- /dev/null +++ b/services/fxaccounts/FxAccountsProfileClient.sys.mjs @@ -0,0 +1,273 @@ +/* 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/. */ + +/** + * A client to fetch profile information for a Firefox Account. + */ +"use strict;"; + +import { + ERRNO_NETWORK, + ERRNO_PARSE, + ERRNO_UNKNOWN_ERROR, + ERROR_CODE_METHOD_NOT_ALLOWED, + ERROR_MSG_METHOD_NOT_ALLOWED, + ERROR_NETWORK, + ERROR_PARSE, + ERROR_UNKNOWN, + log, + SCOPE_PROFILE, + SCOPE_PROFILE_WRITE, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs"; + +const fxAccounts = getFxAccountsSingleton(); +import { RESTRequest } from "resource://services-common/rest.sys.mjs"; + +/** + * Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information. + * + * @param {Object} options Options + * @param {String} options.serverURL + * The URL of the profile server to query. + * Example: https://profile.accounts.firefox.com/v1 + * @param {String} options.token + * The bearer token to access the profile server + * @constructor + */ +export var FxAccountsProfileClient = function (options) { + if (!options || !options.serverURL) { + throw new Error("Missing 'serverURL' configuration option"); + } + + this.fxai = options.fxai || fxAccounts._internal; + + try { + this.serverURL = new URL(options.serverURL); + } catch (e) { + throw new Error("Invalid 'serverURL'"); + } + log.debug("FxAccountsProfileClient: Initialized"); +}; + +FxAccountsProfileClient.prototype = { + /** + * {nsIURI} + * The server to fetch profile information from. + */ + serverURL: null, + + /** + * Interface for making remote requests. + */ + _Request: RESTRequest, + + /** + * Remote request helper which abstracts authentication away. + * + * @param {String} path + * Profile server path, i.e "/profile". + * @param {String} [method] + * Type of request, e.g. "GET". + * @param {String} [etag] + * Optional ETag used for caching purposes. + * @param {Object} [body] + * Optional request body, to be sent as application/json. + * @return Promise + * Resolves: {body: Object, etag: Object} Successful response from the Profile server. + * Rejects: {FxAccountsProfileClientError} Profile client error. + * @private + */ + async _createRequest(path, method = "GET", etag = null, body = null) { + method = method.toUpperCase(); + let token = await this._getTokenForRequest(method); + try { + return await this._rawRequest(path, method, token, etag, body); + } catch (ex) { + if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) { + throw ex; + } + // it's an auth error - assume our token expired and retry. + log.info( + "Fetching the profile returned a 401 - revoking our token and retrying" + ); + await this.fxai.removeCachedOAuthToken({ token }); + token = await this._getTokenForRequest(method); + // and try with the new token - if that also fails then we fail after + // revoking the token. + try { + return await this._rawRequest(path, method, token, etag, body); + } catch (ex) { + if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) { + throw ex; + } + log.info( + "Retry fetching the profile still returned a 401 - revoking our token and failing" + ); + await this.fxai.removeCachedOAuthToken({ token }); + throw ex; + } + } + }, + + /** + * Helper to get an OAuth token for a request. + * + * OAuth tokens are cached, so it's fine to call this for each request. + * + * @param {String} [method] + * Type of request, i.e "GET". + * @return Promise + * Resolves: Object containing "scope", "token" and "key" properties + * Rejects: {FxAccountsProfileClientError} Profile client error. + * @private + */ + async _getTokenForRequest(method) { + let scope = SCOPE_PROFILE; + if (method === "POST") { + scope = SCOPE_PROFILE_WRITE; + } + return this.fxai.getOAuthToken({ scope }); + }, + + /** + * Remote "raw" request helper - doesn't handle auth errors and tokens. + * + * @param {String} path + * Profile server path, i.e "/profile". + * @param {String} method + * Type of request, i.e "GET". + * @param {String} token + * @param {String} etag + * @param {Object} payload + * The payload of the request, if any. + * @return Promise + * Resolves: {body: Object, etag: Object} Successful response from the Profile server + or null if 304 is hit (same ETag). + * Rejects: {FxAccountsProfileClientError} Profile client error. + * @private + */ + async _rawRequest(path, method, token, etag = null, payload = null) { + let profileDataUrl = this.serverURL + path; + let request = new this._Request(profileDataUrl); + + request.setHeader("Authorization", "Bearer " + token); + request.setHeader("Accept", "application/json"); + if (etag) { + request.setHeader("If-None-Match", etag); + } + + if (method != "GET" && method != "POST") { + // method not supported + throw new FxAccountsProfileClientError({ + error: ERROR_NETWORK, + errno: ERRNO_NETWORK, + code: ERROR_CODE_METHOD_NOT_ALLOWED, + message: ERROR_MSG_METHOD_NOT_ALLOWED, + }); + } + try { + await request.dispatch(method, payload); + } catch (error) { + throw new FxAccountsProfileClientError({ + error: ERROR_NETWORK, + errno: ERRNO_NETWORK, + message: error.toString(), + }); + } + + let body = null; + try { + if (request.response.status == 304) { + return null; + } + body = JSON.parse(request.response.body); + } catch (e) { + throw new FxAccountsProfileClientError({ + error: ERROR_PARSE, + errno: ERRNO_PARSE, + code: request.response.status, + message: request.response.body, + }); + } + + // "response.success" means status code is 200 + if (!request.response.success) { + throw new FxAccountsProfileClientError({ + error: body.error || ERROR_UNKNOWN, + errno: body.errno || ERRNO_UNKNOWN_ERROR, + code: request.response.status, + message: body.message || body, + }); + } + return { + body, + etag: request.response.headers.etag, + }; + }, + + /** + * Retrieve user's profile from the server + * + * @param {String} [etag] + * Optional ETag used for caching purposes. (may generate a 304 exception) + * @return Promise + * Resolves: {body: Object, etag: Object} Successful response from the '/profile' endpoint. + * Rejects: {FxAccountsProfileClientError} profile client error. + */ + fetchProfile(etag) { + log.debug("FxAccountsProfileClient: Requested profile"); + return this._createRequest("/profile", "GET", etag); + }, +}; + +/** + * Normalized profile client errors + * @param {Object} [details] + * Error details object + * @param {number} [details.code] + * Error code + * @param {number} [details.errno] + * Error number + * @param {String} [details.error] + * Error description + * @param {String|null} [details.message] + * Error message + * @constructor + */ +export var FxAccountsProfileClientError = function (details) { + details = details || {}; + + this.name = "FxAccountsProfileClientError"; + this.code = details.code || null; + this.errno = details.errno || ERRNO_UNKNOWN_ERROR; + this.error = details.error || ERROR_UNKNOWN; + this.message = details.message || null; +}; + +/** + * Returns error object properties + * + * @returns {{name: *, code: *, errno: *, error: *, message: *}} + * @private + */ +FxAccountsProfileClientError.prototype._toStringFields = function () { + return { + name: this.name, + code: this.code, + errno: this.errno, + error: this.error, + message: this.message, + }; +}; + +/** + * String representation of a profile client error + * + * @returns {String} + */ +FxAccountsProfileClientError.prototype.toString = function () { + return this.name + "(" + JSON.stringify(this._toStringFields()) + ")"; +}; diff --git a/services/fxaccounts/FxAccountsPush.sys.mjs b/services/fxaccounts/FxAccountsPush.sys.mjs new file mode 100644 index 0000000000..e3e5f32de5 --- /dev/null +++ b/services/fxaccounts/FxAccountsPush.sys.mjs @@ -0,0 +1,315 @@ +/* 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 { Async } from "resource://services-common/async.sys.mjs"; + +import { + FXA_PUSH_SCOPE_ACCOUNT_UPDATE, + ONLOGOUT_NOTIFICATION, + ON_ACCOUNT_DESTROYED_NOTIFICATION, + ON_COLLECTION_CHANGED_NOTIFICATION, + ON_COMMAND_RECEIVED_NOTIFICATION, + ON_DEVICE_CONNECTED_NOTIFICATION, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + ON_PASSWORD_CHANGED_NOTIFICATION, + ON_PASSWORD_RESET_NOTIFICATION, + ON_PROFILE_CHANGE_NOTIFICATION, + ON_PROFILE_UPDATED_NOTIFICATION, + ON_VERIFY_LOGIN_NOTIFICATION, + log, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +/** + * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser + * + * @param [options] + * Object, custom options that used for testing + * @constructor + */ +export function FxAccountsPushService(options = {}) { + this.log = log; + + if (options.log) { + // allow custom log for testing purposes + this.log = options.log; + } + + this.log.debug("FxAccountsPush loading service"); + this.wrappedJSObject = this; + this.initialize(options); +} + +FxAccountsPushService.prototype = { + /** + * Helps only initialize observers once. + */ + _initialized: false, + /** + * Instance of the nsIPushService or a mocked object. + */ + pushService: null, + /** + * Instance of FxAccountsInternal or a mocked object. + */ + fxai: null, + /** + * Component ID of this service, helps register this component. + */ + classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"), + /** + * Register used interfaces in this service + */ + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + /** + * Initialize the service and register all the required observers. + * + * @param [options] + */ + initialize(options) { + if (this._initialized) { + return false; + } + + this._initialized = true; + + if (options.pushService) { + this.pushService = options.pushService; + } else { + this.pushService = Cc["@mozilla.org/push/Service;1"].getService( + Ci.nsIPushService + ); + } + + if (options.fxai) { + this.fxai = options.fxai; + } else { + const { getFxAccountsSingleton } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ); + const fxAccounts = getFxAccountsSingleton(); + this.fxai = fxAccounts._internal; + } + + this.asyncObserver = Async.asyncObserver(this, this.log); + // We use an async observer because a device waking up can + // observe multiple "Send Tab received" push notifications at the same time. + // The way these notifications are handled is as follows: + // Read index from storage, make network request, update the index. + // You can imagine what happens when multiple calls race: we load + // the same index multiple times and receive the same exact tabs, multiple times. + // The async observer will ensure we make these network requests serially. + Services.obs.addObserver(this.asyncObserver, this.pushService.pushTopic); + Services.obs.addObserver( + this.asyncObserver, + this.pushService.subscriptionChangeTopic + ); + Services.obs.addObserver(this.asyncObserver, ONLOGOUT_NOTIFICATION); + + this.log.debug("FxAccountsPush initialized"); + return true; + }, + /** + * Registers a new endpoint with the Push Server + * + * @returns {Promise} + * Promise always resolves with a subscription or a null if failed to subscribe. + */ + registerPushEndpoint() { + this.log.trace("FxAccountsPush registerPushEndpoint"); + + return new Promise(resolve => { + this.pushService.subscribe( + FXA_PUSH_SCOPE_ACCOUNT_UPDATE, + Services.scriptSecurityManager.getSystemPrincipal(), + (result, subscription) => { + if (Components.isSuccessCode(result)) { + this.log.debug("FxAccountsPush got subscription"); + resolve(subscription); + } else { + this.log.warn("FxAccountsPush failed to subscribe", result); + resolve(null); + } + } + ); + }); + }, + /** + * Async observer interface to listen to push messages, changes and logout. + * + * @param subject + * @param topic + * @param data + * @returns {Promise} + */ + async observe(subject, topic, data) { + try { + this.log.trace( + `observed topic=${topic}, data=${data}, subject=${subject}` + ); + switch (topic) { + case this.pushService.pushTopic: + if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) { + let message = subject.QueryInterface(Ci.nsIPushMessage); + await this._onPushMessage(message); + } + break; + case this.pushService.subscriptionChangeTopic: + if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) { + await this._onPushSubscriptionChange(); + } + break; + case ONLOGOUT_NOTIFICATION: + // user signed out, we need to stop polling the Push Server + await this.unsubscribe(); + break; + } + } catch (err) { + this.log.error(err); + } + }, + + /** + * Fired when the Push server sends a notification. + * + * @private + * @returns {Promise} + */ + async _onPushMessage(message) { + this.log.trace("FxAccountsPushService _onPushMessage"); + if (!message.data) { + // Use the empty signal to check the verification state of the account right away + this.log.debug("empty push message - checking account status"); + this.fxai.checkVerificationStatus(); + return; + } + let payload = message.data.json(); + this.log.debug(`push command: ${payload.command}`); + switch (payload.command) { + case ON_COMMAND_RECEIVED_NOTIFICATION: + await this.fxai.commands.pollDeviceCommands(payload.data.index); + break; + case ON_DEVICE_CONNECTED_NOTIFICATION: + Services.obs.notifyObservers( + null, + ON_DEVICE_CONNECTED_NOTIFICATION, + payload.data.deviceName + ); + break; + case ON_DEVICE_DISCONNECTED_NOTIFICATION: + this.fxai._handleDeviceDisconnection(payload.data.id); + return; + case ON_PROFILE_UPDATED_NOTIFICATION: + // We already have a "profile updated" notification sent via WebChannel, + // let's just re-use that. + Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION); + return; + case ON_PASSWORD_CHANGED_NOTIFICATION: + case ON_PASSWORD_RESET_NOTIFICATION: + this._onPasswordChanged(); + return; + case ON_ACCOUNT_DESTROYED_NOTIFICATION: + this.fxai._handleAccountDestroyed(payload.data.uid); + return; + case ON_COLLECTION_CHANGED_NOTIFICATION: + Services.obs.notifyObservers( + null, + ON_COLLECTION_CHANGED_NOTIFICATION, + payload.data.collections + ); + return; + case ON_VERIFY_LOGIN_NOTIFICATION: + Services.obs.notifyObservers( + null, + ON_VERIFY_LOGIN_NOTIFICATION, + JSON.stringify(payload.data) + ); + break; + default: + this.log.warn("FxA Push command unrecognized: " + payload.command); + } + }, + /** + * Check the FxA session status after a password change/reset event. + * If the session is invalid, reset credentials and notify listeners of + * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed + * + * @returns {Promise} + * @private + */ + _onPasswordChanged() { + return this.fxai.withCurrentAccountState(async state => { + return this.fxai.checkAccountStatus(state); + }); + }, + /** + * Fired when the Push server drops a subscription, or the subscription identifier changes. + * + * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages + * + * @returns {Promise} + * @private + */ + _onPushSubscriptionChange() { + this.log.trace("FxAccountsPushService _onPushSubscriptionChange"); + return this.fxai.updateDeviceRegistration(); + }, + /** + * Unsubscribe from the Push server + * + * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe() + * + * @returns {Promise} - The promise resolves with a bool to indicate if we successfully unsubscribed. + * The promise never rejects. + * @private + */ + unsubscribe() { + this.log.trace("FxAccountsPushService unsubscribe"); + return new Promise(resolve => { + this.pushService.unsubscribe( + FXA_PUSH_SCOPE_ACCOUNT_UPDATE, + Services.scriptSecurityManager.getSystemPrincipal(), + (result, ok) => { + if (Components.isSuccessCode(result)) { + if (ok === true) { + this.log.debug("FxAccountsPushService unsubscribed"); + } else { + this.log.debug( + "FxAccountsPushService had no subscription to unsubscribe" + ); + } + } else { + this.log.warn( + "FxAccountsPushService failed to unsubscribe", + result + ); + } + return resolve(ok); + } + ); + }); + }, + + /** + * Get our Push server subscription. + * + * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#getSubscription() + * + * @returns {Promise} - resolves with the subscription or null. Never rejects. + */ + getSubscription() { + return new Promise(resolve => { + this.pushService.getSubscription( + FXA_PUSH_SCOPE_ACCOUNT_UPDATE, + Services.scriptSecurityManager.getSystemPrincipal(), + (result, subscription) => { + if (!subscription) { + this.log.info("FxAccountsPushService no subscription found"); + return resolve(null); + } + return resolve(subscription); + } + ); + }); + }, +}; diff --git a/services/fxaccounts/FxAccountsStorage.sys.mjs b/services/fxaccounts/FxAccountsStorage.sys.mjs new file mode 100644 index 0000000000..24c85dbc2d --- /dev/null +++ b/services/fxaccounts/FxAccountsStorage.sys.mjs @@ -0,0 +1,618 @@ +/* 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 { + DATA_FORMAT_VERSION, + DEFAULT_STORAGE_FILENAME, + FXA_PWDMGR_HOST, + FXA_PWDMGR_PLAINTEXT_FIELDS, + FXA_PWDMGR_REALM, + FXA_PWDMGR_SECURE_FIELDS, + log, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +// A helper function so code can check what fields are able to be stored by +// the storage manager without having a reference to a manager instance. +export function FxAccountsStorageManagerCanStoreField(fieldName) { + return ( + FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName) || + FXA_PWDMGR_SECURE_FIELDS.has(fieldName) + ); +} + +// The storage manager object. +export var FxAccountsStorageManager = function (options = {}) { + this.options = { + filename: options.filename || DEFAULT_STORAGE_FILENAME, + baseDir: options.baseDir || Services.dirsvc.get("ProfD", Ci.nsIFile).path, + }; + this.plainStorage = new JSONStorage(this.options); + // Tests may want to pretend secure storage isn't available. + let useSecure = "useSecure" in options ? options.useSecure : true; + if (useSecure) { + this.secureStorage = new LoginManagerStorage(); + } else { + this.secureStorage = null; + } + this._clearCachedData(); + // See .initialize() below - this protects against it not being called. + this._promiseInitialized = Promise.reject("initialize not called"); + // A promise to avoid storage races - see _queueStorageOperation + this._promiseStorageComplete = Promise.resolve(); +}; + +FxAccountsStorageManager.prototype = { + _initialized: false, + _needToReadSecure: true, + + // An initialization routine that *looks* synchronous to the callers, but + // is actually async as everything else waits for it to complete. + initialize(accountData) { + if (this._initialized) { + throw new Error("already initialized"); + } + this._initialized = true; + // If we just throw away our pre-rejected promise it is reported as an + // unhandled exception when it is GCd - so add an empty .catch handler here + // to prevent this. + this._promiseInitialized.catch(() => {}); + this._promiseInitialized = this._initialize(accountData); + }, + + async _initialize(accountData) { + log.trace("initializing new storage manager"); + try { + if (accountData) { + // If accountData is passed we don't need to read any storage. + this._needToReadSecure = false; + // split it into the 2 parts, write it and we are done. + for (let [name, val] of Object.entries(accountData)) { + if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) { + this.cachedPlain[name] = val; + } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) { + this.cachedSecure[name] = val; + } else { + // Unknown fields are silently discarded, because there is no way + // for them to be read back later. + log.error( + "Unknown FxA field name in user data, it will be ignored", + name + ); + } + } + // write it out and we are done. + await this._write(); + return; + } + // So we were initialized without account data - that means we need to + // read the state from storage. We try and read plain storage first and + // only attempt to read secure storage if the plain storage had a user. + this._needToReadSecure = await this._readPlainStorage(); + if (this._needToReadSecure && this.secureStorage) { + await this._doReadAndUpdateSecure(); + } + } finally { + log.trace("initializing of new storage manager done"); + } + }, + + finalize() { + // We can't throw this instance away while it is still writing or we may + // end up racing with the newly created one. + log.trace("StorageManager finalizing"); + return this._promiseInitialized + .then(() => { + return this._promiseStorageComplete; + }) + .then(() => { + this._promiseStorageComplete = null; + this._promiseInitialized = null; + this._clearCachedData(); + log.trace("StorageManager finalized"); + }); + }, + + // We want to make sure we don't end up doing multiple storage requests + // concurrently - which has a small window for reads if the master-password + // is locked at initialization time and becomes unlocked later, and always + // has an opportunity for updates. + // We also want to make sure we finished writing when finalizing, so we + // can't accidentally end up with the previous user's write finishing after + // a signOut attempts to clear it. + // So all such operations "queue" themselves via this. + _queueStorageOperation(func) { + // |result| is the promise we return - it has no .catch handler, so callers + // of the storage operation still see failure as a normal rejection. + let result = this._promiseStorageComplete.then(func); + // But the promise we assign to _promiseStorageComplete *does* have a catch + // handler so that rejections in one storage operation does not prevent + // future operations from starting (ie, _promiseStorageComplete must never + // be in a rejected state) + this._promiseStorageComplete = result.catch(err => { + log.error("${func} failed: ${err}", { func, err }); + }); + return result; + }, + + // Get the account data by combining the plain and secure storage. + // If fieldNames is specified, it may be a string or an array of strings, + // and only those fields are returned. If not specified the entire account + // data is returned except for "in memory" fields. Note that not specifying + // field names will soon be deprecated/removed - we want all callers to + // specify the fields they care about. + async getAccountData(fieldNames = null) { + await this._promiseInitialized; + // We know we are initialized - this means our .cachedPlain is accurate + // and doesn't need to be read (it was read if necessary by initialize). + // So if there's no uid, there's no user signed in. + if (!("uid" in this.cachedPlain)) { + return null; + } + let result = {}; + if (fieldNames === null) { + // The "old" deprecated way of fetching a logged in user. + for (let [name, value] of Object.entries(this.cachedPlain)) { + result[name] = value; + } + // But the secure data may not have been read, so try that now. + await this._maybeReadAndUpdateSecure(); + // .cachedSecure now has as much as it possibly can (which is possibly + // nothing if (a) secure storage remains locked and (b) we've never updated + // a field to be stored in secure storage.) + for (let [name, value] of Object.entries(this.cachedSecure)) { + result[name] = value; + } + return result; + } + // The new explicit way of getting attributes. + if (!Array.isArray(fieldNames)) { + fieldNames = [fieldNames]; + } + let checkedSecure = false; + for (let fieldName of fieldNames) { + if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) { + if (this.cachedPlain[fieldName] !== undefined) { + result[fieldName] = this.cachedPlain[fieldName]; + } + } else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) { + // We may not have read secure storage yet. + if (!checkedSecure) { + await this._maybeReadAndUpdateSecure(); + checkedSecure = true; + } + if (this.cachedSecure[fieldName] !== undefined) { + result[fieldName] = this.cachedSecure[fieldName]; + } + } else { + throw new Error("unexpected field '" + fieldName + "'"); + } + } + return result; + }, + + // Update just the specified fields. This DOES NOT allow you to change to + // a different user, nor to set the user as signed-out. + async updateAccountData(newFields) { + await this._promiseInitialized; + if (!("uid" in this.cachedPlain)) { + // If this storage instance shows no logged in user, then you can't + // update fields. + throw new Error("No user is logged in"); + } + if (!newFields || "uid" in newFields) { + throw new Error("Can't change uid"); + } + log.debug("_updateAccountData with items", Object.keys(newFields)); + // work out what bucket. + for (let [name, value] of Object.entries(newFields)) { + if (value == null) { + delete this.cachedPlain[name]; + // no need to do the "delete on null" thing for this.cachedSecure - + // we need to keep it until we have managed to read so we can nuke + // it on write. + this.cachedSecure[name] = null; + } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) { + this.cachedPlain[name] = value; + } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) { + this.cachedSecure[name] = value; + } else { + // Throwing seems reasonable here as some client code has explicitly + // specified the field name, so it's either confused or needs to update + // how this field is to be treated. + throw new Error("unexpected field '" + name + "'"); + } + } + // If we haven't yet read the secure data, do so now, else we may write + // out partial data. + await this._maybeReadAndUpdateSecure(); + // Now save it - but don't wait on the _write promise - it's queued up as + // a storage operation, so .finalize() will wait for completion, but no need + // for us to. + this._write(); + }, + + _clearCachedData() { + this.cachedPlain = {}; + // If we don't have secure storage available we have cachedPlain and + // cachedSecure be the same object. + this.cachedSecure = this.secureStorage == null ? this.cachedPlain : {}; + }, + + /* Reads the plain storage and caches the read values in this.cachedPlain. + Only ever called once and unlike the "secure" storage, is expected to never + fail (ie, plain storage is considered always available, whereas secure + storage may be unavailable if it is locked). + + Returns a promise that resolves with true if valid account data was found, + false otherwise. + + Note: _readPlainStorage is only called during initialize, so isn't + protected via _queueStorageOperation() nor _promiseInitialized. + */ + async _readPlainStorage() { + let got; + try { + got = await this.plainStorage.get(); + } catch (err) { + // File hasn't been created yet. That will be done + // when write is called. + if (!err.name == "NotFoundError") { + log.error("Failed to read plain storage", err); + } + // either way, we return null. + got = null; + } + if ( + !got || + !got.accountData || + !got.accountData.uid || + got.version != DATA_FORMAT_VERSION + ) { + return false; + } + // We need to update our .cachedPlain, but can't just assign to it as + // it may need to be the exact same object as .cachedSecure + // As a sanity check, .cachedPlain must be empty (as we are called by init) + // XXX - this would be a good use-case for a RuntimeAssert or similar, as + // being added in bug 1080457. + if (Object.keys(this.cachedPlain).length) { + throw new Error("should be impossible to have cached data already."); + } + for (let [name, value] of Object.entries(got.accountData)) { + this.cachedPlain[name] = value; + } + return true; + }, + + /* If we haven't managed to read the secure storage, try now, so + we can merge our cached data with the data that's already been set. + */ + _maybeReadAndUpdateSecure() { + if (this.secureStorage == null || !this._needToReadSecure) { + return null; + } + return this._queueStorageOperation(() => { + if (this._needToReadSecure) { + // we might have read it by now! + return this._doReadAndUpdateSecure(); + } + return null; + }); + }, + + /* Unconditionally read the secure storage and merge our cached data (ie, data + which has already been set while the secure storage was locked) with + the read data + */ + async _doReadAndUpdateSecure() { + let { uid, email } = this.cachedPlain; + try { + log.debug( + "reading secure storage with existing", + Object.keys(this.cachedSecure) + ); + // If we already have anything in .cachedSecure it means something has + // updated cachedSecure before we've read it. That means that after we do + // manage to read we must write back the merged data. + let needWrite = !!Object.keys(this.cachedSecure).length; + let readSecure = await this.secureStorage.get(uid, email); + // and update our cached data with it - anything already in .cachedSecure + // wins (including the fact it may be null or undefined, the latter + // which means it will be removed from storage. + if (readSecure && readSecure.version != DATA_FORMAT_VERSION) { + log.warn("got secure data but the data format version doesn't match"); + readSecure = null; + } + if (readSecure && readSecure.accountData) { + log.debug( + "secure read fetched items", + Object.keys(readSecure.accountData) + ); + for (let [name, value] of Object.entries(readSecure.accountData)) { + if (!(name in this.cachedSecure)) { + this.cachedSecure[name] = value; + } + } + if (needWrite) { + log.debug("successfully read secure data; writing updated data back"); + await this._doWriteSecure(); + } + } + this._needToReadSecure = false; + } catch (ex) { + if (ex instanceof this.secureStorage.STORAGE_LOCKED) { + log.debug("setAccountData: secure storage is locked trying to read"); + } else { + log.error("failed to read secure storage", ex); + throw ex; + } + } + }, + + _write() { + // We don't want multiple writes happening concurrently, and we also need to + // know when an "old" storage manager is done (this.finalize() waits for this) + return this._queueStorageOperation(() => this.__write()); + }, + + async __write() { + // Write everything back - later we could track what's actually dirty, + // but for now we write it all. + log.debug("writing plain storage", Object.keys(this.cachedPlain)); + let toWritePlain = { + version: DATA_FORMAT_VERSION, + accountData: this.cachedPlain, + }; + await this.plainStorage.set(toWritePlain); + + // If we have no secure storage manager we are done. + if (this.secureStorage == null) { + return; + } + // and only attempt to write to secure storage if we've managed to read it, + // otherwise we might clobber data that's already there. + if (!this._needToReadSecure) { + await this._doWriteSecure(); + } + }, + + /* Do the actual write of secure data. Caller is expected to check if we actually + need to write and to ensure we are in a queued storage operation. + */ + async _doWriteSecure() { + // We need to remove null items here. + for (let [name, value] of Object.entries(this.cachedSecure)) { + if (value == null) { + delete this.cachedSecure[name]; + } + } + log.debug("writing secure storage", Object.keys(this.cachedSecure)); + let toWriteSecure = { + version: DATA_FORMAT_VERSION, + accountData: this.cachedSecure, + }; + try { + await this.secureStorage.set(this.cachedPlain.uid, toWriteSecure); + } catch (ex) { + if (!(ex instanceof this.secureStorage.STORAGE_LOCKED)) { + throw ex; + } + // This shouldn't be possible as once it is unlocked it can't be + // re-locked, and we can only be here if we've previously managed to + // read. + log.error("setAccountData: secure storage is locked trying to write"); + } + }, + + // Delete the data for an account - ie, called on "sign out". + deleteAccountData() { + return this._queueStorageOperation(() => this._deleteAccountData()); + }, + + async _deleteAccountData() { + log.debug("removing account data"); + await this._promiseInitialized; + await this.plainStorage.set(null); + if (this.secureStorage) { + await this.secureStorage.set(null); + } + this._clearCachedData(); + log.debug("account data reset"); + }, +}; + +/** + * JSONStorage constructor that creates instances that may set/get + * to a specified file, in a directory that will be created if it + * doesn't exist. + * + * @param options { + * filename: of the file to write to + * baseDir: directory where the file resides + * } + * @return instance + */ +function JSONStorage(options) { + this.baseDir = options.baseDir; + this.path = PathUtils.join(options.baseDir, options.filename); +} + +JSONStorage.prototype = { + set(contents) { + log.trace( + "starting write of json user data", + contents ? Object.keys(contents.accountData) : "null" + ); + let start = Date.now(); + return IOUtils.makeDirectory(this.baseDir, { ignoreExisting: true }) + .then(IOUtils.writeJSON.bind(null, this.path, contents)) + .then(result => { + log.trace( + "finished write of json user data - took", + Date.now() - start + ); + return result; + }); + }, + + get() { + log.trace("starting fetch of json user data"); + let start = Date.now(); + return IOUtils.readJSON(this.path).then(result => { + log.trace("finished fetch of json user data - took", Date.now() - start); + return result; + }); + }, +}; + +function StorageLockedError() {} + +/** + * LoginManagerStorage constructor that creates instances that set/get + * data stored securely in the nsILoginManager. + * + * @return instance + */ + +export function LoginManagerStorage() {} + +LoginManagerStorage.prototype = { + STORAGE_LOCKED: StorageLockedError, + // The fields in the credentials JSON object that are stored in plain-text + // in the profile directory. All other fields are stored in the login manager, + // and thus are only available when the master-password is unlocked. + + // a hook point for testing. + get _isLoggedIn() { + return Services.logins.isLoggedIn; + }, + + // Clear any data from the login manager. Returns true if the login manager + // was unlocked (even if no existing logins existed) or false if it was + // locked (meaning we don't even know if it existed or not.) + async _clearLoginMgrData() { + try { + // Services.logins might be third-party and broken... + await Services.logins.initializationPromise; + if (!this._isLoggedIn) { + return false; + } + let logins = await Services.logins.searchLoginsAsync({ + origin: FXA_PWDMGR_HOST, + httpRealm: FXA_PWDMGR_REALM, + }); + for (let login of logins) { + Services.logins.removeLogin(login); + } + return true; + } catch (ex) { + log.error("Failed to clear login data: ${}", ex); + return false; + } + }, + + async set(uid, contents) { + if (!contents) { + // Nuke it from the login manager. + let cleared = await this._clearLoginMgrData(); + if (!cleared) { + // just log a message - we verify that the uid matches when + // we reload it, so having a stale entry doesn't really hurt. + log.info("not removing credentials from login manager - not logged in"); + } + log.trace("storage set finished clearing account data"); + return; + } + + // We are saving actual data. + log.trace("starting write of user data to the login manager"); + try { + // Services.logins might be third-party and broken... + // and the stuff into the login manager. + await Services.logins.initializationPromise; + // If MP is locked we silently fail - the user may need to re-auth + // next startup. + if (!this._isLoggedIn) { + log.info("not saving credentials to login manager - not logged in"); + throw new this.STORAGE_LOCKED(); + } + // write the data to the login manager. + let loginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" + ); + let login = new loginInfo( + FXA_PWDMGR_HOST, + null, // aFormActionOrigin, + FXA_PWDMGR_REALM, // aHttpRealm, + uid, // aUsername + JSON.stringify(contents), // aPassword + "", // aUsernameField + "" + ); // aPasswordField + + let existingLogins = await Services.logins.searchLoginsAsync({ + origin: FXA_PWDMGR_HOST, + httpRealm: FXA_PWDMGR_REALM, + }); + if (existingLogins.length) { + Services.logins.modifyLogin(existingLogins[0], login); + } else { + await Services.logins.addLoginAsync(login); + } + log.trace("finished write of user data to the login manager"); + } catch (ex) { + if (ex instanceof this.STORAGE_LOCKED) { + throw ex; + } + // just log and consume the error here - it may be a 3rd party login + // manager replacement that's simply broken. + log.error("Failed to save data to the login manager", ex); + } + }, + + async get(uid, email) { + log.trace("starting fetch of user data from the login manager"); + + try { + // Services.logins might be third-party and broken... + // read the data from the login manager and merge it for return. + await Services.logins.initializationPromise; + + if (!this._isLoggedIn) { + log.info( + "returning partial account data as the login manager is locked." + ); + throw new this.STORAGE_LOCKED(); + } + + let logins = await Services.logins.searchLoginsAsync({ + origin: FXA_PWDMGR_HOST, + httpRealm: FXA_PWDMGR_REALM, + }); + if (!logins.length) { + // This could happen if the MP was locked when we wrote the data. + log.info("Can't find any credentials in the login manager"); + return null; + } + let login = logins[0]; + // Support either the uid or the email as the username - as of bug 1183951 + // we store the uid, but we support having either for b/w compat. + if (login.username == uid || login.username == email) { + return JSON.parse(login.password); + } + log.info("username in the login manager doesn't match - ignoring it"); + await this._clearLoginMgrData(); + } catch (ex) { + if (ex instanceof this.STORAGE_LOCKED) { + throw ex; + } + // just log and consume the error here - it may be a 3rd party login + // manager replacement that's simply broken. + log.error("Failed to get data from the login manager", ex); + } + return null; + }, +}; diff --git a/services/fxaccounts/FxAccountsTelemetry.sys.mjs b/services/fxaccounts/FxAccountsTelemetry.sys.mjs new file mode 100644 index 0000000000..1d7b3d4954 --- /dev/null +++ b/services/fxaccounts/FxAccountsTelemetry.sys.mjs @@ -0,0 +1,173 @@ +/* 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/. */ + +// FxA Telemetry support. For hysterical raisins, the actual implementation +// is inside "sync". We should move the core implementation somewhere that's +// sanely shared (eg, services-common?), but let's wait and see where we end up +// first... + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CryptoUtils: "resource://services-crypto/utils.sys.mjs", + + // We use this observers module because we leverage its support for richer + // "subject" data. + Observers: "resource://services-common/observers.sys.mjs", +}); + +import { + PREF_ACCOUNT_ROOT, + log, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +const PREF_SANITIZED_UID = PREF_ACCOUNT_ROOT + "telemetry.sanitized_uid"; +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "pref_sanitizedUid", + PREF_SANITIZED_UID, + "" +); + +export class FxAccountsTelemetry { + constructor(fxai) { + this._fxai = fxai; + Services.telemetry.setEventRecordingEnabled("fxa", true); + } + + // Records an event *in the Fxa/Sync ping*. + recordEvent(object, method, value, extra = undefined) { + // We need to ensure the telemetry module is loaded. + ChromeUtils.importESModule("resource://services-sync/telemetry.sys.mjs"); + // Now it will be listening for the notifications... + lazy.Observers.notify("fxa:telemetry:event", { + object, + method, + value, + extra, + }); + } + + generateUUID() { + return Services.uuid.generateUUID().toString().slice(1, -1); + } + + // A flow ID can be anything that's "probably" unique, so for now use a UUID. + generateFlowID() { + return this.generateUUID(); + } + + // FxA- and Sync-related metrics are submitted in a special-purpose "sync ping". This ping + // identifies the user by a version of their FxA uid that is HMAC-ed with a server-side secret + // key, in an attempt to provide a bit of anonymity. + + // Secret back-channel by which tokenserver client code can set the hashed UID. + // This value conceptually belongs to FxA, but we currently get it from tokenserver, + // so there's some light hackery to put it in the right place. + _setHashedUID(hashedUID) { + if (!hashedUID) { + Services.prefs.clearUserPref(PREF_SANITIZED_UID); + } else { + Services.prefs.setStringPref(PREF_SANITIZED_UID, hashedUID); + } + } + + getSanitizedUID() { + // Sadly, we can only currently obtain this value if the user has enabled sync. + return lazy.pref_sanitizedUid || null; + } + + // Sanitize the ID of a device into something suitable for including in the + // ping. Returns null if no transformation is possible. + sanitizeDeviceId(deviceId) { + const uid = this.getSanitizedUID(); + if (!uid) { + // Sadly, we can only currently get this if the user has enabled sync. + return null; + } + // Combine the raw device id with the sanitized uid to create a stable + // unique identifier that can't be mapped back to the user's FxA + // identity without knowing the metrics HMAC key. + // The result is 64 bytes long, which in retrospect is probably excessive, + // but it's already shipping... + return lazy.CryptoUtils.sha256(deviceId + uid); + } + + // Record the connection of FxA or one of its services. + // Note that you must call this before performing the actual connection + // or we may record incorrect data - for example, we will not be able to + // determine whether FxA itself was connected before this call. + // + // Currently sends an event in the main telemetry event ping rather than the + // FxA/Sync ping (although this might change in the future) + // + // @param services - An array of service names which should be recorded. FxA + // itself is not counted as a "service" - ie, an empty array should be passed + // if the account is connected without anything else . + // + // @param how - How the connection was done. + async recordConnection(services, how = null) { + try { + let extra = {}; + // Record that fxa was connected if it isn't currently - it will be soon. + if (!(await this._fxai.getUserAccountData())) { + extra.fxa = "true"; + } + // Events.yaml only declares "sync" as a valid service. + if (services.includes("sync")) { + extra.sync = "true"; + } + Services.telemetry.recordEvent("fxa", "connect", "account", how, extra); + } catch (ex) { + log.error("Failed to record connection telemetry", ex); + console.error("Failed to record connection telemetry", ex); + } + } + + // Record the disconnection of FxA or one of its services. + // Note that you must call this before performing the actual disconnection + // or we may record incomplete data - for example, if this is called after + // disconnection, we've almost certainly lost the ability to record what + // services were enabled prior to disconnection. + // + // Currently sends an event in the main telemetry event ping rather than the + // FxA/Sync ping (although this might change in the future) + // + // @param service - the service being disconnected. If null, the account + // itself is being disconnected, so all connected services are too. + // + // @param how - how the disconnection was done. + async recordDisconnection(service = null, how = null) { + try { + let extra = {}; + if (!service) { + extra.fxa = "true"; + // We need a way to enumerate all services - but for now we just hard-code + // all possibilities here. + if (Services.prefs.prefHasUserValue("services.sync.username")) { + extra.sync = "true"; + } + } else if (service == "sync") { + extra[service] = "true"; + } else { + // Events.yaml only declares "sync" as a valid service. + log.warn( + `recordDisconnection has invalid value for service: ${service}` + ); + } + Services.telemetry.recordEvent( + "fxa", + "disconnect", + "account", + how, + extra + ); + } catch (ex) { + log.error("Failed to record disconnection telemetry", ex); + console.error("Failed to record disconnection telemetry", ex); + } + } +} diff --git a/services/fxaccounts/FxAccountsWebChannel.sys.mjs b/services/fxaccounts/FxAccountsWebChannel.sys.mjs new file mode 100644 index 0000000000..fdd0b75e93 --- /dev/null +++ b/services/fxaccounts/FxAccountsWebChannel.sys.mjs @@ -0,0 +1,824 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Firefox Accounts Web Channel. + * + * Uses the WebChannel component to receive messages + * about account state changes. + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { + COMMAND_PROFILE_CHANGE, + COMMAND_LOGIN, + COMMAND_LOGOUT, + COMMAND_OAUTH, + COMMAND_DELETE, + COMMAND_CAN_LINK_ACCOUNT, + COMMAND_SYNC_PREFERENCES, + COMMAND_CHANGE_PASSWORD, + COMMAND_FXA_STATUS, + COMMAND_PAIR_HEARTBEAT, + COMMAND_PAIR_SUPP_METADATA, + COMMAND_PAIR_AUTHORIZE, + COMMAND_PAIR_DECLINE, + COMMAND_PAIR_COMPLETE, + COMMAND_PAIR_PREFERENCES, + COMMAND_FIREFOX_VIEW, + FX_OAUTH_CLIENT_ID, + ON_PROFILE_CHANGE_NOTIFICATION, + PREF_LAST_FXA_USER, + WEBCHANNEL_ID, + log, + logPII, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CryptoUtils: "resource://services-crypto/utils.sys.mjs", + FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs", + FxAccountsStorageManagerCanStoreField: + "resource://gre/modules/FxAccountsStorage.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + Weave: "resource://services-sync/main.sys.mjs", + WebChannel: "resource://gre/modules/WebChannel.sys.mjs", +}); +ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton(); +}); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "pairingEnabled", + "identity.fxaccounts.pairing.enabled" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "separatePrivilegedMozillaWebContentProcess", + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", + false +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "separatedMozillaDomains", + "browser.tabs.remote.separatedMozillaDomains", + "", + false, + val => val.split(",") +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "accountServer", + "identity.fxaccounts.remote.root", + null, + false, + val => Services.io.newURI(val) +); + +// These engines were added years after Sync had been introduced, they need +// special handling since they are system add-ons and are un-available on +// older versions of Firefox. +const EXTRA_ENGINES = ["addresses", "creditcards"]; + +// These engines will be displayed to the user to pick which they would like to +// use +const CHOOSE_WHAT_TO_SYNC = [ + "addons", + "addresses", + "bookmarks", + "creditcards", + "history", + "passwords", + "preferences", + "tabs", +]; + +/** + * A helper function that extracts the message and stack from an error object. + * Returns a `{ message, stack }` tuple. `stack` will be null if the error + * doesn't have a stack trace. + */ +function getErrorDetails(error) { + // Replace anything that looks like it might be a filepath on Windows or Unix + let cleanMessage = String(error) + .replace(/\\.*\\/gm, "[REDACTED]") + .replace(/\/.*\//gm, "[REDACTED]"); + let details = { message: cleanMessage, stack: null }; + + // Adapted from Console.sys.mjs. + if (error.stack) { + let frames = []; + for (let frame = error.stack; frame; frame = frame.caller) { + frames.push(String(frame).padStart(4)); + } + details.stack = frames.join("\n"); + } + + return details; +} + +/** + * Create a new FxAccountsWebChannel to listen for account updates + * + * @param {Object} options Options + * @param {Object} options + * @param {String} options.content_uri + * The FxA Content server uri + * @param {String} options.channel_id + * The ID of the WebChannel + * @param {String} options.helpers + * Helpers functions. Should only be passed in for testing. + * @constructor + */ +export function FxAccountsWebChannel(options) { + if (!options) { + throw new Error("Missing configuration options"); + } + if (!options.content_uri) { + throw new Error("Missing 'content_uri' option"); + } + this._contentUri = options.content_uri; + + if (!options.channel_id) { + throw new Error("Missing 'channel_id' option"); + } + this._webChannelId = options.channel_id; + + // options.helpers is only specified by tests. + ChromeUtils.defineLazyGetter(this, "_helpers", () => { + return options.helpers || new FxAccountsWebChannelHelpers(options); + }); + + this._setupChannel(); +} + +FxAccountsWebChannel.prototype = { + /** + * WebChannel that is used to communicate with content page + */ + _channel: null, + + /** + * Helpers interface that does the heavy lifting. + */ + _helpers: null, + + /** + * WebChannel ID. + */ + _webChannelId: null, + /** + * WebChannel origin, used to validate origin of messages + */ + _webChannelOrigin: null, + + /** + * Release all resources that are in use. + */ + tearDown() { + this._channel.stopListening(); + this._channel = null; + this._channelCallback = null; + }, + + /** + * Configures and registers a new WebChannel + * + * @private + */ + _setupChannel() { + // if this.contentUri is present but not a valid URI, then this will throw an error. + try { + this._webChannelOrigin = Services.io.newURI(this._contentUri); + this._registerChannel(); + } catch (e) { + log.error(e); + throw e; + } + }, + + _receiveMessage(message, sendingContext) { + const { command, data } = message; + let shouldCheckRemoteType = + lazy.separatePrivilegedMozillaWebContentProcess && + lazy.separatedMozillaDomains.some(function (val) { + return ( + lazy.accountServer.asciiHost == val || + lazy.accountServer.asciiHost.endsWith("." + val) + ); + }); + let { currentRemoteType } = sendingContext.browsingContext; + if (shouldCheckRemoteType && currentRemoteType != "privilegedmozilla") { + log.error( + `Rejected FxA webchannel message from remoteType = ${currentRemoteType}` + ); + return; + } + + let browser = sendingContext.browsingContext.top.embedderElement; + switch (command) { + case COMMAND_PROFILE_CHANGE: + Services.obs.notifyObservers( + null, + ON_PROFILE_CHANGE_NOTIFICATION, + data.uid + ); + break; + case COMMAND_LOGIN: + this._helpers + .login(data) + .catch(error => this._sendError(error, message, sendingContext)); + break; + case COMMAND_OAUTH: + this._helpers + .oauthLogin(data) + .catch(error => this._sendError(error, message, sendingContext)); + break; + case COMMAND_LOGOUT: + case COMMAND_DELETE: + this._helpers + .logout(data.uid) + .catch(error => this._sendError(error, message, sendingContext)); + break; + case COMMAND_CAN_LINK_ACCOUNT: + let canLinkAccount = this._helpers.shouldAllowRelink(data.email); + + let response = { + command, + messageId: message.messageId, + data: { ok: canLinkAccount }, + }; + + log.debug("FxAccountsWebChannel response", response); + this._channel.send(response, sendingContext); + break; + case COMMAND_SYNC_PREFERENCES: + this._helpers.openSyncPreferences(browser, data.entryPoint); + break; + case COMMAND_PAIR_PREFERENCES: + if (lazy.pairingEnabled) { + let window = browser.ownerGlobal; + // We should close the FxA tab after we open our pref page + let selectedTab = window.gBrowser.selectedTab; + window.switchToTabHavingURI( + "about:preferences?action=pair#sync", + true, + { + ignoreQueryString: true, + replaceQueryString: true, + adoptIntoActiveWindow: true, + ignoreFragment: "whenComparing", + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + // close the tab + window.gBrowser.removeTab(selectedTab); + } + break; + case COMMAND_FIREFOX_VIEW: + this._helpers.openFirefoxView(browser, data.entryPoint); + break; + case COMMAND_CHANGE_PASSWORD: + this._helpers + .changePassword(data) + .catch(error => this._sendError(error, message, sendingContext)); + break; + case COMMAND_FXA_STATUS: + log.debug("fxa_status received"); + + const service = data && data.service; + const isPairing = data && data.isPairing; + const context = data && data.context; + this._helpers + .getFxaStatus(service, sendingContext, isPairing, context) + .then(fxaStatus => { + let response = { + command, + messageId: message.messageId, + data: fxaStatus, + }; + this._channel.send(response, sendingContext); + }) + .catch(error => this._sendError(error, message, sendingContext)); + break; + case COMMAND_PAIR_HEARTBEAT: + case COMMAND_PAIR_SUPP_METADATA: + case COMMAND_PAIR_AUTHORIZE: + case COMMAND_PAIR_DECLINE: + case COMMAND_PAIR_COMPLETE: + log.debug(`Pairing command ${command} received`); + const { channel_id: channelId } = data; + delete data.channel_id; + const flow = lazy.FxAccountsPairingFlow.get(channelId); + if (!flow) { + log.warn(`Could not find a pairing flow for ${channelId}`); + return; + } + flow.onWebChannelMessage(command, data).then(replyData => { + this._channel.send( + { + command, + messageId: message.messageId, + data: replyData, + }, + sendingContext + ); + }); + break; + default: + log.warn("Unrecognized FxAccountsWebChannel command", command); + // As a safety measure we also terminate any pending FxA pairing flow. + lazy.FxAccountsPairingFlow.finalizeAll(); + break; + } + }, + + _sendError(error, incomingMessage, sendingContext) { + log.error("Failed to handle FxAccountsWebChannel message", error); + this._channel.send( + { + command: incomingMessage.command, + messageId: incomingMessage.messageId, + data: { + error: getErrorDetails(error), + }, + }, + sendingContext + ); + }, + + /** + * Create a new channel with the WebChannelBroker, setup a callback listener + * @private + */ + _registerChannel() { + /** + * Processes messages that are called back from the FxAccountsChannel + * + * @param webChannelId {String} + * Command webChannelId + * @param message {Object} + * Command message + * @param sendingContext {Object} + * Message sending context. + * @param sendingContext.browsingContext {BrowsingContext} + * The browsingcontext from which the + * WebChannelMessageToChrome was sent. + * @param sendingContext.eventTarget {EventTarget} + * The where the message was sent. + * @param sendingContext.principal {Principal} + * The of the EventTarget where the message was sent. + * @private + * + */ + let listener = (webChannelId, message, sendingContext) => { + if (message) { + log.debug("FxAccountsWebChannel message received", message.command); + if (logPII()) { + log.debug("FxAccountsWebChannel message details", message); + } + try { + this._receiveMessage(message, sendingContext); + } catch (error) { + this._sendError(error, message, sendingContext); + } + } + }; + + this._channelCallback = listener; + this._channel = new lazy.WebChannel( + this._webChannelId, + this._webChannelOrigin + ); + this._channel.listen(listener); + log.debug( + "FxAccountsWebChannel registered: " + + this._webChannelId + + " with origin " + + this._webChannelOrigin.prePath + ); + }, +}; + +export function FxAccountsWebChannelHelpers(options) { + options = options || {}; + + this._fxAccounts = options.fxAccounts || lazy.fxAccounts; + this._weaveXPCOM = options.weaveXPCOM || null; + this._privateBrowsingUtils = + options.privateBrowsingUtils || lazy.PrivateBrowsingUtils; +} + +FxAccountsWebChannelHelpers.prototype = { + // If the last fxa account used for sync isn't this account, we display + // a modal dialog checking they really really want to do this... + // (This is sync-specific, so ideally would be in sync's identity module, + // but it's a little more seamless to do here, and sync is currently the + // only fxa consumer, so... + shouldAllowRelink(acctName) { + return ( + !this._needRelinkWarning(acctName) || this._promptForRelink(acctName) + ); + }, + + async _initializeSync() { + // A sync-specific hack - we want to ensure sync has been initialized + // before we set the signed-in user. + // XXX - probably not true any more, especially now we have observerPreloads + // in FxAccounts.jsm? + let xps = + this._weaveXPCOM || + Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports) + .wrappedJSObject; + await xps.whenLoaded(); + return xps; + }, + + _setEnabledEngines(offeredEngines, declinedEngines) { + if (offeredEngines && declinedEngines) { + EXTRA_ENGINES.forEach(engine => { + if ( + offeredEngines.includes(engine) && + !declinedEngines.includes(engine) + ) { + // These extra engines are disabled by default. + Services.prefs.setBoolPref(`services.sync.engine.${engine}`, true); + } + }); + log.debug("Received declined engines", declinedEngines); + lazy.Weave.Service.engineManager.setDeclined(declinedEngines); + declinedEngines.forEach(engine => { + Services.prefs.setBoolPref(`services.sync.engine.${engine}`, false); + }); + } + }, + /** + * stores sync login info it in the fxaccounts service + * + * @param accountData the user's account data and credentials + */ + async login(accountData) { + // We don't act on customizeSync anymore, it used to open a dialog inside + // the browser to selecte the engines to sync but we do it on the web now. + log.debug("Webchannel is logging a user in."); + delete accountData.customizeSync; + + // Save requested services for later. + const requestedServices = accountData.services; + delete accountData.services; + + // the user has already been shown the "can link account" + // screen. No need to keep this data around. + delete accountData.verifiedCanLinkAccount; + + // Remember who it was so we can log out next time. + if (accountData.verified) { + this.setPreviousAccountNameHashPref(accountData.email); + } + + await this._fxAccounts.telemetry.recordConnection( + Object.keys(requestedServices || {}), + "webchannel" + ); + + const xps = await this._initializeSync(); + await this._fxAccounts._internal.setSignedInUser(accountData); + + if (requestedServices) { + // User has enabled Sync. + if (requestedServices.sync) { + const { offeredEngines, declinedEngines } = requestedServices.sync; + this._setEnabledEngines(offeredEngines, declinedEngines); + log.debug("Webchannel is enabling sync"); + await xps.Weave.Service.configure(); + } + } + }, + + /** + * Logins in to sync by completing an OAuth flow + * @param { Object } oauthData: The oauth code and state as returned by the server */ + async oauthLogin(oauthData) { + log.debug("Webchannel is completing the oauth flow"); + const xps = await this._initializeSync(); + const { code, state, declinedSyncEngines, offeredSyncEngines } = oauthData; + const { sessionToken } = + await this._fxAccounts._internal.getUserAccountData(["sessionToken"]); + // First we finish the ongoing oauth flow + const { scopedKeys, refreshToken } = + await this._fxAccounts._internal.completeOAuthFlow( + sessionToken, + code, + state + ); + + // We don't currently use the refresh token in Firefox Desktop, lets be good citizens and revoke it. + await this._fxAccounts._internal.destroyOAuthToken({ token: refreshToken }); + + // Then, we persist the sync keys + await this._fxAccounts._internal.setScopedKeys(scopedKeys); + + // Now that we have the scoped keys, we set our status to verified + await this._fxAccounts._internal.setUserVerified(); + this._setEnabledEngines(offeredSyncEngines, declinedSyncEngines); + log.debug("Webchannel is enabling sync"); + xps.Weave.Service.configure(); + }, + + /** + * logout the fxaccounts service + * + * @param the uid of the account which have been logged out + */ + async logout(uid) { + let fxa = this._fxAccounts; + let userData = await fxa._internal.getUserAccountData(["uid"]); + if (userData && userData.uid === uid) { + await fxa.telemetry.recordDisconnection(null, "webchannel"); + // true argument is `localOnly`, because server-side stuff + // has already been taken care of by the content server + await fxa.signOut(true); + } + }, + + /** + * Check if `sendingContext` is in private browsing mode. + */ + isPrivateBrowsingMode(sendingContext) { + if (!sendingContext) { + log.error("Unable to check for private browsing mode, assuming true"); + return true; + } + + let browser = sendingContext.browsingContext.top.embedderElement; + const isPrivateBrowsing = + this._privateBrowsingUtils.isBrowserPrivate(browser); + log.debug("is private browsing", isPrivateBrowsing); + return isPrivateBrowsing; + }, + + /** + * Check whether sending fxa_status data should be allowed. + */ + shouldAllowFxaStatus(service, sendingContext, isPairing, context) { + // Return user data for any service in non-PB mode. In PB mode, + // only return user data if service==="sync" or is in pairing mode + // (as service will be equal to the OAuth client ID and not "sync"). + // + // This behaviour allows users to click the "Manage Account" + // link from about:preferences#sync while in PB mode and things + // "just work". While in non-PB mode, users can sign into + // Pocket w/o entering their password a 2nd time, while in PB + // mode they *will* have to enter their email/password again. + // + // The difference in behaviour is to try to match user + // expectations as to what is and what isn't part of the browser. + // Sync is viewed as an integral part of the browser, interacting + // with FxA as part of a Sync flow should work all the time. If + // Sync is broken in PB mode, users will think Firefox is broken. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1323853 + log.debug("service", service); + return ( + !this.isPrivateBrowsingMode(sendingContext) || + service === "sync" || + context === "fx_desktop_v3" || + isPairing + ); + }, + + /** + * Get fxa_status information. Resolves to { signedInUser: }. + * If returning status information is not allowed or no user is signed into + * Sync, `user_data` will be null. + */ + async getFxaStatus(service, sendingContext, isPairing, context) { + let signedInUser = null; + + if ( + this.shouldAllowFxaStatus(service, sendingContext, isPairing, context) + ) { + const userData = await this._fxAccounts._internal.getUserAccountData([ + "email", + "sessionToken", + "uid", + "verified", + ]); + if (userData) { + signedInUser = { + email: userData.email, + sessionToken: userData.sessionToken, + uid: userData.uid, + verified: userData.verified, + }; + } + } + + const capabilities = this._getCapabilities(); + + return { + signedInUser, + clientId: FX_OAUTH_CLIENT_ID, + capabilities, + }; + }, + _getCapabilities() { + if ( + Services.prefs.getBoolPref("identity.fxaccounts.oauth.enabled", false) + ) { + return { + multiService: true, + pairing: lazy.pairingEnabled, + choose_what_to_sync: true, + engines: CHOOSE_WHAT_TO_SYNC, + }; + } + return { + multiService: true, + pairing: lazy.pairingEnabled, + engines: this._getAvailableExtraEngines(), + }; + }, + + _getAvailableExtraEngines() { + return EXTRA_ENGINES.filter(engineName => { + try { + return Services.prefs.getBoolPref( + `services.sync.engine.${engineName}.available` + ); + } catch (e) { + return false; + } + }); + }, + + async changePassword(credentials) { + // If |credentials| has fields that aren't handled by accounts storage, + // updateUserAccountData will throw - mainly to prevent errors in code + // that hard-codes field names. + // However, in this case the field names aren't really in our control. + // We *could* still insist the server know what fields names are valid, + // but that makes life difficult for the server when Firefox adds new + // features (ie, new fields) - forcing the server to track a map of + // versions to supported field names doesn't buy us much. + // So we just remove field names we know aren't handled. + let newCredentials = { + device: null, // Force a brand new device registration. + // We force the re-encryption of the send tab keys using the new sync key after the password change + encryptedSendTabKeys: null, + }; + for (let name of Object.keys(credentials)) { + if ( + name == "email" || + name == "uid" || + lazy.FxAccountsStorageManagerCanStoreField(name) + ) { + newCredentials[name] = credentials[name]; + } else { + log.info("changePassword ignoring unsupported field", name); + } + } + await this._fxAccounts._internal.updateUserAccountData(newCredentials); + await this._fxAccounts._internal.updateDeviceRegistration(); + }, + + /** + * Get the hash of account name of the previously signed in account + */ + getPreviousAccountNameHashPref() { + try { + return Services.prefs.getStringPref(PREF_LAST_FXA_USER); + } catch (_) { + return ""; + } + }, + + /** + * Given an account name, set the hash of the previously signed in account + * + * @param acctName the account name of the user's account. + */ + setPreviousAccountNameHashPref(acctName) { + Services.prefs.setStringPref( + PREF_LAST_FXA_USER, + lazy.CryptoUtils.sha256Base64(acctName) + ); + }, + + /** + * Open Sync Preferences in the current tab of the browser + * + * @param {Object} browser the browser in which to open preferences + * @param {String} [entryPoint] entryPoint to use for logging + */ + openSyncPreferences(browser, entryPoint) { + let uri = "about:preferences"; + if (entryPoint) { + uri += "?entrypoint=" + encodeURIComponent(entryPoint); + } + uri += "#sync"; + + browser.loadURI(Services.io.newURI(uri), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + }, + + /** + * Open Firefox View in the browser's window + * + * @param {Object} browser the browser in whose window we'll open Firefox View + */ + openFirefoxView(browser) { + browser.ownerGlobal.FirefoxViewHandler.openTab("syncedtabs"); + }, + + /** + * If a user signs in using a different account, the data from the + * previous account and the new account will be merged. Ask the user + * if they want to continue. + * + * @private + */ + _needRelinkWarning(acctName) { + let prevAcctHash = this.getPreviousAccountNameHashPref(); + return ( + prevAcctHash && prevAcctHash != lazy.CryptoUtils.sha256Base64(acctName) + ); + }, + + /** + * Show the user a warning dialog that the data from the previous account + * and the new account will be merged. + * + * @private + */ + _promptForRelink(acctName) { + let sb = Services.strings.createBundle( + "chrome://browser/locale/syncSetup.properties" + ); + let continueLabel = sb.GetStringFromName("continue.label"); + let title = sb.GetStringFromName("relinkVerify.title"); + let description = sb.formatStringFromName("relinkVerify.description", [ + acctName, + ]); + let body = + sb.GetStringFromName("relinkVerify.heading") + "\n\n" + description; + let ps = Services.prompt; + let buttonFlags = + ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL + + ps.BUTTON_POS_1_DEFAULT; + + // If running in context of the browser chrome, window does not exist. + let pressed = Services.prompt.confirmEx( + null, + title, + body, + buttonFlags, + continueLabel, + null, + null, + null, + {} + ); + return pressed === 0; // 0 is the "continue" button + }, +}; + +var singleton; + +// The entry-point for this module, which ensures only one of our channels is +// ever created - we require this because the WebChannel is global in scope +// (eg, it uses the observer service to tell interested parties of interesting +// things) and allowing multiple channels would cause such notifications to be +// sent multiple times. +export var EnsureFxAccountsWebChannel = () => { + let contentUri = Services.urlFormatter.formatURLPref( + "identity.fxaccounts.remote.root" + ); + if (singleton && singleton._contentUri !== contentUri) { + singleton.tearDown(); + singleton = null; + } + if (!singleton) { + try { + if (contentUri) { + // The FxAccountsWebChannel listens for events and updates + // the state machine accordingly. + singleton = new FxAccountsWebChannel({ + content_uri: contentUri, + channel_id: WEBCHANNEL_ID, + }); + } else { + log.warn("FxA WebChannel functionaly is disabled due to no URI pref."); + } + } catch (ex) { + log.error("Failed to create FxA WebChannel", ex); + } + } +}; diff --git a/services/fxaccounts/components.conf b/services/fxaccounts/components.conf new file mode 100644 index 0000000000..992c88d0cb --- /dev/null +++ b/services/fxaccounts/components.conf @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{1b7db999-2ecd-4abf-bb95-a726896798ca}', + 'contract_ids': ['@mozilla.org/fxaccounts/push;1'], + 'esModule': 'resource://gre/modules/FxAccountsPush.sys.mjs', + 'constructor': 'FxAccountsPushService', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + 'categories': {'push': 'chrome://fxa-device-update'}, + }, +] diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build new file mode 100644 index 0000000000..0047122c2d --- /dev/null +++ b/services/fxaccounts/moz.build @@ -0,0 +1,38 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Firefox Accounts") + +MOCHITEST_CHROME_MANIFESTS += ["tests/mochitest/chrome.toml"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] + +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"] + +EXTRA_JS_MODULES += [ + "Credentials.sys.mjs", + "FxAccounts.sys.mjs", + "FxAccountsClient.sys.mjs", + "FxAccountsCommands.sys.mjs", + "FxAccountsCommon.sys.mjs", + "FxAccountsConfig.sys.mjs", + "FxAccountsDevice.sys.mjs", + "FxAccountsKeys.sys.mjs", + "FxAccountsOAuth.sys.mjs", + "FxAccountsPairing.sys.mjs", + "FxAccountsPairingChannel.sys.mjs", + "FxAccountsProfile.sys.mjs", + "FxAccountsProfileClient.sys.mjs", + "FxAccountsPush.sys.mjs", + "FxAccountsStorage.sys.mjs", + "FxAccountsTelemetry.sys.mjs", + "FxAccountsWebChannel.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/services/fxaccounts/tests/browser/browser.toml b/services/fxaccounts/tests/browser/browser.toml new file mode 100644 index 0000000000..7cb752b59a --- /dev/null +++ b/services/fxaccounts/tests/browser/browser.toml @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_device_connected.js"] + +["browser_verify_login.js"] diff --git a/services/fxaccounts/tests/browser/browser_device_connected.js b/services/fxaccounts/tests/browser/browser_device_connected.js new file mode 100644 index 0000000000..8e567ddf35 --- /dev/null +++ b/services/fxaccounts/tests/browser/browser_device_connected.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); + +const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver +); +const DEVICES_URL = "https://example.com/devices"; + +add_setup(async function () { + const origManageDevicesURI = FxAccounts.config.promiseManageDevicesURI; + FxAccounts.config.promiseManageDevicesURI = () => + Promise.resolve(DEVICES_URL); + setupMockAlertsService(); + + registerCleanupFunction(function () { + FxAccounts.config.promiseManageDevicesURI = origManageDevicesURI; + delete window.FxAccounts; + }); +}); + +async function testDeviceConnected(deviceName) { + info("testDeviceConnected with deviceName=" + deviceName); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:mozilla" + ); + await waitForDocLoadComplete(); + + let waitForTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + + Services.obs.notifyObservers(null, "fxaccounts:device_connected", deviceName); + + let tab = await waitForTabPromise; + Assert.ok("Tab successfully opened"); + + Assert.equal(tab.linkedBrowser.currentURI.spec, DEVICES_URL); + + BrowserTestUtils.removeTab(tab); +} + +add_task(async function () { + await testDeviceConnected("My phone"); +}); + +add_task(async function () { + await testDeviceConnected(null); +}); diff --git a/services/fxaccounts/tests/browser/browser_verify_login.js b/services/fxaccounts/tests/browser/browser_verify_login.js new file mode 100644 index 0000000000..fa9d952a52 --- /dev/null +++ b/services/fxaccounts/tests/browser/browser_verify_login.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + let payload = { + data: { + deviceName: "Laptop", + url: "https://example.com/newLogin", + title: "Sign-in Request", + body: "New sign-in request from vershwal's Nighty on Intel Mac OS X 10.12", + }, + }; + info("testVerifyNewSignin"); + setupMockAlertsService(); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:mozilla" + ); + await waitForDocLoadComplete(); + + let waitForTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + + Services.obs.notifyObservers( + null, + "fxaccounts:verify_login", + JSON.stringify(payload.data) + ); + + let tab = await waitForTabPromise; + Assert.ok("Tab successfully opened"); + Assert.equal(tab.linkedBrowser.currentURI.spec, payload.data.url); + BrowserTestUtils.removeTab(tab); +}); diff --git a/services/fxaccounts/tests/browser/head.js b/services/fxaccounts/tests/browser/head.js new file mode 100644 index 0000000000..e9fb8ad0dc --- /dev/null +++ b/services/fxaccounts/tests/browser/head.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Waits for the next load to complete in any browser or the given browser. + * If a is given it waits for a load in any of its browsers. + * + * @return promise + */ +function waitForDocLoadComplete(aBrowser = gBrowser) { + return new Promise(resolve => { + let listener = { + onStateChange(webProgress, req, flags, status) { + let docStop = + Ci.nsIWebProgressListener.STATE_IS_NETWORK | + Ci.nsIWebProgressListener.STATE_STOP; + info( + "Saw state " + + flags.toString(16) + + " and status " + + status.toString(16) + ); + + // When a load needs to be retargetted to a new process it is cancelled + // with NS_BINDING_ABORTED so ignore that case + if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) { + aBrowser.removeProgressListener(this); + waitForDocLoadComplete.listeners.delete(this); + + let chan = req.QueryInterface(Ci.nsIChannel); + info("Browser loaded " + chan.originalURI.spec); + resolve(); + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + }; + aBrowser.addProgressListener(listener); + waitForDocLoadComplete.listeners.add(listener); + info("Waiting for browser load"); + }); +} + +function setupMockAlertsService() { + const alertsService = { + showAlertNotification: ( + image, + title, + text, + clickable, + cookie, + clickCallback + ) => { + // We are invoking the event handler ourselves directly. + clickCallback(null, "alertclickcallback", null); + }, + }; + const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver + ); + gBrowserGlue.observe( + { wrappedJSObject: alertsService }, + "browser-glue-test", + "mock-alerts-service" + ); +} + +// Keep a set of progress listeners for waitForDocLoadComplete() to make sure +// they're not GC'ed before we saw the page load. +waitForDocLoadComplete.listeners = new Set(); +registerCleanupFunction(() => waitForDocLoadComplete.listeners.clear()); diff --git a/services/fxaccounts/tests/mochitest/chrome.toml b/services/fxaccounts/tests/mochitest/chrome.toml new file mode 100644 index 0000000000..5e88133317 --- /dev/null +++ b/services/fxaccounts/tests/mochitest/chrome.toml @@ -0,0 +1,5 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = ["file_invalidEmailCase.sjs"] + +["test_invalidEmailCase.html"] diff --git a/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs b/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs new file mode 100644 index 0000000000..971cf52bba --- /dev/null +++ b/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This server simulates the behavior of /account/login on the Firefox Accounts + * auth server in the case where the user is trying to sign in with an email + * with the wrong capitalization. + * + * https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountlogin + * + * The expected behavior is that on the first attempt, with the wrong email, + * the server will respond with a 400 and the canonical email capitalization + * that the client should use. The client then has one chance to sign in with + * this different capitalization. + * + * In this test, the user with the account id "Greta.Garbo@gmail.COM" initially + * tries to sign in as "greta.garbo@gmail.com". + * + * On success, the client is responsible for updating its sign-in user state + * and recording the proper email capitalization. + */ + +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const goodEmail = "Greta.Garbo@gmail.COM"; +const badEmail = "greta.garbo@gmail.com"; + +function handleRequest(request, response) { + let body = new BinaryInputStream(request.bodyInputStream); + let bytes = []; + let available; + while ((available = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(available)); + } + + let data = JSON.parse(String.fromCharCode.apply(null, bytes)); + let message; + + switch (data.email) { + case badEmail: + // Almost - try again with fixed email case + message = { + code: 400, + errno: 120, + error: "Incorrect email case", + email: goodEmail, + }; + response.setStatusLine(request.httpVersion, 400, "Almost"); + break; + + case goodEmail: + // Successful login. + message = { + uid: "your-uid", + sessionToken: "your-sessionToken", + keyFetchToken: "your-keyFetchToken", + verified: true, + authAt: 1392144866, + }; + response.setStatusLine(request.httpVersion, 200, "Yay"); + break; + + default: + // Anything else happening in this test is a failure. + message = { + code: 400, + errno: 999, + error: "What happened!?", + }; + response.setStatusLine(request.httpVersion, 400, "Ouch"); + break; + } + + let messageStr = JSON.stringify(message); + response.bodyOutputStream.write(messageStr, messageStr.length); +} diff --git a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html b/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html new file mode 100644 index 0000000000..4b5e943591 --- /dev/null +++ b/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html @@ -0,0 +1,129 @@ + + + + + + Test for Firefox Accounts (Bug 963835) + + + + + +Mozilla Bug 963835 +

+ +
+
+
+ + diff --git a/services/fxaccounts/tests/xpcshell/head.js b/services/fxaccounts/tests/xpcshell/head.js new file mode 100644 index 0000000000..921888e2e3 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/head.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../../../common/tests/unit/head_helpers.js */ +/* import-globals-from ../../../common/tests/unit/head_http.js */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { SCOPE_OLD_SYNC } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +// Some mock key data, in both scoped-key and legacy field formats. +const MOCK_ACCOUNT_KEYS = { + scopedKeys: { + [SCOPE_OLD_SYNC]: { + kid: "1234567890123-u7u7u7u7u7u7u7u7u7u7uw", + k: "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg", + kty: "oct", + }, + }, +}; + +(function initFxAccountsTestingInfrastructure() { + do_get_profile(); + + let { initTestLogging } = ChromeUtils.importESModule( + "resource://testing-common/services/common/logging.sys.mjs" + ); + + initTestLogging("Trace"); +}).call(this); diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js new file mode 100644 index 0000000000..c4aec73a03 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_accounts.js @@ -0,0 +1,1642 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CryptoUtils } = ChromeUtils.importESModule( + "resource://services-crypto/utils.sys.mjs" +); +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { FxAccountsClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsClient.sys.mjs" +); +const { + ERRNO_INVALID_AUTH_TOKEN, + ERROR_NO_ACCOUNT, + FX_OAUTH_CLIENT_ID, + ONLOGIN_NOTIFICATION, + ONLOGOUT_NOTIFICATION, + ONVERIFIED_NOTIFICATION, + DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY, + PREF_LAST_FXA_USER, +} = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +// We grab some additional stuff via backstage passes. +var { AccountState } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); + +const MOCK_TOKEN_RESPONSE = { + access_token: + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69", + token_type: "bearer", + scope: "https://identity.mozilla.com/apps/oldsync", + expires_in: 21600, + auth_at: 1589579900, +}; + +initTestLogging("Trace"); + +var log = Log.repository.getLogger("Services.FxAccounts.test"); +log.level = Log.Level.Debug; + +// See verbose logging from FxAccounts.jsm and jwcrypto.jsm. +Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace"); +Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace; +Services.prefs.setStringPref("services.crypto.jwcrypto.log.level", "Debug"); + +/* + * The FxAccountsClient communicates with the remote Firefox + * Accounts auth server. Mock the server calls, with a little + * lag time to simulate some latency. + * + * We add the _verified attribute to mock the change in verification + * state on the FXA server. + */ + +function MockStorageManager() {} + +MockStorageManager.prototype = { + promiseInitialized: Promise.resolve(), + + initialize(accountData) { + this.accountData = accountData; + }, + + finalize() { + return Promise.resolve(); + }, + + getAccountData(fields = null) { + let result; + if (!this.accountData) { + result = null; + } else if (fields == null) { + // can't use cloneInto as the keys get upset... + result = {}; + for (let field of Object.keys(this.accountData)) { + result[field] = this.accountData[field]; + } + } else { + if (!Array.isArray(fields)) { + fields = [fields]; + } + result = {}; + for (let field of fields) { + result[field] = this.accountData[field]; + } + } + return Promise.resolve(result); + }, + + updateAccountData(updatedFields) { + if (!this.accountData) { + return Promise.resolve(); + } + for (let [name, value] of Object.entries(updatedFields)) { + if (value == null) { + delete this.accountData[name]; + } else { + this.accountData[name] = value; + } + } + return Promise.resolve(); + }, + + deleteAccountData() { + this.accountData = null; + return Promise.resolve(); + }, +}; + +function MockFxAccountsClient() { + this._email = "nobody@example.com"; + this._verified = false; + this._deletedOnServer = false; // for our accountStatus mock + + // mock calls up to the auth server to determine whether the + // user account has been verified + this.recoveryEmailStatus = async function (sessionToken) { + // simulate a call to /recovery_email/status + return { + email: this._email, + verified: this._verified, + }; + }; + + this.accountStatus = async function (uid) { + return !!uid && !this._deletedOnServer; + }; + + this.sessionStatus = async function () { + // If the sessionStatus check says an account is OK, we typically will not + // end up calling accountStatus - so this must return false if accountStatus + // would. + return !this._deletedOnServer; + }; + + this.accountKeys = function (keyFetchToken) { + return new Promise(resolve => { + do_timeout(50, () => { + resolve({ + kA: expandBytes("11"), + wrapKB: expandBytes("22"), + }); + }); + }); + }; + + this.getScopedKeyData = function (sessionToken, client_id, scopes) { + Assert.ok(sessionToken); + Assert.equal(client_id, FX_OAUTH_CLIENT_ID); + Assert.equal(scopes, SCOPE_OLD_SYNC); + return new Promise(resolve => { + do_timeout(50, () => { + resolve({ + "https://identity.mozilla.com/apps/oldsync": { + identifier: "https://identity.mozilla.com/apps/oldsync", + keyRotationSecret: + "0000000000000000000000000000000000000000000000000000000000000000", + keyRotationTimestamp: 1234567890123, + }, + }); + }); + }); + }; + + this.resendVerificationEmail = function (sessionToken) { + // Return the session token to show that we received it in the first place + return Promise.resolve(sessionToken); + }; + + this.signOut = () => Promise.resolve(); + + FxAccountsClient.apply(this); +} +MockFxAccountsClient.prototype = {}; +Object.setPrototypeOf( + MockFxAccountsClient.prototype, + FxAccountsClient.prototype +); +/* + * We need to mock the FxAccounts module's interfaces to external + * services, such as storage and the FxAccounts client. We also + * mock the now() method, so that we can simulate the passing of + * time and verify that signatures expire correctly. + */ +function MockFxAccounts(credentials = null) { + let result = new FxAccounts({ + VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms + + _getCertificateSigned_calls: [], + _d_signCertificate: Promise.withResolvers(), + _now_is: new Date(), + now() { + return this._now_is; + }, + newAccountState(newCredentials) { + // we use a real accountState but mocked storage. + let storage = new MockStorageManager(); + storage.initialize(newCredentials); + return new AccountState(storage); + }, + fxAccountsClient: new MockFxAccountsClient(), + observerPreloads: [], + device: { + _registerOrUpdateDevice() {}, + _checkRemoteCommandsUpdateNeeded: async () => false, + }, + profile: { + getProfile() { + return null; + }, + }, + }); + // and for convenience so we don't have to touch as many lines in this test + // when we refactored FxAccounts.jsm :) + result.setSignedInUser = function (creds) { + return result._internal.setSignedInUser(creds); + }; + return result; +} + +/* + * Some tests want a "real" fxa instance - however, we still mock the storage + * to keep the tests fast on b2g. + */ +async function MakeFxAccounts({ internal = {}, credentials } = {}) { + if (!internal.newAccountState) { + // we use a real accountState but mocked storage. + internal.newAccountState = function (newCredentials) { + let storage = new MockStorageManager(); + storage.initialize(newCredentials); + return new AccountState(storage); + }; + } + if (!internal._signOutServer) { + internal._signOutServer = () => Promise.resolve(); + } + if (internal.device) { + if (!internal.device._registerOrUpdateDevice) { + internal.device._registerOrUpdateDevice = () => Promise.resolve(); + internal.device._checkRemoteCommandsUpdateNeeded = async () => false; + } + } else { + internal.device = { + _registerOrUpdateDevice() {}, + _checkRemoteCommandsUpdateNeeded: async () => false, + }; + } + if (!internal.observerPreloads) { + internal.observerPreloads = []; + } + let result = new FxAccounts(internal); + + if (credentials) { + await result._internal.setSignedInUser(credentials); + } + return result; +} + +add_task(async function test_get_signed_in_user_initially_unset() { + _("Check getSignedInUser initially and after signout reports no user"); + let account = await MakeFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234567890abcdef1234567890abcdef", + sessionToken: "dead", + verified: true, + ...MOCK_ACCOUNT_KEYS, + }; + let result = await account.getSignedInUser(); + Assert.equal(result, null); + + await account._internal.setSignedInUser(credentials); + + // getSignedInUser only returns a subset. + result = await account.getSignedInUser(); + Assert.deepEqual(result.email, credentials.email); + Assert.deepEqual(result.scopedKeys, undefined); + + // for the sake of testing, use the low-level function to check it's all there + result = await account._internal.currentAccountState.getUserAccountData(); + Assert.deepEqual(result.email, credentials.email); + Assert.deepEqual(result.scopedKeys, credentials.scopedKeys); + + // sign out + let localOnly = true; + await account.signOut(localOnly); + + // user should be undefined after sign out + result = await account.getSignedInUser(); + Assert.equal(result, null); +}); + +add_task(async function test_set_signed_in_user_signs_out_previous_account() { + _("Check setSignedInUser signs out the previous account."); + let signOutServerCalled = false; + let credentials = { + email: "foo@example.com", + uid: "1234567890abcdef1234567890abcdef", + sessionToken: "dead", + verified: true, + ...MOCK_ACCOUNT_KEYS, + }; + let account = await MakeFxAccounts({ credentials }); + + account._internal._signOutServer = () => { + signOutServerCalled = true; + return Promise.resolve(true); + }; + + await account._internal.setSignedInUser(credentials); + Assert.ok(signOutServerCalled); +}); + +add_task(async function test_update_account_data() { + _("Check updateUserAccountData does the right thing."); + let credentials = { + email: "foo@example.com", + uid: "1234567890abcdef1234567890abcdef", + sessionToken: "dead", + verified: true, + ...MOCK_ACCOUNT_KEYS, + }; + let account = await MakeFxAccounts({ credentials }); + + let newCreds = { + email: credentials.email, + uid: credentials.uid, + sessionToken: "alive", + }; + await account._internal.updateUserAccountData(newCreds); + Assert.equal( + (await account._internal.getUserAccountData()).sessionToken, + "alive", + "new field value was saved" + ); + + // but we should fail attempting to change the uid. + newCreds = { + email: credentials.email, + uid: "11111111111111111111222222222222", + sessionToken: "alive", + }; + await Assert.rejects( + account._internal.updateUserAccountData(newCreds), + /The specified credentials aren't for the current user/ + ); + + // should fail without the uid. + newCreds = { + sessionToken: "alive", + }; + await Assert.rejects( + account._internal.updateUserAccountData(newCreds), + /The specified credentials have no uid/ + ); + + // and should fail with a field name that's not known by storage. + newCreds = { + email: credentials.email, + uid: "11111111111111111111222222222222", + foo: "bar", + }; + await Assert.rejects( + account._internal.updateUserAccountData(newCreds), + /The specified credentials aren't for the current user/ + ); +}); + +// Sanity-check that our mocked client is working correctly +add_test(function test_client_mock() { + let fxa = new MockFxAccounts(); + let client = fxa._internal.fxAccountsClient; + Assert.equal(client._verified, false); + Assert.equal(typeof client.signIn, "function"); + + // The recoveryEmailStatus function eventually fulfills its promise + client.recoveryEmailStatus().then(response => { + Assert.equal(response.verified, false); + run_next_test(); + }); +}); + +// Sign in a user, and after a little while, verify the user's email. +// Right after signing in the user, we should get the 'onlogin' notification. +// Polling should detect that the email is verified, and eventually +// 'onverified' should be observed +add_test(function test_verification_poll() { + let fxa = new MockFxAccounts(); + let test_user = getTestUser("francine"); + let login_notification_received = false; + + makeObserver(ONVERIFIED_NOTIFICATION, function () { + log.debug("test_verification_poll observed onverified"); + // Once email verification is complete, we will observe onverified + fxa._internal + .getUserAccountData() + .then(user => { + // And confirm that the user's state has changed + Assert.equal(user.verified, true); + Assert.equal(user.email, test_user.email); + Assert.equal( + Services.prefs.getStringPref(PREF_LAST_FXA_USER), + CryptoUtils.sha256Base64(test_user.email) + ); + Assert.ok(login_notification_received); + }) + .finally(run_next_test); + }); + + makeObserver(ONLOGIN_NOTIFICATION, function () { + log.debug("test_verification_poll observer onlogin"); + login_notification_received = true; + }); + + fxa.setSignedInUser(test_user).then(() => { + fxa._internal.getUserAccountData().then(user => { + // The user is signing in, but email has not been verified yet + Assert.equal(user.verified, false); + do_timeout(200, function () { + log.debug("Mocking verification of francine's email"); + fxa._internal.fxAccountsClient._email = test_user.email; + fxa._internal.fxAccountsClient._verified = true; + }); + }); + }); +}); + +// Sign in the user, but never verify the email. The check-email +// poll should time out. No verifiedlogin event should be observed, and the +// internal whenVerified promise should be rejected +add_test(function test_polling_timeout() { + // This test could be better - the onverified observer might fire on + // somebody else's stack, and we're not making sure that we're not receiving + // such a message. In other words, this tests either failure, or success, but + // not both. + + let fxa = new MockFxAccounts(); + let test_user = getTestUser("carol"); + + let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function () { + do_throw("We should not be getting a login event!"); + }); + + fxa._internal.POLL_SESSION = 1; + + let p = fxa._internal.whenVerified({}); + + fxa.setSignedInUser(test_user).then(() => { + p.then( + success => { + do_throw("this should not succeed"); + }, + fail => { + removeObserver(); + fxa.signOut().then(run_next_test); + } + ); + }); +}); + +// For bug 1585299 - ensure we only get a single ONVERIFIED notification. +add_task(async function test_onverified_once() { + let fxa = new MockFxAccounts(); + let user = getTestUser("francine"); + + let numNotifications = 0; + + function observe(aSubject, aTopic, aData) { + numNotifications += 1; + } + Services.obs.addObserver(observe, ONVERIFIED_NOTIFICATION); + + fxa._internal.POLL_SESSION = 1; + + await fxa.setSignedInUser(user); + + Assert.ok(!(await fxa.getSignedInUser()).verified, "starts unverified"); + + await fxa._internal.startPollEmailStatus( + fxa._internal.currentAccountState, + user.sessionToken, + "start" + ); + + Assert.ok(!(await fxa.getSignedInUser()).verified, "still unverified"); + + log.debug("Mocking verification of francine's email"); + fxa._internal.fxAccountsClient._email = user.email; + fxa._internal.fxAccountsClient._verified = true; + + await fxa._internal.startPollEmailStatus( + fxa._internal.currentAccountState, + user.sessionToken, + "again" + ); + + Assert.ok((await fxa.getSignedInUser()).verified, "now verified"); + + Assert.equal(numNotifications, 1, "expect exactly 1 ONVERIFIED"); + + Services.obs.removeObserver(observe, ONVERIFIED_NOTIFICATION); + await fxa.signOut(); +}); + +add_test(function test_pollEmailStatus_start_verified() { + let fxa = new MockFxAccounts(); + let test_user = getTestUser("carol"); + + fxa._internal.POLL_SESSION = 20 * 60000; + fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 50000; + + fxa.setSignedInUser(test_user).then(() => { + fxa._internal.getUserAccountData().then(user => { + fxa._internal.fxAccountsClient._email = test_user.email; + fxa._internal.fxAccountsClient._verified = true; + const mock = sinon.mock(fxa._internal); + mock.expects("_scheduleNextPollEmailStatus").never(); + fxa._internal + .startPollEmailStatus( + fxa._internal.currentAccountState, + user.sessionToken, + "start" + ) + .then(() => { + mock.verify(); + mock.restore(); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_pollEmailStatus_start() { + let fxa = new MockFxAccounts(); + let test_user = getTestUser("carol"); + + fxa._internal.POLL_SESSION = 20 * 60000; + fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456; + + fxa.setSignedInUser(test_user).then(() => { + fxa._internal.getUserAccountData().then(user => { + const mock = sinon.mock(fxa._internal); + mock + .expects("_scheduleNextPollEmailStatus") + .once() + .withArgs( + fxa._internal.currentAccountState, + user.sessionToken, + 123456, + "start" + ); + fxa._internal + .startPollEmailStatus( + fxa._internal.currentAccountState, + user.sessionToken, + "start" + ) + .then(() => { + mock.verify(); + mock.restore(); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_pollEmailStatus_start_subsequent() { + let fxa = new MockFxAccounts(); + let test_user = getTestUser("carol"); + + fxa._internal.POLL_SESSION = 20 * 60000; + fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456; + fxa._internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321; + fxa._internal.VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD = -1; + + fxa.setSignedInUser(test_user).then(() => { + fxa._internal.getUserAccountData().then(user => { + const mock = sinon.mock(fxa._internal); + mock + .expects("_scheduleNextPollEmailStatus") + .once() + .withArgs( + fxa._internal.currentAccountState, + user.sessionToken, + 654321, + "start" + ); + fxa._internal + .startPollEmailStatus( + fxa._internal.currentAccountState, + user.sessionToken, + "start" + ) + .then(() => { + mock.verify(); + mock.restore(); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_pollEmailStatus_browser_startup() { + let fxa = new MockFxAccounts(); + let test_user = getTestUser("carol"); + + fxa._internal.POLL_SESSION = 20 * 60000; + fxa._internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321; + + fxa.setSignedInUser(test_user).then(() => { + fxa._internal.getUserAccountData().then(user => { + const mock = sinon.mock(fxa._internal); + mock + .expects("_scheduleNextPollEmailStatus") + .once() + .withArgs( + fxa._internal.currentAccountState, + user.sessionToken, + 654321, + "browser-startup" + ); + fxa._internal + .startPollEmailStatus( + fxa._internal.currentAccountState, + user.sessionToken, + "browser-startup" + ) + .then(() => { + mock.verify(); + mock.restore(); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_pollEmailStatus_push() { + let fxa = new MockFxAccounts(); + let test_user = getTestUser("carol"); + + fxa.setSignedInUser(test_user).then(() => { + fxa._internal.getUserAccountData().then(user => { + const mock = sinon.mock(fxa._internal); + mock.expects("_scheduleNextPollEmailStatus").never(); + fxa._internal + .startPollEmailStatus( + fxa._internal.currentAccountState, + user.sessionToken, + "push" + ) + .then(() => { + mock.verify(); + mock.restore(); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_getKeyForScope() { + let fxa = new MockFxAccounts(); + let user = getTestUser("eusebius"); + + // Once email has been verified, we will be able to get keys + user.verified = true; + + fxa.setSignedInUser(user).then(() => { + fxa._internal.getUserAccountData().then(user2 => { + // Before getKeyForScope, we have no keys + Assert.equal(!!user2.scopedKeys, false); + // And we still have a key-fetch token and unwrapBKey to use + Assert.equal(!!user2.keyFetchToken, true); + Assert.equal(!!user2.unwrapBKey, true); + + fxa.keys.getKeyForScope(SCOPE_OLD_SYNC).then(() => { + fxa._internal.getUserAccountData().then(user3 => { + // Now we should have keys + Assert.equal(fxa._internal.isUserEmailVerified(user3), true); + Assert.equal(!!user3.verified, true); + Assert.notEqual(null, user3.scopedKeys); + Assert.equal(user3.keyFetchToken, undefined); + Assert.equal(user3.unwrapBKey, undefined); + run_next_test(); + }); + }); + }); + }); +}); + +add_task( + async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_high_level_keys() { + let fxa = new MockFxAccounts(); + let user = getTestUser("eusebius"); + + user.verified = true; + + // An account state with the deprecated kinto extension sync keys... + user.kExtSync = + "f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" + + "5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395"; + user.kExtKbHash = + "6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273"; + user.scopedKeys = { + ...MOCK_ACCOUNT_KEYS.scopedKeys, + }; + + await fxa.setSignedInUser(user); + // getKeyForScope will run the migration + await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC); + let newUser = await fxa._internal.getUserAccountData(); + // Then, the deprecated keys will be removed + Assert.strictEqual(newUser.kExtSync, undefined); + Assert.strictEqual(newUser.kExtKbHash, undefined); + } +); + +add_task( + async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_scoped_keys() { + let fxa = new MockFxAccounts(); + let user = getTestUser("eusebius"); + const DEPRECATED_SCOPE_WEBEXT_SYNC = "sync:addon_storage"; + const EXTRA_SCOPE = "an unknown, but non-deprecated scope"; + user.verified = true; + user.ecosystemUserId = "ecoUserId"; + user.ecosystemAnonId = "ecoAnonId"; + user.scopedKeys = { + ...MOCK_ACCOUNT_KEYS.scopedKeys, + [DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY]: + MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC], + [DEPRECATED_SCOPE_WEBEXT_SYNC]: + MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC], + [EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC], + }; + + await fxa.setSignedInUser(user); + await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC); + let newUser = await fxa._internal.getUserAccountData(); + // It should have removed the deprecated ecosystem_telemetry key, + // and the old kinto extension sync key + // but left the other keys intact. + const expectedScopedKeys = { + ...MOCK_ACCOUNT_KEYS.scopedKeys, + [EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC], + }; + Assert.deepEqual(newUser.scopedKeys, expectedScopedKeys); + Assert.equal(newUser.ecosystemUserId, null); + Assert.equal(newUser.ecosystemAnonId, null); + } +); + +add_task(async function test_getKeyForScope_nonexistent_account() { + let fxa = new MockFxAccounts(); + let bismarck = getTestUser("bismarck"); + + let client = fxa._internal.fxAccountsClient; + client.accountStatus = () => Promise.resolve(false); + client.sessionStatus = () => Promise.resolve(false); + client.accountKeys = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + + await fxa.setSignedInUser(bismarck); + + let promiseLogout = new Promise(resolve => { + makeObserver(ONLOGOUT_NOTIFICATION, function () { + log.debug("test_getKeyForScope_nonexistent_account observed logout"); + resolve(); + }); + }); + + // XXX - the exception message here isn't ideal, but doesn't really matter... + await Assert.rejects( + fxa.keys.getKeyForScope(SCOPE_OLD_SYNC), + /A different user signed in/ + ); + + await promiseLogout; + + let user = await fxa._internal.getUserAccountData(); + Assert.equal(user, null); +}); + +// getKeyForScope with invalid keyFetchToken should delete keyFetchToken from storage +add_task(async function test_getKeyForScope_invalid_token() { + let fxa = new MockFxAccounts(); + let yusuf = getTestUser("yusuf"); + + let client = fxa._internal.fxAccountsClient; + client.accountStatus = () => Promise.resolve(true); // account exists. + client.sessionStatus = () => Promise.resolve(false); // session is invalid. + client.accountKeys = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + + await fxa.setSignedInUser(yusuf); + let user = await fxa._internal.getUserAccountData(); + Assert.notEqual(user.encryptedSendTabKeys, null); + + try { + await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC); + Assert.ok(false); + } catch (err) { + Assert.equal(err.code, 401); + Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN); + } + + user = await fxa._internal.getUserAccountData(); + Assert.equal(user.email, yusuf.email); + Assert.equal(user.keyFetchToken, null); + // We verify that encryptedSendTabKeys are also wiped + // when a user's credentials are wiped + Assert.equal(user.encryptedSendTabKeys, null); + await fxa._internal.abortExistingFlow(); +}); + +// Test vectors from +// https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#Test_Vectors +add_task(async function test_getKeyForScope_oldsync() { + let fxa = new MockFxAccounts(); + let client = fxa._internal.fxAccountsClient; + client.getScopedKeyData = () => + Promise.resolve({ + "https://identity.mozilla.com/apps/oldsync": { + identifier: "https://identity.mozilla.com/apps/oldsync", + keyRotationSecret: + "0000000000000000000000000000000000000000000000000000000000000000", + keyRotationTimestamp: 1510726317123, + }, + }); + + // We mock the server returning the wrapKB from our test vectors + client.accountKeys = async () => { + return { + wrapKB: CommonUtils.hexToBytes( + "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f" + ), + }; + }; + + // We set the user to have the keyFetchToken and unwrapBKey from our test vectors + let user = { + ...getTestUser("eusebius"), + uid: "aeaa1725c7a24ff983c6295725d5fc9b", + keyFetchToken: + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + unwrapBKey: + "6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a", + sessionToken: "mock session token, used in metadata request", + verified: true, + }; + await fxa.setSignedInUser(user); + + // We derive, persist and return the sync key + const key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC); + + // We verify the key returned matches what we would expect from the test vectors + // kb = 2ee722fdd8ccaa721bdeb2d1b76560efef705b04349d9357c3e592cf4906e075 (from test vectors) + // + // kid can be verified by "${keyRotationTimestamp}-${sha256(kb)[0:16]}" + // + // k can be verified by HKDF(kb, undefined, "identity.mozilla.com/picl/v1/oldsync", 64) + Assert.deepEqual(key, { + scope: SCOPE_OLD_SYNC, + kid: "1510726317123-BAik7hEOdpGnPZnPBSdaTg", + k: "fwM5VZu0Spf5XcFRZYX2zk6MrqZP7zvovCBcvuKwgYMif3hz98FHmIVa3qjKjrW0J244Zj-P5oWaOcQbvypmpw", + kty: "oct", + }); +}); + +add_task(async function test_getScopedKeys_cached_key() { + let fxa = new MockFxAccounts(); + let user = { + ...getTestUser("eusebius"), + uid: "aeaa1725c7a24ff983c6295725d5fc9b", + verified: true, + ...MOCK_ACCOUNT_KEYS, + }; + + await fxa.setSignedInUser(user); + let key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC); + Assert.deepEqual(key, { + scope: SCOPE_OLD_SYNC, + ...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC], + }); +}); + +add_task(async function test_getScopedKeys_unavailable_scope() { + let fxa = new MockFxAccounts(); + let user = { + ...getTestUser("eusebius"), + uid: "aeaa1725c7a24ff983c6295725d5fc9b", + verified: true, + ...MOCK_ACCOUNT_KEYS, + }; + await fxa.setSignedInUser(user); + await Assert.rejects( + fxa.keys.getKeyForScope("otherkeybearingscope"), + /Key not available for scope/ + ); +}); + +add_task(async function test_getScopedKeys_misconfigured_fxa_server() { + let fxa = new MockFxAccounts(); + let client = fxa._internal.fxAccountsClient; + client.getScopedKeyData = () => + Promise.resolve({ + wrongscope: { + identifier: "wrongscope", + keyRotationSecret: + "0000000000000000000000000000000000000000000000000000000000000000", + keyRotationTimestamp: 1510726331712, + }, + }); + let user = { + ...getTestUser("eusebius"), + uid: "aeaa1725c7a24ff983c6295725d5fc9b", + verified: true, + keyFetchToken: + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + unwrapBKey: + "6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a", + sessionToken: "mock session token, used in metadata request", + }; + await fxa.setSignedInUser(user); + await Assert.rejects( + fxa.keys.getKeyForScope(SCOPE_OLD_SYNC), + /The FxA server did not grant Firefox the `oldsync` scope/ + ); +}); + +add_task(async function test_setScopedKeys() { + const fxa = new MockFxAccounts(); + const user = { + ...getTestUser("foo"), + verified: true, + }; + await fxa.setSignedInUser(user); + await fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys); + const key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC); + Assert.deepEqual(key, { + scope: SCOPE_OLD_SYNC, + ...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC], + }); +}); + +add_task(async function test_setScopedKeys_user_not_signed_in() { + const fxa = new MockFxAccounts(); + await Assert.rejects( + fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys), + /Cannot persist keys, no user signed in/ + ); +}); + +// _fetchAndUnwrapAndDeriveKeys with no keyFetchToken should trigger signOut +// XXX - actually, it probably shouldn't - bug 1572313. +add_test(function test_fetchAndUnwrapAndDeriveKeys_no_token() { + let fxa = new MockFxAccounts(); + let user = getTestUser("lettuce.protheroe"); + delete user.keyFetchToken; + + makeObserver(ONLOGOUT_NOTIFICATION, function () { + log.debug("test_fetchAndUnwrapKeys_no_token observed logout"); + fxa._internal.getUserAccountData().then(user2 => { + fxa._internal.abortExistingFlow().then(run_next_test); + }); + }); + + fxa + .setSignedInUser(user) + .then(user2 => { + return fxa.keys._fetchAndUnwrapAndDeriveKeys(); + }) + .catch(error => { + log.info("setSignedInUser correctly rejected"); + }); +}); + +// Alice (User A) signs up but never verifies her email. Then Bob (User B) +// signs in with a verified email. Ensure that no sign-in events are triggered +// on Alice's behalf. In the end, Bob should be the signed-in user. +add_test(function test_overlapping_signins() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + let bob = getTestUser("bob"); + + makeObserver(ONVERIFIED_NOTIFICATION, function () { + log.debug("test_overlapping_signins observed onverified"); + // Once email verification is complete, we will observe onverified + fxa._internal.getUserAccountData().then(user => { + Assert.equal(user.email, bob.email); + Assert.equal(user.verified, true); + run_next_test(); + }); + }); + + // Alice is the user signing in; her email is unverified. + fxa.setSignedInUser(alice).then(() => { + log.debug("Alice signing in ..."); + fxa._internal.getUserAccountData().then(user => { + Assert.equal(user.email, alice.email); + Assert.equal(user.verified, false); + log.debug("Alice has not verified her email ..."); + + // Now Bob signs in instead and actually verifies his email + log.debug("Bob signing in ..."); + fxa.setSignedInUser(bob).then(() => { + do_timeout(200, function () { + // Mock email verification ... + log.debug("Bob verifying his email ..."); + fxa._internal.fxAccountsClient._verified = true; + }); + }); + }); + }); +}); + +add_task(async function test_resend_email_not_signed_in() { + let fxa = new MockFxAccounts(); + + try { + await fxa.resendVerificationEmail(); + } catch (err) { + Assert.equal(err.message, ERROR_NO_ACCOUNT); + return; + } + do_throw("Should not be able to resend email when nobody is signed in"); +}); + +add_task(async function test_accountStatus() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + + // If we have no user, we have no account server-side + let result = await fxa.checkAccountStatus(); + Assert.ok(!result); + // Set a user - the fxAccountsClient mock will say "ok". + await fxa.setSignedInUser(alice); + result = await fxa.checkAccountStatus(); + Assert.ok(result); + // flag the item as deleted on the server. + fxa._internal.fxAccountsClient._deletedOnServer = true; + result = await fxa.checkAccountStatus(); + Assert.ok(!result); + fxa._internal.fxAccountsClient._deletedOnServer = false; + await fxa.signOut(); +}); + +add_task(async function test_resend_email_invalid_token() { + let fxa = new MockFxAccounts(); + let sophia = getTestUser("sophia"); + Assert.notEqual(sophia.sessionToken, null); + + let client = fxa._internal.fxAccountsClient; + client.resendVerificationEmail = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + // This test wants the account to exist but the local session invalid. + client.accountStatus = uid => { + Assert.ok(uid, "got a uid to check"); + return Promise.resolve(true); + }; + client.sessionStatus = token => { + Assert.ok(token, "got a token to check"); + return Promise.resolve(false); + }; + + await fxa.setSignedInUser(sophia); + let user = await fxa._internal.getUserAccountData(); + Assert.equal(user.email, sophia.email); + Assert.equal(user.verified, false); + log.debug("Sophia wants verification email resent"); + + try { + await fxa.resendVerificationEmail(); + Assert.ok( + false, + "resendVerificationEmail should reject invalid session token" + ); + } catch (err) { + Assert.equal(err.code, 401); + Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN); + } + + user = await fxa._internal.getUserAccountData(); + Assert.equal(user.email, sophia.email); + Assert.equal(user.sessionToken, null); + await fxa._internal.abortExistingFlow(); +}); + +add_test(function test_resend_email() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + + let initialState = fxa._internal.currentAccountState; + + // Alice is the user signing in; her email is unverified. + fxa.setSignedInUser(alice).then(() => { + log.debug("Alice signing in"); + + // We're polling for the first email + Assert.ok(fxa._internal.currentAccountState !== initialState); + let aliceState = fxa._internal.currentAccountState; + + // The polling timer is ticking + Assert.ok(fxa._internal.currentTimer > 0); + + fxa._internal.getUserAccountData().then(user => { + Assert.equal(user.email, alice.email); + Assert.equal(user.verified, false); + log.debug("Alice wants verification email resent"); + + fxa.resendVerificationEmail().then(result => { + // Mock server response; ensures that the session token actually was + // passed to the client to make the hawk call + Assert.equal(result, "alice's session token"); + + // Timer was not restarted + Assert.ok(fxa._internal.currentAccountState === aliceState); + + // Timer is still ticking + Assert.ok(fxa._internal.currentTimer > 0); + + // Ok abort polling before we go on to the next test + fxa._internal.abortExistingFlow(); + run_next_test(); + }); + }); + }); +}); + +Services.prefs.setStringPref( + "identity.fxaccounts.remote.oauth.uri", + "https://example.com/v1" +); + +add_test(async function test_getOAuthTokenWithSessionToken() { + Services.prefs.setBoolPref( + "identity.fxaccounts.useSessionTokensForOAuth", + true + ); + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let oauthTokenCalled = false; + + let client = fxa._internal.fxAccountsClient; + client.accessTokenWithSessionToken = async ( + sessionTokenHex, + clientId, + scope, + ttl + ) => { + oauthTokenCalled = true; + Assert.equal(sessionTokenHex, "alice's session token"); + Assert.equal(clientId, "5882386c6d801776"); + Assert.equal(scope, "profile"); + Assert.equal(ttl, undefined); + return MOCK_TOKEN_RESPONSE; + }; + + await fxa.setSignedInUser(alice); + const result = await fxa.getOAuthToken({ scope: "profile" }); + Assert.ok(oauthTokenCalled); + Assert.equal(result, MOCK_TOKEN_RESPONSE.access_token); + Services.prefs.setBoolPref( + "identity.fxaccounts.useSessionTokensForOAuth", + false + ); + run_next_test(); +}); + +add_task(async function test_getOAuthTokenCachedWithSessionToken() { + Services.prefs.setBoolPref( + "identity.fxaccounts.useSessionTokensForOAuth", + true + ); + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let numOauthTokenCalls = 0; + + let client = fxa._internal.fxAccountsClient; + client.accessTokenWithSessionToken = async () => { + numOauthTokenCalls++; + return MOCK_TOKEN_RESPONSE; + }; + + await fxa.setSignedInUser(alice); + let result = await fxa.getOAuthToken({ + scope: "profile", + service: "test-service", + }); + Assert.equal(numOauthTokenCalls, 1); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); + + // requesting it again should not re-fetch the token. + result = await fxa.getOAuthToken({ + scope: "profile", + service: "test-service", + }); + Assert.equal(numOauthTokenCalls, 1); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); + // But requesting the same service and a different scope *will* get a new one. + result = await fxa.getOAuthToken({ + scope: "something-else", + service: "test-service", + }); + Assert.equal(numOauthTokenCalls, 2); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); + Services.prefs.setBoolPref( + "identity.fxaccounts.useSessionTokensForOAuth", + false + ); +}); + +add_test(function test_getOAuthTokenScopedWithSessionToken() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let numOauthTokenCalls = 0; + + let client = fxa._internal.fxAccountsClient; + client.accessTokenWithSessionToken = async ( + _sessionTokenHex, + _clientId, + scopeString + ) => { + equal(scopeString, "bar foo"); // scopes are sorted locally before request. + numOauthTokenCalls++; + return MOCK_TOKEN_RESPONSE; + }; + + fxa.setSignedInUser(alice).then(() => { + fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => { + Assert.equal(numOauthTokenCalls, 1); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); + run_next_test(); + }); + }); +}); + +add_task(async function test_getOAuthTokenCachedScopeNormalization() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let numOAuthTokenCalls = 0; + + let client = fxa._internal.fxAccountsClient; + client.accessTokenWithSessionToken = async ( + _sessionTokenHex, + _clientId, + scopeString + ) => { + numOAuthTokenCalls++; + return MOCK_TOKEN_RESPONSE; + }; + + await fxa.setSignedInUser(alice); + let result = await fxa.getOAuthToken({ + scope: ["foo", "bar"], + service: "test-service", + }); + Assert.equal(numOAuthTokenCalls, 1); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); + + // requesting it again with the scope array in a different order should not re-fetch the token. + result = await fxa.getOAuthToken({ + scope: ["bar", "foo"], + service: "test-service", + }); + Assert.equal(numOAuthTokenCalls, 1); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); + // requesting it again with the scope array in different case should not re-fetch the token. + result = await fxa.getOAuthToken({ + scope: ["Bar", "Foo"], + service: "test-service", + }); + Assert.equal(numOAuthTokenCalls, 1); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); + // But requesting with a new entry in the array does fetch one. + result = await fxa.getOAuthToken({ + scope: ["foo", "bar", "etc"], + service: "test-service", + }); + Assert.equal(numOAuthTokenCalls, 2); + Assert.equal( + result, + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" + ); +}); + +add_test(function test_getOAuthToken_invalid_param() { + let fxa = new MockFxAccounts(); + + fxa.getOAuthToken().catch(err => { + Assert.equal(err.message, "INVALID_PARAMETER"); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_invalid_scope_array() { + let fxa = new MockFxAccounts(); + + fxa.getOAuthToken({ scope: [] }).catch(err => { + Assert.equal(err.message, "INVALID_PARAMETER"); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_misconfigure_oauth_uri() { + let fxa = new MockFxAccounts(); + + const prevServerURL = Services.prefs.getStringPref( + "identity.fxaccounts.remote.oauth.uri" + ); + Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri"); + + fxa.getOAuthToken().catch(err => { + Assert.equal(err.message, "INVALID_PARAMETER"); + // revert the pref + Services.prefs.setStringPref( + "identity.fxaccounts.remote.oauth.uri", + prevServerURL + ); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_no_account() { + let fxa = new MockFxAccounts(); + + fxa._internal.currentAccountState.getUserAccountData = function () { + return Promise.resolve(null); + }; + + fxa.getOAuthToken({ scope: "profile" }).catch(err => { + Assert.equal(err.message, "NO_ACCOUNT"); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_unverified() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + + fxa.setSignedInUser(alice).then(() => { + fxa.getOAuthToken({ scope: "profile" }).catch(err => { + Assert.equal(err.message, "UNVERIFIED_ACCOUNT"); + fxa.signOut().then(run_next_test); + }); + }); +}); + +add_test(function test_getOAuthToken_error() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + let client = fxa._internal.fxAccountsClient; + client.accessTokenWithSessionToken = () => { + return Promise.reject("boom"); + }; + + fxa.setSignedInUser(alice).then(() => { + fxa.getOAuthToken({ scope: "profile" }).catch(err => { + equal(err.details, "boom"); + run_next_test(); + }); + }); +}); + +add_task(async function test_listAttachedOAuthClients() { + const ONE_HOUR = 60 * 60 * 1000; + const ONE_DAY = 24 * ONE_HOUR; + + const timestamp = Date.now(); + + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + let client = fxa._internal.fxAccountsClient; + client.attachedClients = async () => { + return { + body: [ + // This entry was previously filtered but no longer is! + { + clientId: "a2270f727f45f648", + deviceId: "deadbeef", + sessionTokenId: null, + name: "Firefox Preview (no session token)", + scope: ["profile", "https://identity.mozilla.com/apps/oldsync"], + lastAccessTime: Date.now(), + }, + { + clientId: "802d56ef2a9af9fa", + deviceId: null, + sessionTokenId: null, + name: "Firefox Monitor", + scope: ["profile"], + lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR, + }, + { + clientId: "1f30e32975ae5112", + deviceId: null, + sessionTokenId: null, + name: "Firefox Send", + scope: ["profile", "https://identity.mozilla.com/apps/send"], + lastAccessTime: Date.now() - ONE_DAY * 2 - ONE_HOUR, + }, + // One with a future date should be impossible, but having a negative + // result here would almost certainly confuse something! + { + clientId: "future-date", + deviceId: null, + sessionTokenId: null, + name: "Whatever", + lastAccessTime: Date.now() + ONE_DAY, + }, + // A missing/null lastAccessTime should end up with a missing lastAccessedDaysAgo + { + clientId: "missing-date", + deviceId: null, + sessionTokenId: null, + name: "Whatever", + }, + ], + headers: { "x-timestamp": timestamp.toString() }, + }; + }; + + await fxa.setSignedInUser(alice); + const clients = await fxa.listAttachedOAuthClients(); + Assert.deepEqual(clients, [ + { + id: "a2270f727f45f648", + lastAccessedDaysAgo: 0, + }, + { + id: "802d56ef2a9af9fa", + lastAccessedDaysAgo: 1, + }, + { + id: "1f30e32975ae5112", + lastAccessedDaysAgo: 2, + }, + { + id: "future-date", + lastAccessedDaysAgo: 0, + }, + { + id: "missing-date", + lastAccessedDaysAgo: null, + }, + ]); +}); + +add_task(async function test_getSignedInUserProfile() { + let alice = getTestUser("alice"); + alice.verified = true; + + let mockProfile = { + getProfile() { + return Promise.resolve({ avatar: "image" }); + }, + tearDown() {}, + }; + let fxa = new FxAccounts({ + _signOutServer() { + return Promise.resolve(); + }, + device: { + _registerOrUpdateDevice() { + return Promise.resolve(); + }, + }, + }); + + await fxa._internal.setSignedInUser(alice); + fxa._internal._profile = mockProfile; + let result = await fxa.getSignedInUser(); + Assert.ok(!!result); + Assert.equal(result.avatar, "image"); +}); + +add_task(async function test_getSignedInUserProfile_error_uses_account_data() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa._internal.getSignedInUser = function () { + return Promise.resolve({ email: "foo@bar.com" }); + }; + fxa._internal._profile = { + getProfile() { + return Promise.reject("boom"); + }, + tearDown() { + teardownCalled = true; + }, + }; + + let teardownCalled = false; + await fxa.setSignedInUser(alice); + let result = await fxa.getSignedInUser(); + Assert.deepEqual(result.avatar, null); + await fxa.signOut(); + Assert.ok(teardownCalled); +}); + +add_task(async function test_checkVerificationStatusFailed() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + let client = fxa._internal.fxAccountsClient; + client.recoveryEmailStatus = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + client.accountStatus = () => Promise.resolve(true); + client.sessionStatus = () => Promise.resolve(false); + + await fxa.setSignedInUser(alice); + let user = await fxa._internal.getUserAccountData(); + Assert.notEqual(alice.sessionToken, null); + Assert.equal(user.email, alice.email); + Assert.equal(user.verified, true); + + await fxa._internal.checkVerificationStatus(); + + user = await fxa._internal.getUserAccountData(); + Assert.equal(user.email, alice.email); + Assert.equal(user.sessionToken, null); +}); + +add_task(async function test_flushLogFile() { + _("Tests flushLogFile"); + let account = await MakeFxAccounts(); + let promiseObserved = new Promise(res => { + log.info("Adding flush-log-file observer."); + Services.obs.addObserver(function onFlushLogFile() { + Services.obs.removeObserver( + onFlushLogFile, + "service:log-manager:flush-log-file" + ); + res(); + }, "service:log-manager:flush-log-file"); + }); + account.flushLogFile(); + await promiseObserved; +}); + +/* + * End of tests. + * Utility functions follow. + */ + +function expandHex(two_hex) { + // Return a 64-character hex string, encoding 32 identical bytes. + let eight_hex = two_hex + two_hex + two_hex + two_hex; + let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex; + return thirtytwo_hex + thirtytwo_hex; +} + +function expandBytes(two_hex) { + return CommonUtils.hexToBytes(expandHex(two_hex)); +} + +function getTestUser(name) { + return { + email: name + "@example.com", + uid: "1ad7f5024cc74ec1a209071fd2fae348", + sessionToken: name + "'s session token", + keyFetchToken: name + "'s keyfetch token", + unwrapBKey: expandHex("44"), + verified: false, + encryptedSendTabKeys: name + "'s encrypted Send tab keys", + }; +} + +function makeObserver(aObserveTopic, aObserveFunc) { + let observer = { + // nsISupports provides type management in C++ + // nsIObserver is to be an observer + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(aSubject, aTopic, aData) { + log.debug("observed " + aTopic + " " + aData); + if (aTopic == aObserveTopic) { + removeMe(); + aObserveFunc(aSubject, aTopic, aData); + } + }, + }; + + function removeMe() { + log.debug("removing observer for " + aObserveTopic); + Services.obs.removeObserver(observer, aObserveTopic); + } + + Services.obs.addObserver(observer, aObserveTopic); + return removeMe; +} diff --git a/services/fxaccounts/tests/xpcshell/test_accounts_config.js b/services/fxaccounts/tests/xpcshell/test_accounts_config.js new file mode 100644 index 0000000000..33ace13c47 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_accounts_config.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); + +add_task( + async function test_non_https_remote_server_uri_with_requireHttps_false() { + Services.prefs.setBoolPref("identity.fxaccounts.allowHttp", true); + Services.prefs.setStringPref( + "identity.fxaccounts.remote.root", + "http://example.com/" + ); + Assert.equal( + await FxAccounts.config.promiseConnectAccountURI("test"), + "http://example.com/?context=fx_desktop_v3&entrypoint=test&action=email&service=sync" + ); + + Services.prefs.clearUserPref("identity.fxaccounts.remote.root"); + Services.prefs.clearUserPref("identity.fxaccounts.allowHttp"); + } +); + +add_task(async function test_non_https_remote_server_uri() { + Services.prefs.setStringPref( + "identity.fxaccounts.remote.root", + "http://example.com/" + ); + await Assert.rejects( + FxAccounts.config.promiseConnectAccountURI(), + /Firefox Accounts server must use HTTPS/ + ); + Services.prefs.clearUserPref("identity.fxaccounts.remote.root"); +}); + +add_task(async function test_is_production_config() { + // should start with no auto-config URL. + Assert.ok(!FxAccounts.config.getAutoConfigURL()); + // which means we are using prod. + Assert.ok(FxAccounts.config.isProductionConfig()); + + // Set an auto-config URL. + Services.prefs.setStringPref( + "identity.fxaccounts.autoconfig.uri", + "http://x" + ); + Assert.equal(FxAccounts.config.getAutoConfigURL(), "http://x"); + Assert.ok(!FxAccounts.config.isProductionConfig()); + + // Clear the auto-config URL, but set one of the other config params. + Services.prefs.clearUserPref("identity.fxaccounts.autoconfig.uri"); + Services.prefs.setStringPref("identity.sync.tokenserver.uri", "http://t"); + Assert.ok(!FxAccounts.config.isProductionConfig()); + Services.prefs.clearUserPref("identity.sync.tokenserver.uri"); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js new file mode 100644 index 0000000000..68337eb69e --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js @@ -0,0 +1,1204 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { FxAccountsClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsClient.sys.mjs" +); +const { FxAccountsDevice } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsDevice.sys.mjs" +); +const { + ERRNO_DEVICE_SESSION_CONFLICT, + ERRNO_TOO_MANY_CLIENT_REQUESTS, + ERRNO_UNKNOWN_DEVICE, + ON_DEVICE_CONNECTED_NOTIFICATION, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + ON_DEVICELIST_UPDATED, +} = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); +var { AccountState } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); + +initTestLogging("Trace"); + +var log = Log.repository.getLogger("Services.FxAccounts.test"); +log.level = Log.Level.Debug; + +const BOGUS_PUBLICKEY = + "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc"; +const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw"; + +Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace"); + +const DEVICE_REGISTRATION_VERSION = 42; + +function MockStorageManager() {} + +MockStorageManager.prototype = { + initialize(accountData) { + this.accountData = accountData; + }, + + finalize() { + return Promise.resolve(); + }, + + getAccountData() { + return Promise.resolve(this.accountData); + }, + + updateAccountData(updatedFields) { + for (let [name, value] of Object.entries(updatedFields)) { + if (value == null) { + delete this.accountData[name]; + } else { + this.accountData[name] = value; + } + } + return Promise.resolve(); + }, + + deleteAccountData() { + this.accountData = null; + return Promise.resolve(); + }, +}; + +function MockFxAccountsClient(device) { + this._email = "nobody@example.com"; + // Be careful relying on `this._verified` as it doesn't change if the user's + // state does via setting the `verified` flag in the user data. + this._verified = false; + this._deletedOnServer = false; // for testing accountStatus + + // mock calls up to the auth server to determine whether the + // user account has been verified + this.recoveryEmailStatus = function (sessionToken) { + // simulate a call to /recovery_email/status + return Promise.resolve({ + email: this._email, + verified: this._verified, + }); + }; + + this.accountKeys = function (keyFetchToken) { + Assert.ok(keyFetchToken, "must be called with a key-fetch-token"); + // ideally we'd check the verification status here to more closely simulate + // the server, but `this._verified` is a test-only construct and doesn't + // update when the user changes verification status. + Assert.ok(!this._deletedOnServer, "this test thinks the acct is deleted!"); + return { + kA: "test-ka", + wrapKB: "X".repeat(32), + }; + }; + + this.accountStatus = function (uid) { + return Promise.resolve(!!uid && !this._deletedOnServer); + }; + + this.registerDevice = (st, name, type) => + Promise.resolve({ id: device.id, name }); + this.updateDevice = (st, id, name) => Promise.resolve({ id, name }); + this.signOut = () => Promise.resolve({}); + this.getDeviceList = st => + Promise.resolve([ + { + id: device.id, + name: device.name, + type: device.type, + pushCallback: device.pushCallback, + pushEndpointExpired: device.pushEndpointExpired, + isCurrentDevice: st === device.sessionToken, + }, + ]); + + FxAccountsClient.apply(this); +} +MockFxAccountsClient.prototype = {}; +Object.setPrototypeOf( + MockFxAccountsClient.prototype, + FxAccountsClient.prototype +); + +async function MockFxAccounts(credentials, device = {}) { + let fxa = new FxAccounts({ + newAccountState(creds) { + // we use a real accountState but mocked storage. + let storage = new MockStorageManager(); + storage.initialize(creds); + return new AccountState(storage); + }, + fxAccountsClient: new MockFxAccountsClient(device, credentials), + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + getKey(type) { + return ChromeUtils.base64URLDecode( + type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, + { padding: "ignore" } + ); + }, + }); + }); + }, + unsubscribe() { + return Promise.resolve(); + }, + }, + commands: { + async availableCommands() { + return {}; + }, + }, + device: { + DEVICE_REGISTRATION_VERSION, + _checkRemoteCommandsUpdateNeeded: async () => false, + }, + VERIFICATION_POLL_TIMEOUT_INITIAL: 1, + }); + fxa._internal.device._fxai = fxa._internal; + await fxa._internal.setSignedInUser(credentials); + Services.prefs.setStringPref( + "identity.fxaccounts.account.device.name", + device.name || "mock device name" + ); + return fxa; +} + +function updateUserAccountData(fxa, data) { + return fxa._internal.updateUserAccountData(data); +} + +add_task(async function test_updateDeviceRegistration_with_new_device() { + const deviceName = "foo"; + const deviceType = "bar"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + // Remove the current device registration (setSignedInUser does one!). + await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({ + id: "newly-generated device id", + createdAt: Date.now(), + name: deviceName, + type: deviceType, + }); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.updateDevice.count, 0); + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.registerDevice.count, 1); + Assert.equal(spy.registerDevice.args[0].length, 4); + Assert.equal(spy.registerDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.registerDevice.args[0][1], deviceName); + Assert.equal(spy.registerDevice.args[0][2], "desktop"); + Assert.equal( + spy.registerDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(data.device.id, "newly-generated device id"); + Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION); + await fxa.signOut(true); +}); + +add_task(async function test_updateDeviceRegistration_with_existing_device() { + const deviceId = "my device id"; + const deviceName = "phil's device"; + + const credentials = getTestUser("pb"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: deviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({ + id: deviceId, + name: deviceName, + }); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.registerDevice.count, 0); + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.updateDevice.count, 1); + Assert.equal(spy.updateDevice.args[0].length, 4); + Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.updateDevice.args[0][1], deviceId); + Assert.equal(spy.updateDevice.args[0][2], deviceName); + Assert.equal( + spy.updateDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(data.device.id, deviceId); + Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION); + await fxa.signOut(true); +}); + +add_task( + async function test_updateDeviceRegistration_with_unknown_device_error() { + const deviceName = "foo"; + const deviceType = "bar"; + const currentDeviceId = "my device id"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: currentDeviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({ + id: "a different newly-generated device id", + createdAt: Date.now(), + name: deviceName, + type: deviceType, + }); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.reject({ + code: 400, + errno: ERRNO_UNKNOWN_DEVICE, + }); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.registerDevice.count, 0); + Assert.equal(spy.updateDevice.count, 1); + Assert.equal(spy.updateDevice.args[0].length, 4); + Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.updateDevice.args[0][1], currentDeviceId); + Assert.equal(spy.updateDevice.args[0][2], deviceName); + Assert.equal( + spy.updateDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(null, data.device); + await fxa.signOut(true); + } +); + +add_task( + async function test_updateDeviceRegistration_with_device_session_conflict_error() { + const deviceName = "foo"; + const deviceType = "bar"; + const currentDeviceId = "my device id"; + const conflictingDeviceId = "conflicting device id"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: currentDeviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [], times: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + spy.updateDevice.time = Date.now(); + if (spy.updateDevice.count === 1) { + return Promise.reject({ + code: 400, + errno: ERRNO_DEVICE_SESSION_CONFLICT, + }); + } + return Promise.resolve({ + id: conflictingDeviceId, + name: deviceName, + }); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + spy.getDeviceList.time = Date.now(); + return Promise.resolve([ + { + id: "ignore", + name: "ignore", + type: "ignore", + isCurrentDevice: false, + }, + { + id: conflictingDeviceId, + name: deviceName, + type: deviceType, + isCurrentDevice: true, + }, + ]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.registerDevice.count, 0); + Assert.equal(spy.updateDevice.count, 1); + Assert.equal(spy.updateDevice.args[0].length, 4); + Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.updateDevice.args[0][1], currentDeviceId); + Assert.equal(spy.updateDevice.args[0][2], deviceName); + Assert.equal( + spy.updateDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + Assert.equal(spy.getDeviceList.count, 1); + Assert.equal(spy.getDeviceList.args[0].length, 1); + Assert.equal(spy.getDeviceList.args[0][0], credentials.sessionToken); + Assert.ok(spy.getDeviceList.time >= spy.updateDevice.time); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(data.device.id, conflictingDeviceId); + Assert.equal(data.device.registrationVersion, null); + await fxa.signOut(true); + } +); + +add_task( + async function test_updateDeviceRegistration_with_unrecoverable_error() { + const deviceName = "foo"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.reject({ + code: 400, + errno: ERRNO_TOO_MANY_CLIENT_REQUESTS, + }); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.updateDevice.count, 0); + Assert.equal(spy.registerDevice.count, 1); + Assert.equal(spy.registerDevice.args[0].length, 4); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(null, data.device); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_no_device_id_invokes_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); + + const spy = { count: 0, args: [] }; + fxa._internal.currentAccountState.getUserAccountData = () => + Promise.resolve({ + email: credentials.email, + registrationVersion: DEVICE_REGISTRATION_VERSION, + }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + spy.args.push(arguments); + return Promise.resolve("bar"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 1); + Assert.equal(spy.args[0].length, 2); + Assert.equal(spy.args[0][1].email, credentials.email); + Assert.equal(null, spy.args[0][1].device); + Assert.equal(result, "bar"); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_registration_version_outdated_invokes_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + + const spy = { count: 0, args: [] }; + fxa._internal.currentAccountState.getUserAccountData = () => + Promise.resolve({ + device: { + id: "my id", + registrationVersion: 0, + registeredCommandsKeys: [], + }, + }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + spy.args.push(arguments); + return Promise.resolve("wibble"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 1); + Assert.equal(spy.args[0].length, 2); + Assert.equal(spy.args[0][1].device.id, "my id"); + Assert.equal(result, "wibble"); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + + const spy = { count: 0 }; + fxa._internal.currentAccountState.getUserAccountData = async () => ({ + device: { + id: "foo's device id", + registrationVersion: DEVICE_REGISTRATION_VERSION, + registeredCommandsKeys: [], + }, + }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + return Promise.resolve("bar"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 0); + Assert.equal(result, "foo's device id"); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_device_id_and_with_no_registration_version_invokes_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + + const spy = { count: 0, args: [] }; + fxa._internal.currentAccountState.getUserAccountData = () => + Promise.resolve({ device: { id: "wibble" } }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + spy.args.push(arguments); + return Promise.resolve("wibble"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 1); + Assert.equal(spy.args[0].length, 2); + Assert.equal(spy.args[0][1].device.id, "wibble"); + Assert.equal(result, "wibble"); + await fxa.signOut(true); + } +); + +add_task(async function test_verification_updates_registration() { + const deviceName = "foo"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { + id: "device-id", + name: deviceName, + }); + + // We should already have a device registration, but without send-tab due to + // our inability to fetch keys for an unverified users. + const state = fxa._internal.currentAccountState; + const { device } = await state.getUserAccountData(); + Assert.equal(device.registeredCommandsKeys.length, 0); + + let updatePromise = new Promise(resolve => { + const old_registerOrUpdateDevice = fxa.device._registerOrUpdateDevice.bind( + fxa.device + ); + fxa.device._registerOrUpdateDevice = async function ( + currentState, + signedInUser + ) { + await old_registerOrUpdateDevice(currentState, signedInUser); + fxa.device._registerOrUpdateDevice = old_registerOrUpdateDevice; + resolve(); + }; + }); + + fxa._internal.checkEmailStatus = async function (sessionToken) { + credentials.verified = true; + return credentials; + }; + + await updatePromise; + + const { device: newDevice, encryptedSendTabKeys } = + await state.getUserAccountData(); + Assert.equal(newDevice.registeredCommandsKeys.length, 1); + Assert.notEqual(encryptedSendTabKeys, null); + await fxa.signOut(true); +}); + +add_task(async function test_devicelist_pushendpointexpired() { + const deviceId = "mydeviceid"; + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: deviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([ + { + id: "mydeviceid", + name: "foo", + type: "desktop", + isCurrentDevice: true, + pushEndpointExpired: true, + pushCallback: "https://example.com", + }, + ]); + }; + let polledForMissedCommands = false; + fxa._internal.commands.pollDeviceCommands = () => { + polledForMissedCommands = true; + }; + + await fxa.device.refreshDeviceList(); + + Assert.equal(spy.getDeviceList.count, 1); + Assert.equal(spy.updateDevice.count, 1); + Assert.ok(polledForMissedCommands); + await fxa.signOut(true); +}); + +add_task(async function test_devicelist_nopushcallback() { + const deviceId = "mydeviceid"; + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: deviceId, + registeredCommandsKeys: [], + registrationVersion: 1, + }, + }); + + const spy = { + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([ + { + id: "mydeviceid", + name: "foo", + type: "desktop", + isCurrentDevice: true, + pushEndpointExpired: false, + pushCallback: null, + }, + ]); + }; + + let polledForMissedCommands = false; + fxa._internal.commands.pollDeviceCommands = () => { + polledForMissedCommands = true; + }; + + await fxa.device.refreshDeviceList(); + + Assert.equal(spy.getDeviceList.count, 1); + Assert.equal(spy.updateDevice.count, 1); + Assert.ok(polledForMissedCommands); + await fxa.signOut(true); +}); + +add_task(async function test_refreshDeviceList() { + let credentials = getTestUser("baz"); + + let storage = new MockStorageManager(); + storage.initialize(credentials); + let state = new AccountState(storage); + + let fxAccountsClient = new MockFxAccountsClient({ + id: "deviceAAAAAA", + name: "iPhone", + type: "phone", + pushCallback: "http://mochi.test:8888", + pushEndpointExpired: false, + sessionToken: credentials.sessionToken, + }); + let spy = { + getDeviceList: { count: 0 }, + }; + const deviceListUpdateObserver = { + count: 0, + observe(subject, topic, data) { + this.count++; + }, + }; + Services.obs.addObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED); + + fxAccountsClient.getDeviceList = (function (old) { + return function getDeviceList() { + spy.getDeviceList.count += 1; + return old.apply(this, arguments); + }; + })(fxAccountsClient.getDeviceList); + let fxai = { + _now: Date.now(), + _generation: 0, + fxAccountsClient, + now() { + return this._now; + }, + withVerifiedAccountState(func) { + // Ensure `func` is called asynchronously, and simulate the possibility + // of a different user signng in while the promise is in-flight. + const currentGeneration = this._generation; + return Promise.resolve() + .then(_ => func(state)) + .then(result => { + if (currentGeneration < this._generation) { + throw new Error("Another user has signed in"); + } + return result; + }); + }, + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + getKey(type) { + return ChromeUtils.base64URLDecode( + type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, + { padding: "ignore" } + ); + }, + }); + }); + }, + unsubscribe() { + return Promise.resolve(); + }, + getSubscription() { + return Promise.resolve({ + isExpired: () => { + return false; + }, + endpoint: "http://mochi.test:8888", + }); + }, + }, + async _handleTokenError(e) { + _(`Test failure: ${e} - ${e.stack}`); + throw e; + }, + }; + let device = new FxAccountsDevice(fxai); + device._checkRemoteCommandsUpdateNeeded = async () => false; + + Assert.equal( + device.recentDeviceList, + null, + "Should not have device list initially" + ); + Assert.ok(await device.refreshDeviceList(), "Should refresh list"); + Assert.equal( + deviceListUpdateObserver.count, + 1, + `${ON_DEVICELIST_UPDATED} was notified` + ); + Assert.deepEqual( + device.recentDeviceList, + [ + { + id: "deviceAAAAAA", + name: "iPhone", + type: "phone", + pushCallback: "http://mochi.test:8888", + pushEndpointExpired: false, + isCurrentDevice: true, + }, + ], + "Should fetch device list" + ); + Assert.equal( + spy.getDeviceList.count, + 1, + "Should make request to refresh list" + ); + Assert.ok( + !(await device.refreshDeviceList()), + "Should not refresh device list if fresh" + ); + Assert.equal( + deviceListUpdateObserver.count, + 1, + `${ON_DEVICELIST_UPDATED} was not notified` + ); + + fxai._now += device.TIME_BETWEEN_FXA_DEVICES_FETCH_MS; + + let refreshPromise = device.refreshDeviceList(); + let secondRefreshPromise = device.refreshDeviceList(); + Assert.ok( + await Promise.all([refreshPromise, secondRefreshPromise]), + "Should refresh list if stale" + ); + Assert.equal( + spy.getDeviceList.count, + 2, + "Should only make one request if called with pending request" + ); + Assert.equal( + deviceListUpdateObserver.count, + 2, + `${ON_DEVICELIST_UPDATED} only notified once` + ); + + device.observe(null, ON_DEVICE_CONNECTED_NOTIFICATION); + await device.refreshDeviceList(); + Assert.equal( + spy.getDeviceList.count, + 3, + "Should refresh device list after connecting new device" + ); + Assert.equal( + deviceListUpdateObserver.count, + 3, + `${ON_DEVICELIST_UPDATED} notified when new device connects` + ); + device.observe( + null, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + JSON.stringify({ isLocalDevice: false }) + ); + await device.refreshDeviceList(); + Assert.equal( + spy.getDeviceList.count, + 4, + "Should refresh device list after disconnecting device" + ); + Assert.equal( + deviceListUpdateObserver.count, + 4, + `${ON_DEVICELIST_UPDATED} notified when device disconnects` + ); + device.observe( + null, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + JSON.stringify({ isLocalDevice: true }) + ); + await device.refreshDeviceList(); + Assert.equal( + spy.getDeviceList.count, + 4, + "Should not refresh device list after disconnecting this device" + ); + Assert.equal( + deviceListUpdateObserver.count, + 4, + `${ON_DEVICELIST_UPDATED} not notified again` + ); + + let refreshBeforeResetPromise = device.refreshDeviceList({ + ignoreCached: true, + }); + fxai._generation++; + Assert.equal( + deviceListUpdateObserver.count, + 4, + `${ON_DEVICELIST_UPDATED} not notified` + ); + await Assert.rejects(refreshBeforeResetPromise, /Another user has signed in/); + + device.reset(); + Assert.equal( + device.recentDeviceList, + null, + "Should clear device list after resetting" + ); + Assert.ok( + await device.refreshDeviceList(), + "Should fetch new list after resetting" + ); + Assert.equal( + deviceListUpdateObserver.count, + 5, + `${ON_DEVICELIST_UPDATED} notified after reset` + ); + Services.obs.removeObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED); +}); + +add_task(async function test_push_resubscribe() { + let credentials = getTestUser("baz"); + + let storage = new MockStorageManager(); + storage.initialize(credentials); + let state = new AccountState(storage); + + let mockDevice = { + id: "deviceAAAAAA", + name: "iPhone", + type: "phone", + pushCallback: "http://mochi.test:8888", + pushEndpointExpired: false, + sessionToken: credentials.sessionToken, + }; + + var mockSubscription = { + isExpired: () => { + return false; + }, + endpoint: "http://mochi.test:8888", + }; + + let fxAccountsClient = new MockFxAccountsClient(mockDevice); + + const spy = { + _registerOrUpdateDevice: { count: 0 }, + }; + + let fxai = { + _now: Date.now(), + _generation: 0, + fxAccountsClient, + now() { + return this._now; + }, + withVerifiedAccountState(func) { + // Ensure `func` is called asynchronously, and simulate the possibility + // of a different user signng in while the promise is in-flight. + const currentGeneration = this._generation; + return Promise.resolve() + .then(_ => func(state)) + .then(result => { + if (currentGeneration < this._generation) { + throw new Error("Another user has signed in"); + } + return result; + }); + }, + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + getKey(type) { + return ChromeUtils.base64URLDecode( + type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, + { padding: "ignore" } + ); + }, + }); + }); + }, + unsubscribe() { + return Promise.resolve(); + }, + getSubscription() { + return Promise.resolve(mockSubscription); + }, + }, + commands: { + async pollDeviceCommands() {}, + }, + async _handleTokenError(e) { + _(`Test failure: ${e} - ${e.stack}`); + throw e; + }, + }; + let device = new FxAccountsDevice(fxai); + device._checkRemoteCommandsUpdateNeeded = async () => false; + device._registerOrUpdateDevice = async () => { + spy._registerOrUpdateDevice.count += 1; + }; + + Assert.ok(await device.refreshDeviceList(), "Should refresh list"); + Assert.equal(spy._registerOrUpdateDevice.count, 0, "not expecting a refresh"); + + mockDevice.pushEndpointExpired = true; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 1, + "end-point expired means should resubscribe" + ); + + mockDevice.pushEndpointExpired = false; + mockSubscription.isExpired = () => true; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 2, + "push service saying expired should resubscribe" + ); + + mockSubscription.isExpired = () => false; + mockSubscription.endpoint = "something-else"; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 3, + "push service endpoint diff should resubscribe" + ); + + mockSubscription = null; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 4, + "push service saying no sub should resubscribe" + ); + + // reset everything to make sure we didn't leave something behind causing the above to + // not check what we thought it was. + mockSubscription = { + isExpired: () => { + return false; + }, + endpoint: "http://mochi.test:8888", + }; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 4, + "resetting to good data should not resubscribe" + ); +}); + +add_task(async function test_checking_remote_availableCommands_mismatch() { + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + fxa.device._checkRemoteCommandsUpdateNeeded = + FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded; + fxa.commands.availableCommands = async () => { + return { + "https://identity.mozilla.com/cmd/open-uri": "local-keys", + }; + }; + + const ourDevice = { + isCurrentDevice: true, + availableCommands: { + "https://identity.mozilla.com/cmd/open-uri": "remote-keys", + }, + }; + Assert.ok( + await fxa.device._checkRemoteCommandsUpdateNeeded( + ourDevice.availableCommands + ) + ); +}); + +add_task(async function test_checking_remote_availableCommands_match() { + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + fxa.device._checkRemoteCommandsUpdateNeeded = + FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded; + fxa.commands.availableCommands = async () => { + return { + "https://identity.mozilla.com/cmd/open-uri": "local-keys", + }; + }; + + const ourDevice = { + isCurrentDevice: true, + availableCommands: { + "https://identity.mozilla.com/cmd/open-uri": "local-keys", + }, + }; + Assert.ok( + !(await fxa.device._checkRemoteCommandsUpdateNeeded( + ourDevice.availableCommands + )) + ); +}); + +function getTestUser(name) { + return { + email: name + "@example.com", + uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348", + sessionToken: name + "'s session token", + verified: false, + ...MOCK_ACCOUNT_KEYS, + }; +} diff --git a/services/fxaccounts/tests/xpcshell/test_client.js b/services/fxaccounts/tests/xpcshell/test_client.js new file mode 100644 index 0000000000..f3cc48a70e --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_client.js @@ -0,0 +1,966 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccountsClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsClient.sys.mjs" +); + +const FAKE_SESSION_TOKEN = + "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"; + +// https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys +var ACCOUNT_KEYS = { + keyFetch: h( + // eslint-disable-next-line no-useless-concat + "8081828384858687 88898a8b8c8d8e8f" + "9091929394959697 98999a9b9c9d9e9f" + ), + + response: h( + "ee5c58845c7c9412 b11bbd20920c2fdd" + + "d83c33c9cd2c2de2 d66b222613364636" + + "c2c0f8cfbb7c6304 72c0bd88451342c6" + + "c05b14ce342c5ad4 6ad89e84464c993c" + + "3927d30230157d08 17a077eef4b20d97" + + "6f7a97363faf3f06 4c003ada7d01aa70" + ), + + kA: h( + // eslint-disable-next-line no-useless-concat + "2021222324252627 28292a2b2c2d2e2f" + "3031323334353637 38393a3b3c3d3e3f" + ), + + wrapKB: h( + // eslint-disable-next-line no-useless-concat + "4041424344454647 48494a4b4c4d4e4f" + "5051525354555657 58595a5b5c5d5e5f" + ), +}; + +add_task(async function test_authenticated_get_request() { + let message = '{"msg": "Great Success!"}'; + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + let method = "GET"; + + let server = httpd_setup({ + "/foo": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + + let result = await client._request("/foo", method, credentials); + Assert.equal("Great Success!", result.msg); + + await promiseStopServer(server); +}); + +add_task(async function test_authenticated_post_request() { + let credentials = { + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", + algorithm: "sha256", + }; + let method = "POST"; + + let server = httpd_setup({ + "/foo": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + response.bodyOutputStream.writeFrom( + request.bodyInputStream, + request.bodyInputStream.available() + ); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + + let result = await client._request("/foo", method, credentials, { + foo: "bar", + }); + Assert.equal("bar", result.foo); + + await promiseStopServer(server); +}); + +add_task(async function test_500_error() { + let message = "

Ooops!

"; + let method = "GET"; + + let server = httpd_setup({ + "/foo": function (request, response) { + response.setStatusLine(request.httpVersion, 500, "Internal Server Error"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + + try { + await client._request("/foo", method); + do_throw("Expected to catch an exception"); + } catch (e) { + Assert.equal(500, e.code); + Assert.equal("Internal Server Error", e.message); + } + + await promiseStopServer(server); +}); + +add_task(async function test_backoffError() { + let method = "GET"; + let server = httpd_setup({ + "/retryDelay": function (request, response) { + response.setHeader("Retry-After", "30"); + response.setStatusLine( + request.httpVersion, + 429, + "Client has sent too many requests" + ); + let message = "

Ooops!

"; + response.bodyOutputStream.write(message, message.length); + }, + "/duringDelayIShouldNotBeCalled": function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + let jsonMessage = '{"working": "yes"}'; + response.bodyOutputStream.write(jsonMessage, jsonMessage.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + + // Retry-After header sets client.backoffError + Assert.equal(client.backoffError, null); + try { + await client._request("/retryDelay", method); + } catch (e) { + Assert.equal(429, e.code); + Assert.equal(30, e.retryAfter); + Assert.notEqual(typeof client.fxaBackoffTimer, "undefined"); + Assert.notEqual(client.backoffError, null); + } + // While delay is in effect, client short-circuits any requests + // and re-rejects with previous error. + try { + await client._request("/duringDelayIShouldNotBeCalled", method); + throw new Error("I should not be reached"); + } catch (e) { + Assert.equal(e.retryAfter, 30); + Assert.equal(e.message, "Client has sent too many requests"); + Assert.notEqual(client.backoffError, null); + } + // Once timer fires, client nulls error out and HTTP calls work again. + client._clearBackoff(); + let result = await client._request("/duringDelayIShouldNotBeCalled", method); + Assert.equal(client.backoffError, null); + Assert.equal(result.working, "yes"); + + await promiseStopServer(server); +}); + +add_task(async function test_signUp() { + let creationMessage_noKey = JSON.stringify({ + uid: "uid", + sessionToken: "sessionToken", + }); + let creationMessage_withKey = JSON.stringify({ + uid: "uid", + sessionToken: "sessionToken", + keyFetchToken: "keyFetchToken", + }); + let errorMessage = JSON.stringify({ + code: 400, + errno: 101, + error: "account exists", + }); + let created = false; + + // Note these strings must be unicode and not already utf-8 encoded. + let unicodeUsername = "andr\xe9@example.org"; // 'andré@example.org' + let unicodePassword = "p\xe4ssw\xf6rd"; // 'pässwörd' + let server = httpd_setup({ + "/account/create": function (request, response) { + let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + body = CommonUtils.decodeUTF8(body); + let jsonBody = JSON.parse(body); + + // https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors + + if (created) { + // Error trying to create same account a second time + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write(errorMessage, errorMessage.length); + return; + } + + if (jsonBody.email == unicodeUsername) { + Assert.equal("", request._queryString); + Assert.equal( + jsonBody.authPW, + "247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375" + ); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write( + creationMessage_noKey, + creationMessage_noKey.length + ); + return; + } + + if (jsonBody.email == "you@example.org") { + Assert.equal("keys=true", request._queryString); + Assert.equal( + jsonBody.authPW, + "e5c1cdfdaa5fcee06142db865b212cc8ba8abee2a27d639d42c139f006cdb930" + ); + created = true; + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write( + creationMessage_withKey, + creationMessage_withKey.length + ); + return; + } + // just throwing here doesn't make any log noise, so have an assertion + // fail instead. + Assert.ok(false, "unexpected email: " + jsonBody.email); + }, + }); + + // Try to create an account without retrieving optional keys. + let client = new FxAccountsClient(server.baseURI); + let result = await client.signUp(unicodeUsername, unicodePassword); + Assert.equal("uid", result.uid); + Assert.equal("sessionToken", result.sessionToken); + Assert.equal(undefined, result.keyFetchToken); + Assert.equal( + result.unwrapBKey, + "de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28" + ); + + // Try to create an account retrieving optional keys. + result = await client.signUp("you@example.org", "pässwörd", true); + Assert.equal("uid", result.uid); + Assert.equal("sessionToken", result.sessionToken); + Assert.equal("keyFetchToken", result.keyFetchToken); + Assert.equal( + result.unwrapBKey, + "f589225b609e56075d76eb74f771ff9ab18a4dc0e901e131ba8f984c7fb0ca8c" + ); + + // Try to create an existing account. Triggers error path. + try { + result = await client.signUp(unicodeUsername, unicodePassword); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(101, expectedError.errno); + } + + await promiseStopServer(server); +}); + +add_task(async function test_signIn() { + let sessionMessage_noKey = JSON.stringify({ + sessionToken: FAKE_SESSION_TOKEN, + }); + let sessionMessage_withKey = JSON.stringify({ + sessionToken: FAKE_SESSION_TOKEN, + keyFetchToken: "keyFetchToken", + }); + let errorMessage_notExistent = JSON.stringify({ + code: 400, + errno: 102, + error: "doesn't exist", + }); + let errorMessage_wrongCap = JSON.stringify({ + code: 400, + errno: 120, + error: "Incorrect email case", + email: "you@example.com", + }); + + // Note this strings must be unicode and not already utf-8 encoded. + let unicodeUsername = "m\xe9@example.com"; // 'mé@example.com' + let server = httpd_setup({ + "/account/login": function (request, response) { + let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + body = CommonUtils.decodeUTF8(body); + let jsonBody = JSON.parse(body); + + if (jsonBody.email == unicodeUsername) { + Assert.equal("", request._queryString); + Assert.equal( + jsonBody.authPW, + "08b9d111196b8408e8ed92439da49206c8ecfbf343df0ae1ecefcd1e0174a8b6" + ); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write( + sessionMessage_noKey, + sessionMessage_noKey.length + ); + } else if (jsonBody.email == "you@example.com") { + Assert.equal("keys=true", request._queryString); + Assert.equal( + jsonBody.authPW, + "93d20ec50304d496d0707ec20d7e8c89459b6396ec5dd5b9e92809c5e42856c7" + ); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write( + sessionMessage_withKey, + sessionMessage_withKey.length + ); + } else if (jsonBody.email == "You@example.com") { + // Error trying to sign in with a wrong capitalization + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write( + errorMessage_wrongCap, + errorMessage_wrongCap.length + ); + } else { + // Error trying to sign in to nonexistent account + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write( + errorMessage_notExistent, + errorMessage_notExistent.length + ); + } + }, + }); + + // Login without retrieving optional keys + let client = new FxAccountsClient(server.baseURI); + let result = await client.signIn(unicodeUsername, "bigsecret"); + Assert.equal(FAKE_SESSION_TOKEN, result.sessionToken); + Assert.equal( + result.unwrapBKey, + "c076ec3f4af123a615157154c6e1d0d6293e514fd7b0221e32d50517ecf002b8" + ); + Assert.equal(undefined, result.keyFetchToken); + + // Login with retrieving optional keys + result = await client.signIn("you@example.com", "bigsecret", true); + Assert.equal(FAKE_SESSION_TOKEN, result.sessionToken); + Assert.equal( + result.unwrapBKey, + "65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624" + ); + Assert.equal("keyFetchToken", result.keyFetchToken); + + // Retry due to wrong email capitalization + result = await client.signIn("You@example.com", "bigsecret", true); + Assert.equal(FAKE_SESSION_TOKEN, result.sessionToken); + Assert.equal( + result.unwrapBKey, + "65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624" + ); + Assert.equal("keyFetchToken", result.keyFetchToken); + + // Trigger error path + try { + result = await client.signIn("yøü@bad.example.org", "nofear"); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(102, expectedError.errno); + } + + await promiseStopServer(server); +}); + +add_task(async function test_signOut() { + let signoutMessage = JSON.stringify({}); + let errorMessage = JSON.stringify({ + code: 400, + errno: 102, + error: "doesn't exist", + }); + let signedOut = false; + + let server = httpd_setup({ + "/session/destroy": function (request, response) { + if (!signedOut) { + signedOut = true; + Assert.ok(request.hasHeader("Authorization")); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(signoutMessage, signoutMessage.length); + return; + } + + // Error trying to sign out of nonexistent account + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write(errorMessage, errorMessage.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + let result = await client.signOut("FakeSession"); + Assert.equal(typeof result, "object"); + + // Trigger error path + try { + result = await client.signOut("FakeSession"); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(102, expectedError.errno); + } + + await promiseStopServer(server); +}); + +add_task(async function test_recoveryEmailStatus() { + let emailStatus = JSON.stringify({ verified: true }); + let errorMessage = JSON.stringify({ + code: 400, + errno: 102, + error: "doesn't exist", + }); + let tries = 0; + + let server = httpd_setup({ + "/recovery_email/status": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + Assert.equal("", request._queryString); + + if (tries === 0) { + tries += 1; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(emailStatus, emailStatus.length); + return; + } + + // Second call gets an error trying to query a nonexistent account + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write(errorMessage, errorMessage.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + let result = await client.recoveryEmailStatus(FAKE_SESSION_TOKEN); + Assert.equal(result.verified, true); + + // Trigger error path + try { + result = await client.recoveryEmailStatus("some bogus session"); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(102, expectedError.errno); + } + + await promiseStopServer(server); +}); + +add_task(async function test_recoveryEmailStatusWithReason() { + let emailStatus = JSON.stringify({ verified: true }); + let server = httpd_setup({ + "/recovery_email/status": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + // if there is a query string then it will have a reason + Assert.equal("reason=push", request._queryString); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(emailStatus, emailStatus.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + let result = await client.recoveryEmailStatus(FAKE_SESSION_TOKEN, { + reason: "push", + }); + Assert.equal(result.verified, true); + await promiseStopServer(server); +}); + +add_task(async function test_resendVerificationEmail() { + let emptyMessage = "{}"; + let errorMessage = JSON.stringify({ + code: 400, + errno: 102, + error: "doesn't exist", + }); + let tries = 0; + + let server = httpd_setup({ + "/recovery_email/resend_code": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + if (tries === 0) { + tries += 1; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(emptyMessage, emptyMessage.length); + return; + } + + // Second call gets an error trying to query a nonexistent account + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write(errorMessage, errorMessage.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + let result = await client.resendVerificationEmail(FAKE_SESSION_TOKEN); + Assert.equal(JSON.stringify(result), emptyMessage); + + // Trigger error path + try { + result = await client.resendVerificationEmail("some bogus session"); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(102, expectedError.errno); + } + + await promiseStopServer(server); +}); + +add_task(async function test_accountKeys() { + // Four calls to accountKeys(). The first one should work correctly, and we + // should get a valid bundle back, in exchange for our keyFetch token, from + // which we correctly derive kA and wrapKB. The subsequent three calls + // should all trigger separate error paths. + let responseMessage = JSON.stringify({ bundle: ACCOUNT_KEYS.response }); + let errorMessage = JSON.stringify({ + code: 400, + errno: 102, + error: "doesn't exist", + }); + let emptyMessage = "{}"; + let attempt = 0; + + let server = httpd_setup({ + "/account/keys": function (request, response) { + Assert.ok(request.hasHeader("Authorization")); + attempt += 1; + + switch (attempt) { + case 1: + // First time succeeds + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write( + responseMessage, + responseMessage.length + ); + break; + + case 2: + // Second time, return no bundle to trigger client error + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(emptyMessage, emptyMessage.length); + break; + + case 3: + // Return gibberish to trigger client MAC error + // Tweak a byte + let garbageResponse = JSON.stringify({ + bundle: ACCOUNT_KEYS.response.slice(0, -1) + "1", + }); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write( + garbageResponse, + garbageResponse.length + ); + break; + + case 4: + // Trigger error for nonexistent account + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write(errorMessage, errorMessage.length); + break; + } + }, + }); + + let client = new FxAccountsClient(server.baseURI); + + // First try, all should be good + let result = await client.accountKeys(ACCOUNT_KEYS.keyFetch); + Assert.equal(CommonUtils.hexToBytes(ACCOUNT_KEYS.kA), result.kA); + Assert.equal(CommonUtils.hexToBytes(ACCOUNT_KEYS.wrapKB), result.wrapKB); + + // Second try, empty bundle should trigger error + try { + result = await client.accountKeys(ACCOUNT_KEYS.keyFetch); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(expectedError.message, "failed to retrieve keys"); + } + + // Third try, bad bundle results in MAC error + try { + result = await client.accountKeys(ACCOUNT_KEYS.keyFetch); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(expectedError.message, "error unbundling encryption keys"); + } + + // Fourth try, pretend account doesn't exist + try { + result = await client.accountKeys(ACCOUNT_KEYS.keyFetch); + do_throw("Expected to catch an exception"); + } catch (expectedError) { + Assert.equal(102, expectedError.errno); + } + + await promiseStopServer(server); +}); + +add_task(async function test_accessTokenWithSessionToken() { + let server = httpd_setup({ + "/oauth/token": function (request, response) { + const responseMessage = JSON.stringify({ + access_token: + "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69", + token_type: "bearer", + scope: "https://identity.mozilla.com/apps/oldsync", + expires_in: 21600, + auth_at: 1589579900, + }); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(responseMessage, responseMessage.length); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + let sessionTokenHex = + "0599c36ebb5cad6feb9285b9547b65342b5434d55c07b33bffd4307ab8f82dc4"; + let clientId = "5882386c6d801776"; + let scope = "https://identity.mozilla.com/apps/oldsync"; + let ttl = 100; + let result = await client.accessTokenWithSessionToken( + sessionTokenHex, + clientId, + scope, + ttl + ); + Assert.ok(result); + + await promiseStopServer(server); +}); + +add_task(async function test_accountExists() { + let existsMessage = JSON.stringify({ + error: "wrong password", + code: 400, + errno: 103, + }); + let doesntExistMessage = JSON.stringify({ + error: "no such account", + code: 400, + errno: 102, + }); + let emptyMessage = "{}"; + + let server = httpd_setup({ + "/account/login": function (request, response) { + let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + let jsonBody = JSON.parse(body); + + switch (jsonBody.email) { + // We'll test that these users' accounts exist + case "i.exist@example.com": + case "i.also.exist@example.com": + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write(existsMessage, existsMessage.length); + break; + + // This user's account doesn't exist + case "i.dont.exist@example.com": + response.setStatusLine(request.httpVersion, 400, "Bad request"); + response.bodyOutputStream.write( + doesntExistMessage, + doesntExistMessage.length + ); + break; + + // This user throws an unexpected response + // This will reject the client signIn promise + case "i.break.things@example.com": + response.setStatusLine(request.httpVersion, 500, "Alas"); + response.bodyOutputStream.write(emptyMessage, emptyMessage.length); + break; + + default: + throw new Error("Unexpected login from " + jsonBody.email); + } + }, + }); + + let client = new FxAccountsClient(server.baseURI); + let result; + + result = await client.accountExists("i.exist@example.com"); + Assert.ok(result); + + result = await client.accountExists("i.also.exist@example.com"); + Assert.ok(result); + + result = await client.accountExists("i.dont.exist@example.com"); + Assert.ok(!result); + + try { + result = await client.accountExists("i.break.things@example.com"); + do_throw("Expected to catch an exception"); + } catch (unexpectedError) { + Assert.equal(unexpectedError.code, 500); + } + + await promiseStopServer(server); +}); + +add_task(async function test_registerDevice() { + const DEVICE_ID = "device id"; + const DEVICE_NAME = "device name"; + const DEVICE_TYPE = "device type"; + const ERROR_NAME = "test that the client promise rejects"; + + const server = httpd_setup({ + "/account/device": function (request, response) { + const body = JSON.parse( + CommonUtils.readBytesFromInputStream(request.bodyInputStream) + ); + + if ( + body.id || + !body.name || + !body.type || + Object.keys(body).length !== 2 + ) { + response.setStatusLine(request.httpVersion, 400, "Invalid request"); + response.bodyOutputStream.write("{}", 2); + return; + } + + if (body.name === ERROR_NAME) { + response.setStatusLine(request.httpVersion, 500, "Alas"); + response.bodyOutputStream.write("{}", 2); + return; + } + + body.id = DEVICE_ID; + body.createdAt = Date.now(); + + const responseMessage = JSON.stringify(body); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(responseMessage, responseMessage.length); + }, + }); + + const client = new FxAccountsClient(server.baseURI); + const result = await client.registerDevice( + FAKE_SESSION_TOKEN, + DEVICE_NAME, + DEVICE_TYPE + ); + + Assert.ok(result); + Assert.equal(Object.keys(result).length, 4); + Assert.equal(result.id, DEVICE_ID); + Assert.equal(typeof result.createdAt, "number"); + Assert.ok(result.createdAt > 0); + Assert.equal(result.name, DEVICE_NAME); + Assert.equal(result.type, DEVICE_TYPE); + + try { + await client.registerDevice(FAKE_SESSION_TOKEN, ERROR_NAME, DEVICE_TYPE); + do_throw("Expected to catch an exception"); + } catch (unexpectedError) { + Assert.equal(unexpectedError.code, 500); + } + + await promiseStopServer(server); +}); + +add_task(async function test_updateDevice() { + const DEVICE_ID = "some other id"; + const DEVICE_NAME = "some other name"; + const ERROR_ID = "test that the client promise rejects"; + + const server = httpd_setup({ + "/account/device": function (request, response) { + const body = JSON.parse( + CommonUtils.readBytesFromInputStream(request.bodyInputStream) + ); + + if ( + !body.id || + !body.name || + body.type || + Object.keys(body).length !== 2 + ) { + response.setStatusLine(request.httpVersion, 400, "Invalid request"); + response.bodyOutputStream.write("{}", 2); + return; + } + + if (body.id === ERROR_ID) { + response.setStatusLine(request.httpVersion, 500, "Alas"); + response.bodyOutputStream.write("{}", 2); + return; + } + + const responseMessage = JSON.stringify(body); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(responseMessage, responseMessage.length); + }, + }); + + const client = new FxAccountsClient(server.baseURI); + const result = await client.updateDevice( + FAKE_SESSION_TOKEN, + DEVICE_ID, + DEVICE_NAME + ); + + Assert.ok(result); + Assert.equal(Object.keys(result).length, 2); + Assert.equal(result.id, DEVICE_ID); + Assert.equal(result.name, DEVICE_NAME); + + try { + await client.updateDevice(FAKE_SESSION_TOKEN, ERROR_ID, DEVICE_NAME); + do_throw("Expected to catch an exception"); + } catch (unexpectedError) { + Assert.equal(unexpectedError.code, 500); + } + + await promiseStopServer(server); +}); + +add_task(async function test_getDeviceList() { + let canReturnDevices; + + const server = httpd_setup({ + "/account/devices": function (request, response) { + if (canReturnDevices) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write("[]", 2); + } else { + response.setStatusLine(request.httpVersion, 500, "Alas"); + response.bodyOutputStream.write("{}", 2); + } + }, + }); + + const client = new FxAccountsClient(server.baseURI); + + canReturnDevices = true; + const result = await client.getDeviceList(FAKE_SESSION_TOKEN); + Assert.ok(Array.isArray(result)); + Assert.equal(result.length, 0); + + try { + canReturnDevices = false; + await client.getDeviceList(FAKE_SESSION_TOKEN); + do_throw("Expected to catch an exception"); + } catch (unexpectedError) { + Assert.equal(unexpectedError.code, 500); + } + + await promiseStopServer(server); +}); + +add_task(async function test_client_metrics() { + function writeResp(response, msg) { + if (typeof msg === "object") { + msg = JSON.stringify(msg); + } + response.bodyOutputStream.write(msg, msg.length); + } + + let server = httpd_setup({ + "/session/destroy": function (request, response) { + response.setHeader("Content-Type", "application/json; charset=utf-8"); + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + writeResp(response, { + error: "invalid authentication timestamp", + code: 401, + errno: 111, + }); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + + await Assert.rejects( + client.signOut(FAKE_SESSION_TOKEN, { + service: "sync", + }), + function (err) { + return err.errno == 111; + } + ); + + await promiseStopServer(server); +}); + +add_task(async function test_email_case() { + let canonicalEmail = "greta.garbo@gmail.com"; + let clientEmail = "Greta.Garbo@gmail.COM"; + let attempts = 0; + + function writeResp(response, msg) { + if (typeof msg === "object") { + msg = JSON.stringify(msg); + } + response.bodyOutputStream.write(msg, msg.length); + } + + let server = httpd_setup({ + "/account/login": function (request, response) { + response.setHeader("Content-Type", "application/json; charset=utf-8"); + attempts += 1; + if (attempts > 2) { + response.setStatusLine( + request.httpVersion, + 429, + "Sorry, you had your chance" + ); + return writeResp(response, ""); + } + + let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + let jsonBody = JSON.parse(body); + let email = jsonBody.email; + + // If the client has the wrong case on the email, we return a 400, with + // the capitalization of the email as saved in the accounts database. + if (email == canonicalEmail) { + response.setStatusLine(request.httpVersion, 200, "Yay"); + return writeResp(response, { areWeHappy: "yes" }); + } + + response.setStatusLine(request.httpVersion, 400, "Incorrect email case"); + return writeResp(response, { + code: 400, + errno: 120, + error: "Incorrect email case", + email: canonicalEmail, + }); + }, + }); + + let client = new FxAccountsClient(server.baseURI); + + let result = await client.signIn(clientEmail, "123456"); + Assert.equal(result.areWeHappy, "yes"); + Assert.equal(attempts, 2); + + await promiseStopServer(server); +}); + +// turn formatted test vectors into normal hex strings +function h(hexStr) { + return hexStr.replace(/\s+/g, ""); +} diff --git a/services/fxaccounts/tests/xpcshell/test_commands.js b/services/fxaccounts/tests/xpcshell/test_commands.js new file mode 100644 index 0000000000..3fa42da439 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_commands.js @@ -0,0 +1,708 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccountsCommands, SendTab } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommands.sys.mjs" +); + +const { FxAccountsClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsClient.sys.mjs" +); + +const { COMMAND_SENDTAB, COMMAND_SENDTAB_TAIL } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +class TelemetryMock { + constructor() { + this._events = []; + this._uuid_counter = 0; + } + + recordEvent(object, method, value, extra = undefined) { + this._events.push({ object, method, value, extra }); + } + + generateFlowID() { + this._uuid_counter += 1; + return this._uuid_counter.toString(); + } + + sanitizeDeviceId(id) { + return id + "-san"; + } +} + +function FxaInternalMock() { + return { + telemetry: new TelemetryMock(), + }; +} + +function MockFxAccountsClient() { + FxAccountsClient.apply(this); +} + +MockFxAccountsClient.prototype = {}; +Object.setPrototypeOf( + MockFxAccountsClient.prototype, + FxAccountsClient.prototype +); + +add_task(async function test_sendtab_isDeviceCompatible() { + const sendTab = new SendTab(null, null); + let device = { name: "My device" }; + Assert.ok(!sendTab.isDeviceCompatible(device)); + device = { name: "My device", availableCommands: {} }; + Assert.ok(!sendTab.isDeviceCompatible(device)); + device = { + name: "My device", + availableCommands: { + "https://identity.mozilla.com/cmd/open-uri": "payload", + }, + }; + Assert.ok(sendTab.isDeviceCompatible(device)); +}); + +add_task(async function test_sendtab_send() { + const commands = { + invoke: sinon.spy((cmd, device, payload) => { + if (device.name == "Device 1") { + throw new Error("Invoke error!"); + } + Assert.equal(payload.encrypted, "encryptedpayload"); + }), + }; + const fxai = FxaInternalMock(); + const sendTab = new SendTab(commands, fxai); + sendTab._encrypt = (bytes, device) => { + if (device.name == "Device 2") { + throw new Error("Encrypt error!"); + } + return "encryptedpayload"; + }; + const to = [ + { name: "Device 1" }, + { name: "Device 2" }, + { id: "dev3", name: "Device 3" }, + ]; + // although we are sending to 3 devices, only 1 is successful - so there's + // only 1 streamID we care about. However, we've created IDs even for the + // failing items - so it's "4" + const expectedTelemetryStreamID = "4"; + const tab = { title: "Foo", url: "https://foo.bar/" }; + const report = await sendTab.send(to, tab); + Assert.equal(report.succeeded.length, 1); + Assert.equal(report.failed.length, 2); + Assert.equal(report.succeeded[0].name, "Device 3"); + Assert.equal(report.failed[0].device.name, "Device 1"); + Assert.equal(report.failed[0].error.message, "Invoke error!"); + Assert.equal(report.failed[1].device.name, "Device 2"); + Assert.equal(report.failed[1].error.message, "Encrypt error!"); + Assert.ok(commands.invoke.calledTwice); + Assert.deepEqual(fxai.telemetry._events, [ + { + object: "command-sent", + method: COMMAND_SENDTAB_TAIL, + value: "dev3-san", + extra: { flowID: "1", streamID: expectedTelemetryStreamID }, + }, + ]); +}); + +add_task(async function test_sendtab_send_rate_limit() { + const rateLimitReject = { + code: 429, + retryAfter: 5, + retryAfterLocalized: "retry after 5 seconds", + }; + const fxAccounts = { + fxAccountsClient: new MockFxAccountsClient(), + getUserAccountData() { + return {}; + }, + telemetry: new TelemetryMock(), + }; + let rejected = false; + let invoked = 0; + fxAccounts.fxAccountsClient.invokeCommand = async function invokeCommand() { + invoked++; + Assert.ok(invoked <= 2, "only called twice and not more"); + if (rejected) { + return {}; + } + rejected = true; + return Promise.reject(rateLimitReject); + }; + const commands = new FxAccountsCommands(fxAccounts); + const sendTab = new SendTab(commands, fxAccounts); + sendTab._encrypt = () => "encryptedpayload"; + + const tab = { title: "Foo", url: "https://foo.bar/" }; + let report = await sendTab.send([{ name: "Device 1" }], tab); + Assert.equal(report.succeeded.length, 0); + Assert.equal(report.failed.length, 1); + Assert.equal(report.failed[0].error, rateLimitReject); + + report = await sendTab.send([{ name: "Device 1" }], tab); + Assert.equal(report.succeeded.length, 0); + Assert.equal(report.failed.length, 1); + Assert.ok( + report.failed[0].error.message.includes( + "Invoke for " + + "https://identity.mozilla.com/cmd/open-uri is rate-limited" + ) + ); + + commands._invokeRateLimitExpiry = Date.now() - 1000; + report = await sendTab.send([{ name: "Device 1" }], tab); + Assert.equal(report.succeeded.length, 1); + Assert.equal(report.failed.length, 0); +}); + +add_task(async function test_sendtab_receive() { + // We are testing 'receive' here, but might as well go through 'send' + // to package the data and for additional testing... + const commands = { + _invokes: [], + invoke(cmd, device, payload) { + this._invokes.push({ cmd, device, payload }); + }, + }; + + const fxai = FxaInternalMock(); + const sendTab = new SendTab(commands, fxai); + sendTab._encrypt = (bytes, device) => { + return bytes; + }; + sendTab._decrypt = bytes => { + return bytes; + }; + const tab = { title: "tab title", url: "http://example.com" }; + const to = [{ id: "devid", name: "The Device" }]; + const reason = "push"; + + await sendTab.send(to, tab); + Assert.equal(commands._invokes.length, 1); + + for (let { cmd, device, payload } of commands._invokes) { + Assert.equal(cmd, COMMAND_SENDTAB); + // Older Firefoxes would send a plaintext flowID in the top-level payload. + // Test that we sensibly ignore it. + Assert.ok(!payload.hasOwnProperty("flowID")); + // change it - ensure we still get what we expect in telemetry later. + payload.flowID = "ignore-me"; + Assert.deepEqual(await sendTab.handle(device.id, payload, reason), { + title: "tab title", + uri: "http://example.com", + }); + } + + Assert.deepEqual(fxai.telemetry._events, [ + { + object: "command-sent", + method: COMMAND_SENDTAB_TAIL, + value: "devid-san", + extra: { flowID: "1", streamID: "2" }, + }, + { + object: "command-received", + method: COMMAND_SENDTAB_TAIL, + value: "devid-san", + extra: { flowID: "1", streamID: "2", reason }, + }, + ]); +}); + +// Test that a client which only sends the flowID in the envelope and not in the +// encrypted body gets recorded without the flowID. +add_task(async function test_sendtab_receive_old_client() { + const fxai = FxaInternalMock(); + const sendTab = new SendTab(null, fxai); + sendTab._decrypt = bytes => { + return bytes; + }; + const data = { entries: [{ title: "title", url: "url" }] }; + // No 'flowID' in the encrypted payload, no 'streamID' anywhere. + const payload = { + flowID: "flow-id", + encrypted: new TextEncoder().encode(JSON.stringify(data)), + }; + const reason = "push"; + await sendTab.handle("sender-id", payload, reason); + Assert.deepEqual(fxai.telemetry._events, [ + { + object: "command-received", + method: COMMAND_SENDTAB_TAIL, + value: "sender-id-san", + // deepEqual doesn't ignore undefined, but our telemetry code and + // JSON.stringify() do... + extra: { flowID: undefined, streamID: undefined, reason }, + }, + ]); +}); + +add_task(function test_commands_getReason() { + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb({}); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + const testCases = [ + { + receivedIndex: 0, + currentIndex: 0, + expectedReason: "poll", + message: "should return reason 'poll'", + }, + { + receivedIndex: 7, + currentIndex: 3, + expectedReason: "push-missed", + message: "should return reason 'push-missed'", + }, + { + receivedIndex: 2, + currentIndex: 8, + expectedReason: "push", + message: "should return reason 'push'", + }, + ]; + for (const tc of testCases) { + const reason = commands._getReason(tc.receivedIndex, tc.currentIndex); + Assert.equal(reason, tc.expectedReason, tc.message); + } +}); + +add_task(async function test_commands_pollDeviceCommands_push() { + // Server state. + const remoteMessages = [ + { + index: 11, + data: {}, + }, + { + index: 12, + data: {}, + }, + ]; + const remoteIndex = 12; + + // Local state. + const pushIndexReceived = 11; + const accountState = { + data: { + device: { + lastCommandIndex: 10, + }, + }, + getUserAccountData() { + return this.data; + }, + updateUserAccountData(data) { + this.data = data; + }, + }; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb(accountState); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + const mockCommands = sinon.mock(commands); + mockCommands.expects("_fetchDeviceCommands").once().withArgs(11).returns({ + index: remoteIndex, + messages: remoteMessages, + }); + mockCommands + .expects("_handleCommands") + .once() + .withArgs(remoteMessages, pushIndexReceived); + await commands.pollDeviceCommands(pushIndexReceived); + + mockCommands.verify(); + Assert.equal(accountState.data.device.lastCommandIndex, 12); +}); + +add_task( + async function test_commands_pollDeviceCommands_push_already_fetched() { + // Local state. + const pushIndexReceived = 12; + const accountState = { + data: { + device: { + lastCommandIndex: 12, + }, + }, + getUserAccountData() { + return this.data; + }, + updateUserAccountData(data) { + this.data = data; + }, + }; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb(accountState); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + const mockCommands = sinon.mock(commands); + mockCommands.expects("_fetchDeviceCommands").never(); + mockCommands.expects("_handleCommands").never(); + await commands.pollDeviceCommands(pushIndexReceived); + + mockCommands.verify(); + Assert.equal(accountState.data.device.lastCommandIndex, 12); + } +); + +add_task(async function test_commands_handleCommands() { + // This test ensures that `_getReason` is being called by + // `_handleCommands` with the expected parameters. + const pushIndexReceived = 12; + const senderID = "6d09f6c4-89b2-41b3-a0ac-e4c2502b5485"; + const remoteMessageIndex = 8; + const remoteMessages = [ + { + index: remoteMessageIndex, + data: { + command: COMMAND_SENDTAB, + payload: { + encrypted: {}, + }, + sender: senderID, + }, + }, + ]; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb({}); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + commands.sendTab.handle = (sender, data, reason) => { + return { + title: "testTitle", + uri: "https://testURI", + }; + }; + commands._fxai.device = { + refreshDeviceList: () => {}, + recentDeviceList: [ + { + id: senderID, + }, + ], + }; + const mockCommands = sinon.mock(commands); + mockCommands + .expects("_getReason") + .once() + .withExactArgs(pushIndexReceived, remoteMessageIndex); + mockCommands.expects("_notifyFxATabsReceived").once(); + await commands._handleCommands(remoteMessages, pushIndexReceived); + mockCommands.verify(); +}); + +add_task(async function test_commands_handleCommands_invalid_tab() { + // This test ensures that `_getReason` is being called by + // `_handleCommands` with the expected parameters. + const pushIndexReceived = 12; + const senderID = "6d09f6c4-89b2-41b3-a0ac-e4c2502b5485"; + const remoteMessageIndex = 8; + const remoteMessages = [ + { + index: remoteMessageIndex, + data: { + command: COMMAND_SENDTAB, + payload: { + encrypted: {}, + }, + sender: senderID, + }, + }, + ]; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb({}); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + commands.sendTab.handle = (sender, data, reason) => { + return { + title: "badUriTab", + uri: "file://path/to/pdf", + }; + }; + commands._fxai.device = { + refreshDeviceList: () => {}, + recentDeviceList: [ + { + id: senderID, + }, + ], + }; + const mockCommands = sinon.mock(commands); + mockCommands + .expects("_getReason") + .once() + .withExactArgs(pushIndexReceived, remoteMessageIndex); + // We shouldn't have tried to open a tab with an invalid uri + mockCommands.expects("_notifyFxATabsReceived").never(); + + await commands._handleCommands(remoteMessages, pushIndexReceived); + mockCommands.verify(); +}); + +add_task( + async function test_commands_pollDeviceCommands_push_local_state_empty() { + // Server state. + const remoteMessages = [ + { + index: 11, + data: {}, + }, + { + index: 12, + data: {}, + }, + ]; + const remoteIndex = 12; + + // Local state. + const pushIndexReceived = 11; + const accountState = { + data: { + device: {}, + }, + getUserAccountData() { + return this.data; + }, + updateUserAccountData(data) { + this.data = data; + }, + }; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb(accountState); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + const mockCommands = sinon.mock(commands); + mockCommands.expects("_fetchDeviceCommands").once().withArgs(11).returns({ + index: remoteIndex, + messages: remoteMessages, + }); + mockCommands + .expects("_handleCommands") + .once() + .withArgs(remoteMessages, pushIndexReceived); + await commands.pollDeviceCommands(pushIndexReceived); + + mockCommands.verify(); + Assert.equal(accountState.data.device.lastCommandIndex, 12); + } +); + +add_task(async function test_commands_pollDeviceCommands_scheduled_local() { + // Server state. + const remoteMessages = [ + { + index: 11, + data: {}, + }, + { + index: 12, + data: {}, + }, + ]; + const remoteIndex = 12; + const pushIndexReceived = 0; + // Local state. + const accountState = { + data: { + device: { + lastCommandIndex: 10, + }, + }, + getUserAccountData() { + return this.data; + }, + updateUserAccountData(data) { + this.data = data; + }, + }; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb(accountState); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + const mockCommands = sinon.mock(commands); + mockCommands.expects("_fetchDeviceCommands").once().withArgs(11).returns({ + index: remoteIndex, + messages: remoteMessages, + }); + mockCommands + .expects("_handleCommands") + .once() + .withArgs(remoteMessages, pushIndexReceived); + await commands.pollDeviceCommands(); + + mockCommands.verify(); + Assert.equal(accountState.data.device.lastCommandIndex, 12); +}); + +add_task( + async function test_commands_pollDeviceCommands_scheduled_local_state_empty() { + // Server state. + const remoteMessages = [ + { + index: 11, + data: {}, + }, + { + index: 12, + data: {}, + }, + ]; + const remoteIndex = 12; + const pushIndexReceived = 0; + // Local state. + const accountState = { + data: { + device: {}, + }, + getUserAccountData() { + return this.data; + }, + updateUserAccountData(data) { + this.data = data; + }, + }; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb(accountState); + }, + }; + const commands = new FxAccountsCommands(fxAccounts); + const mockCommands = sinon.mock(commands); + mockCommands.expects("_fetchDeviceCommands").once().withArgs(0).returns({ + index: remoteIndex, + messages: remoteMessages, + }); + mockCommands + .expects("_handleCommands") + .once() + .withArgs(remoteMessages, pushIndexReceived); + await commands.pollDeviceCommands(); + + mockCommands.verify(); + Assert.equal(accountState.data.device.lastCommandIndex, 12); + } +); + +add_task(async function test_send_tab_keys_regenerated_if_lost() { + const commands = { + _invokes: [], + invoke(cmd, device, payload) { + this._invokes.push({ cmd, device, payload }); + }, + }; + + // Local state. + const accountState = { + data: { + // Since the device object has no + // sendTabKeys, it will recover + // when we attempt to get the + // encryptedSendTabKeys + device: { + lastCommandIndex: 10, + }, + encryptedSendTabKeys: "keys", + }, + getUserAccountData() { + return this.data; + }, + updateUserAccountData(data) { + this.data = data; + }, + }; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb(accountState); + }, + async getUserAccountData(data) { + return accountState.getUserAccountData(data); + }, + telemetry: new TelemetryMock(), + }; + const sendTab = new SendTab(commands, fxAccounts); + let generateEncryptedKeysCalled = false; + sendTab._generateAndPersistEncryptedSendTabKey = async () => { + generateEncryptedKeysCalled = true; + }; + await sendTab.getEncryptedSendTabKeys(); + Assert.ok(generateEncryptedKeysCalled); +}); + +add_task(async function test_send_tab_keys_are_not_regenerated_if_not_lost() { + const commands = { + _invokes: [], + invoke(cmd, device, payload) { + this._invokes.push({ cmd, device, payload }); + }, + }; + + // Local state. + const accountState = { + data: { + // Since the device object has + // sendTabKeys, it will not try + // to regenerate them + // when we attempt to get the + // encryptedSendTabKeys + device: { + lastCommandIndex: 10, + sendTabKeys: "keys", + }, + encryptedSendTabKeys: "encrypted-keys", + }, + getUserAccountData() { + return this.data; + }, + updateUserAccountData(data) { + this.data = data; + }, + }; + + const fxAccounts = { + async withCurrentAccountState(cb) { + await cb(accountState); + }, + async getUserAccountData(data) { + return accountState.getUserAccountData(data); + }, + telemetry: new TelemetryMock(), + }; + const sendTab = new SendTab(commands, fxAccounts); + let generateEncryptedKeysCalled = false; + sendTab._generateAndPersistEncryptedSendTabKey = async () => { + generateEncryptedKeysCalled = true; + }; + await sendTab.getEncryptedSendTabKeys(); + Assert.ok(!generateEncryptedKeysCalled); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_credentials.js b/services/fxaccounts/tests/xpcshell/test_credentials.js new file mode 100644 index 0000000000..c3656f219d --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_credentials.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Credentials } = ChromeUtils.importESModule( + "resource://gre/modules/Credentials.sys.mjs" +); +const { CryptoUtils } = ChromeUtils.importESModule( + "resource://services-crypto/utils.sys.mjs" +); + +var { hexToBytes: h2b, hexAsString: h2s, bytesAsHex: b2h } = CommonUtils; + +// Test vectors for the "onepw" protocol: +// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors +var vectors = { + "client stretch-KDF": { + email: h("616e6472c3a94065 78616d706c652e6f 7267"), + password: h("70c3a4737377c3b6 7264"), + quickStretchedPW: h( + "e4e8889bd8bd61ad 6de6b95c059d56e7 b50dacdaf62bd846 44af7e2add84345d" + ), + authPW: h( + "247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375" + ), + authSalt: h( + "00f0000000000000 0000000000000000 0000000000000000 0000000000000000" + ), + }, +}; + +// A simple test suite with no utf8 encoding madness. +add_task(async function test_onepw_setup_credentials() { + let email = "francine@example.org"; + let password = CommonUtils.encodeUTF8("i like pie"); + + let pbkdf2 = CryptoUtils.pbkdf2Generate; + let hkdf = CryptoUtils.hkdfLegacy; + + // quickStretch the email + let saltyEmail = Credentials.keyWordExtended("quickStretch", email); + + Assert.equal( + b2h(saltyEmail), + "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a6672616e63696e65406578616d706c652e6f7267" + ); + + let pbkdf2Rounds = 1000; + let pbkdf2Len = 32; + + let quickStretchedPW = await pbkdf2( + password, + saltyEmail, + pbkdf2Rounds, + pbkdf2Len + ); + let quickStretchedActual = + "6b88094c1c73bbf133223f300d101ed70837af48d9d2c1b6e7d38804b20cdde4"; + Assert.equal(b2h(quickStretchedPW), quickStretchedActual); + + // obtain hkdf info + let authKeyInfo = Credentials.keyWord("authPW"); + Assert.equal( + b2h(authKeyInfo), + "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f617574685057" + ); + + // derive auth password + let hkdfSalt = h2b("00"); + let hkdfLen = 32; + let authPW = await hkdf(quickStretchedPW, hkdfSalt, authKeyInfo, hkdfLen); + + Assert.equal( + b2h(authPW), + "4b8dec7f48e7852658163601ff766124c312f9392af6c3d4e1a247eb439be342" + ); + + // derive unwrap key + let unwrapKeyInfo = Credentials.keyWord("unwrapBkey"); + let unwrapKey = await hkdf( + quickStretchedPW, + hkdfSalt, + unwrapKeyInfo, + hkdfLen + ); + + Assert.equal( + b2h(unwrapKey), + "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9" + ); +}); + +add_task(async function test_client_stretch_kdf() { + let expected = vectors["client stretch-KDF"]; + + let email = h2s(expected.email); + let password = h2s(expected.password); + + // Intermediate value from sjcl implementation in fxa-js-client + // The key thing is the c3a9 sequence in "andré" + let salt = Credentials.keyWordExtended("quickStretch", email); + Assert.equal( + b2h(salt), + "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a616e6472c3a9406578616d706c652e6f7267" + ); + + let options = { + stretchedPassLength: 32, + pbkdf2Rounds: 1000, + hkdfSalt: h2b("00"), + hkdfLength: 32, + }; + + let results = await Credentials.setup(email, password, options); + + Assert.equal( + expected.quickStretchedPW, + b2h(results.quickStretchedPW), + "quickStretchedPW is wrong" + ); + + Assert.equal(expected.authPW, b2h(results.authPW), "authPW is wrong"); +}); + +// End of tests +// Utility functions follow + +// turn formatted test vectors into normal hex strings +function h(hexStr) { + return hexStr.replace(/\s+/g, ""); +} diff --git a/services/fxaccounts/tests/xpcshell/test_device.js b/services/fxaccounts/tests/xpcshell/test_device.js new file mode 100644 index 0000000000..037db2b101 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_device.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { getFxAccountsSingleton } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const fxAccounts = getFxAccountsSingleton(); + +const { ON_NEW_DEVICE_ID, PREF_ACCOUNT_ROOT } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +function promiseObserved(topic) { + return new Promise(res => { + Services.obs.addObserver(res, topic); + }); +} + +_("Misc tests for FxAccounts.device"); + +fxAccounts.device._checkRemoteCommandsUpdateNeeded = async () => false; + +add_test(function test_default_device_name() { + // Note that head_helpers overrides getDefaultLocalName - this test is + // really just to ensure the actual implementation is sane - we can't + // really check the value it uses is correct. + // We are just hoping to avoid a repeat of bug 1369285. + let def = fxAccounts.device.getDefaultLocalName(); // make sure it doesn't throw. + _("default value is " + def); + ok(!!def.length); + + // This is obviously tied to the implementation, but we want early warning + // if any of these things fail. + // We really want one of these 2 to provide a value. + let hostname = Services.sysinfo.get("device") || Services.dns.myHostName; + _("hostname is " + hostname); + ok(!!hostname.length); + // the hostname should be in the default. + ok(def.includes(hostname)); + // We expect the following to work as a fallback to the above. + let fallback = Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ).oscpu; + _("UA fallback is " + fallback); + ok(!!fallback.length); + // the fallback should not be in the default + ok(!def.includes(fallback)); + + run_next_test(); +}); + +add_test(function test_migration() { + Services.prefs.clearUserPref("identity.fxaccounts.account.device.name"); + Services.prefs.setStringPref("services.sync.client.name", "my client name"); + // calling getLocalName() should move the name to the new pref and reset the old. + equal(fxAccounts.device.getLocalName(), "my client name"); + equal( + Services.prefs.getStringPref("identity.fxaccounts.account.device.name"), + "my client name" + ); + ok(!Services.prefs.prefHasUserValue("services.sync.client.name")); + run_next_test(); +}); + +add_test(function test_migration_set_before_get() { + Services.prefs.setStringPref("services.sync.client.name", "old client name"); + fxAccounts.device.setLocalName("new client name"); + equal(fxAccounts.device.getLocalName(), "new client name"); + run_next_test(); +}); + +add_task(async function test_reset() { + // We don't test the client name specifically here because the client name + // is set as part of signing the user in via the attempt to register the + // device. + const testPref = PREF_ACCOUNT_ROOT + "test-pref"; + Services.prefs.setStringPref(testPref, "whatever"); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + sessionToken: "dead", + verified: true, + ...MOCK_ACCOUNT_KEYS, + }; + // FxA will try to register its device record in the background after signin. + const registerDevice = sinon + .stub(fxAccounts._internal.fxAccountsClient, "registerDevice") + .callsFake(async () => { + return { id: "foo" }; + }); + await fxAccounts._internal.setSignedInUser(credentials); + // wait for device registration to complete. + await promiseObserved(ON_NEW_DEVICE_ID); + ok(!Services.prefs.prefHasUserValue(testPref)); + // signing the user out should reset the name pref. + const namePref = PREF_ACCOUNT_ROOT + "device.name"; + ok(Services.prefs.prefHasUserValue(namePref)); + await fxAccounts.signOut(/* localOnly = */ true); + ok(!Services.prefs.prefHasUserValue(namePref)); + registerDevice.restore(); +}); + +add_task(async function test_name_sanitization() { + fxAccounts.device.setLocalName("emoji is valid \u2665"); + Assert.equal(fxAccounts.device.getLocalName(), "emoji is valid \u2665"); + + let invalid = "x\uFFFD\n\r\t" + "x".repeat(255); + let sanitized = "x\uFFFD\uFFFD\uFFFD\uFFFD" + "x".repeat(250); // 255 total. + + // If the pref already has the invalid value we still get the valid one back. + Services.prefs.setStringPref( + "identity.fxaccounts.account.device.name", + invalid + ); + Assert.equal(fxAccounts.device.getLocalName(), sanitized); + + // But if we explicitly set it to an invalid name, the sanitized value ends + // up in the pref. + fxAccounts.device.setLocalName(invalid); + Assert.equal(fxAccounts.device.getLocalName(), sanitized); + Assert.equal( + Services.prefs.getStringPref("identity.fxaccounts.account.device.name"), + sanitized + ); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_keys.js b/services/fxaccounts/tests/xpcshell/test_keys.js new file mode 100644 index 0000000000..6e650a1609 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_keys.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccountsKeys } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsKeys.sys.mjs" +); + +// Ref https://github.com/mozilla/fxa-crypto-relier/ for the details +// of these test vectors. + +add_task(async function test_derive_scoped_key_test_vector() { + const keys = new FxAccountsKeys(null); + const uid = "aeaa1725c7a24ff983c6295725d5fc9b"; + const kB = "8b2e1303e21eee06a945683b8d495b9bf079ca30baa37eb8392d9ffa4767be45"; + const scopedKeyMetadata = { + identifier: "app_key:https%3A//example.com", + keyRotationTimestamp: 1510726317000, + keyRotationSecret: + "517d478cb4f994aa69930416648a416fdaa1762c5abf401a2acf11a0f185e98d", + }; + + const scopedKey = await keys._deriveScopedKey( + uid, + CommonUtils.hexToBytes(kB), + "app_key", + scopedKeyMetadata + ); + + Assert.deepEqual(scopedKey, { + kty: "oct", + kid: "1510726317-Voc-Eb9IpoTINuo9ll7bjA", + k: "Kkbk1_Q0oCcTmggeDH6880bQrxin2RLu5D00NcJazdQ", + }); +}); + +add_task(async function test_derive_legacy_sync_key_test_vector() { + const keys = new FxAccountsKeys(null); + const uid = "aeaa1725c7a24ff983c6295725d5fc9b"; + const kB = "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9"; + const scopedKeyMetadata = { + identifier: "https://identity.mozilla.com/apps/oldsync", + keyRotationTimestamp: 1510726317123, + keyRotationSecret: + "0000000000000000000000000000000000000000000000000000000000000000", + }; + + const scopedKey = await keys._deriveLegacyScopedKey( + uid, + CommonUtils.hexToBytes(kB), + "https://identity.mozilla.com/apps/oldsync", + scopedKeyMetadata + ); + + Assert.deepEqual(scopedKey, { + kty: "oct", + kid: "1510726317123-IqQv4onc7VcVE1kTQkyyOw", + k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang", + }); +}); + +add_task(async function test_derive_multiple_keys_at_once() { + const keys = new FxAccountsKeys(null); + const uid = "aeaa1725c7a24ff983c6295725d5fc9b"; + const kB = "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9"; + const scopedKeysMetadata = { + app_key: { + identifier: "app_key:https%3A//example.com", + keyRotationTimestamp: 1510726317000, + keyRotationSecret: + "517d478cb4f994aa69930416648a416fdaa1762c5abf401a2acf11a0f185e98d", + }, + "https://identity.mozilla.com/apps/oldsync": { + identifier: "https://identity.mozilla.com/apps/oldsync", + keyRotationTimestamp: 1510726318123, + keyRotationSecret: + "0000000000000000000000000000000000000000000000000000000000000000", + }, + }; + + const scopedKeys = await keys._deriveScopedKeys( + uid, + CommonUtils.hexToBytes(kB), + scopedKeysMetadata + ); + + Assert.deepEqual(scopedKeys, { + app_key: { + kty: "oct", + kid: "1510726317-tUkxiR1lTlFrTgkF0tJidA", + k: "TYK6Hmj86PfKiqsk9DZmX61nxk9VsExGrwo94HP-0wU", + }, + "https://identity.mozilla.com/apps/oldsync": { + kty: "oct", + kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw", + k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang", + }, + }); +}); + +add_task(async function test_rejects_bad_scoped_key_data() { + const keys = new FxAccountsKeys(null); + const uid = "aeaa1725c7a24ff983c6295725d5fc9b"; + const kB = "8b2e1303e21eee06a945683b8d495b9bf079ca30baa37eb8392d9ffa4767be45"; + const scopedKeyMetadata = { + identifier: "app_key:https%3A//example.com", + keyRotationTimestamp: 1510726317000, + keyRotationSecret: + "517d478cb4f994aa69930416648a416fdaa1762c5abf401a2acf11a0f185e98d", + }; + + await Assert.rejects( + keys._deriveScopedKey( + uid.slice(0, -1), + CommonUtils.hexToBytes(kB), + "app_key", + scopedKeyMetadata + ), + /uid must be a 32-character hex string/ + ); + await Assert.rejects( + keys._deriveScopedKey( + uid.slice(0, -1) + "Q", + CommonUtils.hexToBytes(kB), + "app_key", + scopedKeyMetadata + ), + /uid must be a 32-character hex string/ + ); + await Assert.rejects( + keys._deriveScopedKey( + uid, + CommonUtils.hexToBytes(kB).slice(0, -1), + "app_key", + scopedKeyMetadata + ), + /kBbytes must be exactly 32 bytes/ + ); + await Assert.rejects( + keys._deriveScopedKey(uid, CommonUtils.hexToBytes(kB), "app_key", { + ...scopedKeyMetadata, + identifier: "foo", + }), + /identifier must be a string of length >= 10/ + ); + await Assert.rejects( + keys._deriveScopedKey(uid, CommonUtils.hexToBytes(kB), "app_key", { + ...scopedKeyMetadata, + identifier: {}, + }), + /identifier must be a string of length >= 10/ + ); + await Assert.rejects( + keys._deriveScopedKey(uid, CommonUtils.hexToBytes(kB), "app_key", { + ...scopedKeyMetadata, + keyRotationTimestamp: "xyz", + }), + /keyRotationTimestamp must be a number/ + ); + await Assert.rejects( + keys._deriveScopedKey(uid, CommonUtils.hexToBytes(kB), "app_key", { + ...scopedKeyMetadata, + keyRotationTimestamp: 12345, + }), + /keyRotationTimestamp must round to a 10-digit number/ + ); + await Assert.rejects( + keys._deriveScopedKey(uid, CommonUtils.hexToBytes(kB), "app_key", { + ...scopedKeyMetadata, + keyRotationSecret: scopedKeyMetadata.keyRotationSecret.slice(0, -1), + }), + /keyRotationSecret must be a 64-character hex string/ + ); + await Assert.rejects( + keys._deriveScopedKey(uid, CommonUtils.hexToBytes(kB), "app_key", { + ...scopedKeyMetadata, + keyRotationSecret: scopedKeyMetadata.keyRotationSecret.slice(0, -1) + "z", + }), + /keyRotationSecret must be a 64-character hex string/ + ); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js new file mode 100644 index 0000000000..5b80035418 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests for FxAccounts, storage and the master password. + +// See verbose logging from FxAccounts.jsm +Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace"); + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { FXA_PWDMGR_HOST, FXA_PWDMGR_REALM } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +// Use a backstage pass to get at our LoginManagerStorage object, so we can +// mock the prototype. +var { LoginManagerStorage } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsStorage.sys.mjs" +); +var isLoggedIn = true; +LoginManagerStorage.prototype.__defineGetter__("_isLoggedIn", () => isLoggedIn); + +function setLoginMgrLoggedInState(loggedIn) { + isLoggedIn = loggedIn; +} + +initTestLogging("Trace"); + +async function getLoginMgrData() { + let logins = await Services.logins.searchLoginsAsync({ + origin: FXA_PWDMGR_HOST, + httpRealm: FXA_PWDMGR_REALM, + }); + if (!logins.length) { + return null; + } + Assert.equal(logins.length, 1, "only 1 login available"); + return logins[0]; +} + +function createFxAccounts() { + return new FxAccounts({ + _fxAccountsClient: { + async registerDevice() { + return { id: "deviceAAAAAA" }; + }, + async recoveryEmailStatus() { + return { verified: true }; + }, + async signOut() {}, + }, + updateDeviceRegistration() {}, + _getDeviceName() { + return "mock device name"; + }, + observerPreloads: [], + fxaPushService: { + async registerPushEndpoint() { + return { + endpoint: "http://mochi.test:8888", + getKey() { + return null; + }, + }; + }, + async unsubscribe() { + return true; + }, + }, + }); +} + +add_task(async function test_simple() { + let fxa = createFxAccounts(); + + let creds = { + uid: "abcd", + email: "test@example.com", + sessionToken: "sessionToken", + scopedKeys: { + ...MOCK_ACCOUNT_KEYS.scopedKeys, + }, + verified: true, + }; + await fxa._internal.setSignedInUser(creds); + + // This should have stored stuff in both the .json file in the profile + // dir, and the login dir. + let path = PathUtils.join(PathUtils.profileDir, "signedInUser.json"); + let data = await IOUtils.readJSON(path); + + Assert.strictEqual( + data.accountData.email, + creds.email, + "correct email in the clear text" + ); + Assert.strictEqual( + data.accountData.sessionToken, + creds.sessionToken, + "correct sessionToken in the clear text" + ); + Assert.strictEqual( + data.accountData.verified, + creds.verified, + "correct verified flag" + ); + + Assert.ok( + !("scopedKeys" in data.accountData), + "scopedKeys not stored in clear text" + ); + + let login = await getLoginMgrData(); + Assert.strictEqual(login.username, creds.uid, "uid used for username"); + let loginData = JSON.parse(login.password); + Assert.strictEqual( + loginData.version, + data.version, + "same version flag in both places" + ); + Assert.deepEqual( + loginData.accountData.scopedKeys, + creds.scopedKeys, + "correct scoped keys in the login mgr" + ); + Assert.ok(!("email" in loginData), "email not stored in the login mgr json"); + Assert.ok( + !("sessionToken" in loginData), + "sessionToken not stored in the login mgr json" + ); + Assert.ok( + !("verified" in loginData), + "verified not stored in the login mgr json" + ); + + await fxa.signOut(/* localOnly = */ true); + Assert.strictEqual( + await getLoginMgrData(), + null, + "login mgr data deleted on logout" + ); +}); + +add_task(async function test_MPLocked() { + let fxa = createFxAccounts(); + + let creds = { + uid: "abcd", + email: "test@example.com", + sessionToken: "sessionToken", + scopedKeys: { + ...MOCK_ACCOUNT_KEYS.scopedKeys, + }, + verified: true, + }; + + Assert.strictEqual( + await getLoginMgrData(), + null, + "no login mgr at the start" + ); + // tell the storage that the MP is locked. + setLoginMgrLoggedInState(false); + await fxa._internal.setSignedInUser(creds); + + // This should have stored stuff in the .json, and the login manager stuff + // will not exist. + let path = PathUtils.join(PathUtils.profileDir, "signedInUser.json"); + let data = await IOUtils.readJSON(path); + + Assert.strictEqual( + data.accountData.email, + creds.email, + "correct email in the clear text" + ); + Assert.strictEqual( + data.accountData.sessionToken, + creds.sessionToken, + "correct sessionToken in the clear text" + ); + Assert.strictEqual( + data.accountData.verified, + creds.verified, + "correct verified flag" + ); + + Assert.ok( + !("scopedKeys" in data.accountData), + "scopedKeys not stored in clear text" + ); + + Assert.strictEqual( + await getLoginMgrData(), + null, + "login mgr data doesn't exist" + ); + await fxa.signOut(/* localOnly = */ true); +}); + +add_task(async function test_consistentWithMPEdgeCases() { + setLoginMgrLoggedInState(true); + + let fxa = createFxAccounts(); + + let creds1 = { + uid: "uid1", + email: "test@example.com", + sessionToken: "sessionToken", + scopedKeys: { + [SCOPE_OLD_SYNC]: { + kid: "key id 1", + k: "key material 1", + kty: "oct", + }, + }, + verified: true, + }; + + let creds2 = { + uid: "uid2", + email: "test2@example.com", + sessionToken: "sessionToken2", + [SCOPE_OLD_SYNC]: { + kid: "key id 2", + k: "key material 2", + kty: "oct", + }, + verified: false, + }; + + // Log a user in while MP is unlocked. + await fxa._internal.setSignedInUser(creds1); + + // tell the storage that the MP is locked - this will prevent logout from + // being able to clear the data. + setLoginMgrLoggedInState(false); + + // now set the second credentials. + await fxa._internal.setSignedInUser(creds2); + + // We should still have creds1 data in the login manager. + let login = await getLoginMgrData(); + Assert.strictEqual(login.username, creds1.uid); + // and that we do have the first scopedKeys in the login manager. + Assert.deepEqual( + JSON.parse(login.password).accountData.scopedKeys, + creds1.scopedKeys, + "stale data still in login mgr" + ); + + // Make a new FxA instance (otherwise the values in memory will be used) + // and we want the login manager to be unlocked. + setLoginMgrLoggedInState(true); + fxa = createFxAccounts(); + + let accountData = await fxa.getSignedInUser(); + Assert.strictEqual(accountData.email, creds2.email); + // we should have no scopedKeys at all. + Assert.strictEqual( + accountData.scopedKeys, + undefined, + "stale scopedKey wasn't used" + ); + await fxa.signOut(/* localOnly = */ true); +}); + +// A test for the fact we will accept either a UID or email when looking in +// the login manager. +add_task(async function test_uidMigration() { + setLoginMgrLoggedInState(true); + Assert.strictEqual( + await getLoginMgrData(), + null, + "expect no logins at the start" + ); + + // create the login entry using email as a key. + let contents = { + scopedKeys: { + ...MOCK_ACCOUNT_KEYS.scopedKeys, + }, + }; + + let loginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" + ); + let login = new loginInfo( + FXA_PWDMGR_HOST, + null, // aFormActionOrigin, + FXA_PWDMGR_REALM, // aHttpRealm, + "foo@bar.com", // aUsername + JSON.stringify(contents), // aPassword + "", // aUsernameField + "" + ); // aPasswordField + await Services.logins.addLoginAsync(login); + + // ensure we read it. + let storage = new LoginManagerStorage(); + let got = await storage.get("uid", "foo@bar.com"); + Assert.deepEqual(got, contents); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_flow.js b/services/fxaccounts/tests/xpcshell/test_oauth_flow.js new file mode 100644 index 0000000000..ef5102ae17 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_oauth_flow.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global crypto */ + +"use strict"; + +const { + FxAccountsOAuth, + ERROR_INVALID_SCOPES, + ERROR_INVALID_STATE, + ERROR_SYNC_SCOPE_NOT_GRANTED, + ERROR_NO_KEYS_JWE, + ERROR_OAUTH_FLOW_ABANDONED, +} = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsOAuth.sys.mjs" +); + +const { SCOPE_PROFILE, FX_OAUTH_CLIENT_ID } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs", +}); + +initTestLogging("Trace"); + +add_task(function test_begin_oauth_flow() { + const oauth = new FxAccountsOAuth(); + add_task(async function test_begin_oauth_flow_invalid_scopes() { + try { + await oauth.beginOAuthFlow("foo,fi,fum", "foo"); + Assert.fail("Should have thrown error, scopes must be an array"); + } catch (e) { + Assert.equal(e.message, ERROR_INVALID_SCOPES); + } + try { + await oauth.beginOAuthFlow(["not-a-real-scope", SCOPE_PROFILE]); + Assert.fail("Should have thrown an error, must use a valid scope"); + } catch (e) { + Assert.equal(e.message, ERROR_INVALID_SCOPES); + } + }); + add_task(async function test_begin_oauth_flow_ok() { + const scopes = [SCOPE_PROFILE, SCOPE_OLD_SYNC]; + const queryParams = await oauth.beginOAuthFlow(scopes); + + // First verify default query parameters + Assert.equal(queryParams.client_id, FX_OAUTH_CLIENT_ID); + Assert.equal(queryParams.action, "email"); + Assert.equal(queryParams.response_type, "code"); + Assert.equal(queryParams.access_type, "offline"); + Assert.equal(queryParams.scope, [SCOPE_PROFILE, SCOPE_OLD_SYNC].join(" ")); + + // Then, we verify that the state is a valid Base64 value + const state = queryParams.state; + ChromeUtils.base64URLDecode(state, { padding: "reject" }); + + // Then, we verify that the codeVerifier, can be used to verify the code_challenge + const code_challenge = queryParams.code_challenge; + Assert.equal(queryParams.code_challenge_method, "S256"); + const oauthFlow = oauth.getFlow(state); + const codeVerifierB64 = oauthFlow.verifier; + const expectedChallenge = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(codeVerifierB64) + ); + const expectedChallengeB64 = ChromeUtils.base64URLEncode( + expectedChallenge, + { pad: false } + ); + Assert.equal(expectedChallengeB64, code_challenge); + + // Then, we verify that something encrypted with the `keys_jwk`, can be decrypted using the private key + const keysJwk = queryParams.keys_jwk; + const decodedKeysJwk = JSON.parse( + new TextDecoder().decode( + ChromeUtils.base64URLDecode(keysJwk, { padding: "reject" }) + ) + ); + const plaintext = "text to be encrypted and decrypted!"; + delete decodedKeysJwk.key_ops; + const jwe = await jwcrypto.generateJWE( + decodedKeysJwk, + new TextEncoder().encode(plaintext) + ); + const privateKey = oauthFlow.key; + const decrypted = await jwcrypto.decryptJWE(jwe, privateKey); + Assert.equal(new TextDecoder().decode(decrypted), plaintext); + + // Finally, we verify that we stored the requested scopes + Assert.deepEqual(oauthFlow.requestedScopes, scopes.join(" ")); + }); +}); + +add_task(function test_complete_oauth_flow() { + add_task(async function test_invalid_state() { + const oauth = new FxAccountsOAuth(); + const code = "foo"; + const state = "bar"; + const sessionToken = "01abcef12"; + try { + await oauth.completeOAuthFlow(sessionToken, code, state); + Assert.fail("Should have thrown an error"); + } catch (err) { + Assert.equal(err.message, ERROR_INVALID_STATE); + } + }); + add_task(async function test_sync_scope_not_authorized() { + const fxaClient = { + oauthToken: () => + Promise.resolve({ + access_token: "access_token", + refresh_token: "refresh_token", + // Note that the scope does not include the sync scope + scope: SCOPE_PROFILE, + }), + }; + const oauth = new FxAccountsOAuth(fxaClient); + const scopes = [SCOPE_PROFILE, SCOPE_OLD_SYNC]; + const sessionToken = "01abcef12"; + const queryParams = await oauth.beginOAuthFlow(scopes); + try { + await oauth.completeOAuthFlow(sessionToken, "foo", queryParams.state); + Assert.fail( + "Should have thrown an error because the sync scope was not authorized" + ); + } catch (err) { + Assert.equal(err.message, ERROR_SYNC_SCOPE_NOT_GRANTED); + } + }); + add_task(async function test_jwe_not_returned() { + const scopes = [SCOPE_PROFILE, SCOPE_OLD_SYNC]; + const fxaClient = { + oauthToken: () => + Promise.resolve({ + access_token: "access_token", + refresh_token: "refresh_token", + scope: scopes.join(" "), + }), + }; + const oauth = new FxAccountsOAuth(fxaClient); + const queryParams = await oauth.beginOAuthFlow(scopes); + const sessionToken = "01abcef12"; + try { + await oauth.completeOAuthFlow(sessionToken, "foo", queryParams.state); + Assert.fail( + "Should have thrown an error because we didn't get back a keys_nwe" + ); + } catch (err) { + Assert.equal(err.message, ERROR_NO_KEYS_JWE); + } + }); + add_task(async function test_complete_oauth_ok() { + // First, we initialize some fake values we would typically get + // from outside our system + const scopes = [SCOPE_PROFILE, SCOPE_OLD_SYNC]; + const oauthCode = "fake oauth code"; + const sessionToken = "01abcef12"; + const plainTextScopedKeys = { + kid: "fake key id", + k: "fake key", + kty: "oct", + }; + const fakeAccessToken = "fake access token"; + const fakeRefreshToken = "fake refresh token"; + // Then, we initialize a fake http client, we'll add our fake oauthToken call + // once we have started the oauth flow (so we have the public keys!) + const fxaClient = {}; + // Then, we initialize our oauth object with the given client and begin a new flow + const oauth = new FxAccountsOAuth(fxaClient); + const queryParams = await oauth.beginOAuthFlow(scopes); + // Now that we have the public keys in `keys_jwk`, we use it to generate a JWE + // representing our scoped keys + const keysJwk = queryParams.keys_jwk; + const decodedKeysJwk = JSON.parse( + new TextDecoder().decode( + ChromeUtils.base64URLDecode(keysJwk, { padding: "reject" }) + ) + ); + delete decodedKeysJwk.key_ops; + const jwe = await jwcrypto.generateJWE( + decodedKeysJwk, + new TextEncoder().encode(JSON.stringify(plainTextScopedKeys)) + ); + // We also grab the stored PKCE verifier that the oauth object stored internally + // to verify that we correctly send it as a part of our HTTP request + const storedVerifier = oauth.getFlow(queryParams.state).verifier; + + // To test what happens when more than one flow is completed simulatniously + // We mimic a slow network call on the first oauthToken call and let the second + // one win + let callCount = 0; + let slowResolve; + const resolveFn = (payload, resolve) => { + if (callCount === 1) { + // This is the second call + // lets resolve it so the second call wins + resolve(payload); + } else { + callCount += 1; + // This is the first call, let store our resolve function for later + // it will be resolved once the fast flow is fully completed + slowResolve = () => resolve(payload); + } + }; + + // Now we initialize our mock of the HTTP request, it verifies we passed in all the correct + // parameters and returns what we'd expect a healthy HTTP Response would look like + fxaClient.oauthToken = (sessionTokenHex, code, verifier, clientId) => { + Assert.equal(sessionTokenHex, sessionToken); + Assert.equal(code, oauthCode); + Assert.equal(verifier, storedVerifier); + Assert.equal(clientId, queryParams.client_id); + const response = { + access_token: fakeAccessToken, + refresh_token: fakeRefreshToken, + scope: scopes.join(" "), + keys_jwe: jwe, + }; + return new Promise(resolve => { + resolveFn(response, resolve); + }); + }; + + // Then, we call the completeOAuthFlow function, and get back our access token, + // refresh token and scopedKeys + + // To test what happens when multiple flows race, we create two flows, + // A slow one that will start first, but finish last + // And a fast one that will beat the slow one + const firstCompleteOAuthFlow = oauth + .completeOAuthFlow(sessionToken, oauthCode, queryParams.state) + .then(res => { + // To mimic the slow network connection on the slowCompleteOAuthFlow + // We resume the slow completeOAuthFlow once this one is complete + slowResolve(); + return res; + }); + const secondCompleteOAuthFlow = oauth + .completeOAuthFlow(sessionToken, oauthCode, queryParams.state) + .then(res => { + // since we can't fully gaurentee which oauth flow finishes first, we also resolve here + slowResolve(); + return res; + }); + + const { accessToken, refreshToken, scopedKeys } = await Promise.allSettled([ + firstCompleteOAuthFlow, + secondCompleteOAuthFlow, + ]).then(results => { + let fast; + let slow; + for (const result of results) { + if (result.status === "fulfilled") { + fast = result.value; + } else { + slow = result.reason; + } + } + // We make sure that we indeed have one slow flow that lost + Assert.equal(slow.message, ERROR_OAUTH_FLOW_ABANDONED); + return fast; + }); + + Assert.equal(accessToken, fakeAccessToken); + Assert.equal(refreshToken, fakeRefreshToken); + Assert.deepEqual(scopedKeys, plainTextScopedKeys); + + // Finally, we verify that all stored flows were cleared + Assert.equal(oauth.numOfFlows(), 0); + }); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js new file mode 100644 index 0000000000..798c439212 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js @@ -0,0 +1,180 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { FxAccountsClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsClient.sys.mjs" +); + +// We grab some additional stuff via backstage passes. +var { AccountState } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); + +function promiseNotification(topic) { + return new Promise(resolve => { + let observe = () => { + Services.obs.removeObserver(observe, topic); + resolve(); + }; + Services.obs.addObserver(observe, topic); + }); +} + +// A storage manager that doesn't actually write anywhere. +function MockStorageManager() {} + +MockStorageManager.prototype = { + promiseInitialized: Promise.resolve(), + + initialize(accountData) { + this.accountData = accountData; + }, + + finalize() { + return Promise.resolve(); + }, + + getAccountData() { + return Promise.resolve(this.accountData); + }, + + updateAccountData(updatedFields) { + for (let [name, value] of Object.entries(updatedFields)) { + if (value == null) { + delete this.accountData[name]; + } else { + this.accountData[name] = value; + } + } + return Promise.resolve(); + }, + + deleteAccountData() { + this.accountData = null; + return Promise.resolve(); + }, +}; + +// Just enough mocks so we can avoid hawk etc. +function MockFxAccountsClient() { + this._email = "nobody@example.com"; + this._verified = false; + + this.accountStatus = function (uid) { + return Promise.resolve(!!uid && !this._deletedOnServer); + }; + + this.signOut = function () { + return Promise.resolve(); + }; + this.registerDevice = function () { + return Promise.resolve(); + }; + this.updateDevice = function () { + return Promise.resolve(); + }; + this.signOutAndDestroyDevice = function () { + return Promise.resolve(); + }; + this.getDeviceList = function () { + return Promise.resolve(); + }; + + FxAccountsClient.apply(this); +} + +MockFxAccountsClient.prototype = {}; +Object.setPrototypeOf( + MockFxAccountsClient.prototype, + FxAccountsClient.prototype +); + +function MockFxAccounts(device = {}) { + return new FxAccounts({ + fxAccountsClient: new MockFxAccountsClient(), + newAccountState(credentials) { + // we use a real accountState but mocked storage. + let storage = new MockStorageManager(); + storage.initialize(credentials); + return new AccountState(storage); + }, + _getDeviceName() { + return "mock device name"; + }, + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + }); + }); + }, + }, + }); +} + +async function createMockFxA() { + let fxa = new MockFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + sessionToken: "dead", + scopedKeys: { + [SCOPE_OLD_SYNC]: { + kid: "key id for sync key", + k: "key material for sync key", + kty: "oct", + }, + }, + verified: true, + }; + await fxa._internal.setSignedInUser(credentials); + return fxa; +} + +// The tests. + +add_task(async function testCacheStorage() { + let fxa = await createMockFxA(); + + // Hook what the impl calls to save to disk. + let cas = fxa._internal.currentAccountState; + let origPersistCached = cas._persistCachedTokens.bind(cas); + cas._persistCachedTokens = function () { + return origPersistCached().then(() => { + Services.obs.notifyObservers(null, "testhelper-fxa-cache-persist-done"); + }); + }; + + let promiseWritten = promiseNotification("testhelper-fxa-cache-persist-done"); + let tokenData = { token: "token1", somethingelse: "something else" }; + let scopeArray = ["foo", "bar"]; + cas.setCachedToken(scopeArray, tokenData); + deepEqual(cas.getCachedToken(scopeArray), tokenData); + + deepEqual(cas.oauthTokens, { "bar|foo": tokenData }); + // wait for background write to complete. + await promiseWritten; + + // Check the token cache made it to our mocked storage. + deepEqual(cas.storageManager.accountData.oauthTokens, { + "bar|foo": tokenData, + }); + + // Drop the token from the cache and ensure it is removed from the json. + promiseWritten = promiseNotification("testhelper-fxa-cache-persist-done"); + await cas.removeCachedToken("token1"); + deepEqual(cas.oauthTokens, {}); + await promiseWritten; + deepEqual(cas.storageManager.accountData.oauthTokens, {}); + + // sign out and the token storage should end up with null. + let storageManager = cas.storageManager; // .signOut() removes the attribute. + await fxa.signOut(/* localOnly = */ true); + deepEqual(storageManager.accountData, null); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js new file mode 100644 index 0000000000..82f174edd1 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js @@ -0,0 +1,255 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { FxAccountsClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsClient.sys.mjs" +); +var { AccountState } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); + +function promiseNotification(topic) { + return new Promise(resolve => { + let observe = () => { + Services.obs.removeObserver(observe, topic); + resolve(); + }; + Services.obs.addObserver(observe, topic); + }); +} + +// Just enough mocks so we can avoid hawk and storage. +function MockStorageManager() {} + +MockStorageManager.prototype = { + promiseInitialized: Promise.resolve(), + + initialize(accountData) { + this.accountData = accountData; + }, + + finalize() { + return Promise.resolve(); + }, + + getAccountData() { + return Promise.resolve(this.accountData); + }, + + updateAccountData(updatedFields) { + for (let [name, value] of Object.entries(updatedFields)) { + if (value == null) { + delete this.accountData[name]; + } else { + this.accountData[name] = value; + } + } + return Promise.resolve(); + }, + + deleteAccountData() { + this.accountData = null; + return Promise.resolve(); + }, +}; + +function MockFxAccountsClient(activeTokens) { + this._email = "nobody@example.com"; + this._verified = false; + + this.accountStatus = function (uid) { + return Promise.resolve(!!uid && !this._deletedOnServer); + }; + + this.signOut = function () { + return Promise.resolve(); + }; + this.registerDevice = function () { + return Promise.resolve(); + }; + this.updateDevice = function () { + return Promise.resolve(); + }; + this.signOutAndDestroyDevice = function () { + return Promise.resolve(); + }; + this.getDeviceList = function () { + return Promise.resolve(); + }; + this.accessTokenWithSessionToken = function ( + sessionTokenHex, + clientId, + scope, + ttl + ) { + let token = `token${this.numTokenFetches}`; + if (ttl) { + token += `-ttl-${ttl}`; + } + this.numTokenFetches += 1; + this.activeTokens.add(token); + print("accessTokenWithSessionToken returning token", token); + return Promise.resolve({ access_token: token, ttl }); + }; + this.oauthDestroy = sinon.stub().callsFake((_clientId, token) => { + this.activeTokens.delete(token); + return Promise.resolve(); + }); + + // Test only stuff. + this.activeTokens = activeTokens; + this.numTokenFetches = 0; + + FxAccountsClient.apply(this); +} + +MockFxAccountsClient.prototype = {}; +Object.setPrototypeOf( + MockFxAccountsClient.prototype, + FxAccountsClient.prototype +); + +function MockFxAccounts() { + // The FxA "auth" and "oauth" servers both share the same db of tokens, + // so we need to simulate the same here in the tests. + const activeTokens = new Set(); + return new FxAccounts({ + fxAccountsClient: new MockFxAccountsClient(activeTokens), + newAccountState(credentials) { + // we use a real accountState but mocked storage. + let storage = new MockStorageManager(); + storage.initialize(credentials); + return new AccountState(storage); + }, + _getDeviceName() { + return "mock device name"; + }, + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + }); + }); + }, + }, + }); +} + +async function createMockFxA() { + let fxa = new MockFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + sessionToken: "dead", + scopedKeys: { + [SCOPE_OLD_SYNC]: { + kid: "key id for sync key", + k: "key material for sync key", + kty: "oct", + }, + }, + verified: true, + }; + + await fxa._internal.setSignedInUser(credentials); + return fxa; +} + +// The tests. + +add_task(async function testRevoke() { + let tokenOptions = { scope: "test-scope" }; + let fxa = await createMockFxA(); + let client = fxa._internal.fxAccountsClient; + + // get our first token and check we hit the mock. + let token1 = await fxa.getOAuthToken(tokenOptions); + equal(client.numTokenFetches, 1); + equal(client.activeTokens.size, 1); + ok(token1, "got a token"); + equal(token1, "token0"); + + // drop the new token from our cache. + await fxa.removeCachedOAuthToken({ token: token1 }); + ok(client.oauthDestroy.calledOnce); + + // the revoke should have been successful. + equal(client.activeTokens.size, 0); + // fetching it again hits the server. + let token2 = await fxa.getOAuthToken(tokenOptions); + equal(client.numTokenFetches, 2); + equal(client.activeTokens.size, 1); + ok(token2, "got a token"); + notEqual(token1, token2, "got a different token"); +}); + +add_task(async function testSignOutDestroysTokens() { + let fxa = await createMockFxA(); + let client = fxa._internal.fxAccountsClient; + + // get our first token and check we hit the mock. + let token1 = await fxa.getOAuthToken({ scope: "test-scope" }); + equal(client.numTokenFetches, 1); + equal(client.activeTokens.size, 1); + ok(token1, "got a token"); + + // get another + let token2 = await fxa.getOAuthToken({ scope: "test-scope-2" }); + equal(client.numTokenFetches, 2); + equal(client.activeTokens.size, 2); + ok(token2, "got a token"); + notEqual(token1, token2, "got a different token"); + + // FxA fires an observer when the "background" signout is complete. + let signoutComplete = promiseNotification("testhelper-fxa-signout-complete"); + // now sign out - they should be removed. + await fxa.signOut(); + await signoutComplete; + ok(client.oauthDestroy.calledTwice); + // No active tokens left. + equal(client.activeTokens.size, 0); +}); + +add_task(async function testTokenRaces() { + // Here we do 2 concurrent fetches each for 2 different token scopes (ie, + // 4 token fetches in total). + // This should provoke a potential race in the token fetching but we use + // a map of in-flight token fetches, so we should still only perform 2 + // fetches, but each of the 4 calls should resolve with the correct values. + let fxa = await createMockFxA(); + let client = fxa._internal.fxAccountsClient; + + let results = await Promise.all([ + fxa.getOAuthToken({ scope: "test-scope" }), + fxa.getOAuthToken({ scope: "test-scope" }), + fxa.getOAuthToken({ scope: "test-scope-2" }), + fxa.getOAuthToken({ scope: "test-scope-2" }), + ]); + + equal(client.numTokenFetches, 2, "should have fetched 2 tokens."); + + // Should have 2 unique tokens + results.sort(); + equal(results[0], results[1]); + equal(results[2], results[3]); + // should be 2 active. + equal(client.activeTokens.size, 2); + await fxa.removeCachedOAuthToken({ token: results[0] }); + equal(client.activeTokens.size, 1); + await fxa.removeCachedOAuthToken({ token: results[2] }); + equal(client.activeTokens.size, 0); + ok(client.oauthDestroy.calledTwice); +}); + +add_task(async function testTokenTTL() { + // This tests the TTL option passed into the method + let fxa = await createMockFxA(); + let token = await fxa.getOAuthToken({ scope: "test-ttl", ttl: 1000 }); + equal(token, "token0-ttl-1000"); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_pairing.js b/services/fxaccounts/tests/xpcshell/test_pairing.js new file mode 100644 index 0000000000..eac3112242 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_pairing.js @@ -0,0 +1,384 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccountsPairingFlow } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsPairing.sys.mjs" +); +const { EventEmitter } = ChromeUtils.importESModule( + "resource://gre/modules/EventEmitter.sys.mjs" +); +ChromeUtils.defineESModuleGetters(this, { + jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs", +}); + +const CHANNEL_ID = "sW-UA97Q6Dljqen7XRlYPw"; +const CHANNEL_KEY = crypto.getRandomValues(new Uint8Array(32)); + +const SENDER_SUPP = { + ua: "Firefox Supp", + city: "Nice", + region: "PACA", + country: "France", + remote: "127.0.0.1", +}; +const UID = "abcd"; +const EMAIL = "foo@bar.com"; +const AVATAR = "https://foo.bar/avatar"; +const DISPLAY_NAME = "Foo bar"; +const DEVICE_NAME = "Foo's computer"; + +const PAIR_URI = "https://foo.bar/pair"; +const OAUTH_URI = "https://foo.bar/oauth"; +const KSYNC = "myksync"; +const SESSION = "mysession"; +const fxaConfig = { + promisePairingURI() { + return PAIR_URI; + }, + promiseOAuthURI() { + return OAUTH_URI; + }, +}; +const fxAccounts = { + getSignedInUser() { + return { + uid: UID, + email: EMAIL, + avatar: AVATAR, + displayName: DISPLAY_NAME, + }; + }, + async _withVerifiedAccountState(cb) { + return cb({ + async getUserAccountData() { + return { + sessionToken: SESSION, + }; + }, + }); + }, + _internal: { + keys: { + getKeyForScope(scope) { + return { + kid: "123456", + k: KSYNC, + kty: "oct", + }; + }, + }, + fxAccountsClient: { + async getScopedKeyData() { + return { + "https://identity.mozilla.com/apps/oldsync": { + identifier: "https://identity.mozilla.com/apps/oldsync", + keyRotationTimestamp: 12345678, + }, + }; + }, + async oauthAuthorize() { + return { code: "mycode", state: "mystate" }; + }, + }, + }, +}; +const weave = { + Service: { clientsEngine: { localName: DEVICE_NAME } }, +}; + +class MockPairingChannel extends EventTarget { + get channelId() { + return CHANNEL_ID; + } + + get channelKey() { + return CHANNEL_KEY; + } + + send(data) { + this.dispatchEvent( + new CustomEvent("send", { + detail: { data }, + }) + ); + } + + simulateIncoming(data) { + this.dispatchEvent( + new CustomEvent("message", { + detail: { data, sender: SENDER_SUPP }, + }) + ); + } + + close() { + this.closed = true; + } +} + +add_task(async function testFullFlow() { + const emitter = new EventEmitter(); + const pairingChannel = new MockPairingChannel(); + const pairingUri = await FxAccountsPairingFlow.start({ + emitter, + pairingChannel, + fxAccounts, + fxaConfig, + weave, + }); + Assert.equal( + pairingUri, + `${PAIR_URI}#channel_id=${CHANNEL_ID}&channel_key=${ChromeUtils.base64URLEncode( + CHANNEL_KEY, + { pad: false } + )}` + ); + + const flow = FxAccountsPairingFlow.get(CHANNEL_ID); + + const promiseSwitchToWebContent = emitter.once("view:SwitchToWebContent"); + const promiseMetadataSent = promiseOutgoingMessage(pairingChannel); + const epk = await generateEphemeralKeypair(); + + pairingChannel.simulateIncoming({ + message: "pair:supp:request", + data: { + client_id: "client_id_1", + state: "mystate", + keys_jwk: ChromeUtils.base64URLEncode( + new TextEncoder().encode(JSON.stringify(epk.publicJWK)), + { pad: false } + ), + scope: "profile https://identity.mozilla.com/apps/oldsync", + code_challenge: "chal", + code_challenge_method: "S256", + }, + }); + const sentAuthMetadata = await promiseMetadataSent; + Assert.deepEqual(sentAuthMetadata, { + message: "pair:auth:metadata", + data: { + email: EMAIL, + avatar: AVATAR, + displayName: DISPLAY_NAME, + deviceName: DEVICE_NAME, + }, + }); + const oauthUrl = await promiseSwitchToWebContent; + Assert.equal( + oauthUrl, + `${OAUTH_URI}?client_id=client_id_1&scope=profile+https%3A%2F%2Fidentity.mozilla.com%2Fapps%2Foldsync&email=foo%40bar.com&uid=abcd&channel_id=${CHANNEL_ID}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob%3Apair-auth-webchannel` + ); + + let pairSuppMetadata = await simulateIncomingWebChannel( + flow, + "fxaccounts:pair_supplicant_metadata" + ); + Assert.deepEqual( + { + ua: "Firefox Supp", + city: "Nice", + region: "PACA", + country: "France", + ipAddress: "127.0.0.1", + }, + pairSuppMetadata + ); + + const generateJWE = sinon.spy(jwcrypto, "generateJWE"); + const oauthAuthorize = sinon.spy( + fxAccounts._internal.fxAccountsClient, + "oauthAuthorize" + ); + const promiseOAuthParamsMsg = promiseOutgoingMessage(pairingChannel); + await simulateIncomingWebChannel(flow, "fxaccounts:pair_authorize"); + // We should have generated the expected JWE. + Assert.ok(generateJWE.calledOnce); + const generateArgs = generateJWE.firstCall.args; + Assert.deepEqual(generateArgs[0], epk.publicJWK); + Assert.deepEqual(JSON.parse(new TextDecoder().decode(generateArgs[1])), { + "https://identity.mozilla.com/apps/oldsync": { + kid: "123456", + k: KSYNC, + kty: "oct", + }, + }); + // We should have authorized an oauth code with expected parameters. + Assert.ok(oauthAuthorize.calledOnce); + const oauthCodeArgs = oauthAuthorize.firstCall.args[1]; + console.log(oauthCodeArgs); + Assert.ok(!oauthCodeArgs.keys_jwk); + Assert.deepEqual( + oauthCodeArgs.keys_jwe, + await generateJWE.firstCall.returnValue + ); + Assert.equal(oauthCodeArgs.client_id, "client_id_1"); + Assert.equal(oauthCodeArgs.access_type, "offline"); + Assert.equal(oauthCodeArgs.state, "mystate"); + Assert.equal( + oauthCodeArgs.scope, + "profile https://identity.mozilla.com/apps/oldsync" + ); + Assert.equal(oauthCodeArgs.code_challenge, "chal"); + Assert.equal(oauthCodeArgs.code_challenge_method, "S256"); + + const oAuthParams = await promiseOAuthParamsMsg; + Assert.deepEqual(oAuthParams, { + message: "pair:auth:authorize", + data: { code: "mycode", state: "mystate" }, + }); + + let heartbeat = await simulateIncomingWebChannel( + flow, + "fxaccounts:pair_heartbeat" + ); + Assert.ok(!heartbeat.suppAuthorized); + + await pairingChannel.simulateIncoming({ + message: "pair:supp:authorize", + }); + + heartbeat = await simulateIncomingWebChannel( + flow, + "fxaccounts:pair_heartbeat" + ); + Assert.ok(heartbeat.suppAuthorized); + + await simulateIncomingWebChannel(flow, "fxaccounts:pair_complete"); + // The flow should have been destroyed! + Assert.ok(!FxAccountsPairingFlow.get(CHANNEL_ID)); + Assert.ok(pairingChannel.closed); + generateJWE.restore(); + oauthAuthorize.restore(); +}); + +add_task(async function testUnknownPairingMessage() { + const emitter = new EventEmitter(); + const pairingChannel = new MockPairingChannel(); + await FxAccountsPairingFlow.start({ + emitter, + pairingChannel, + fxAccounts, + fxaConfig, + weave, + }); + const flow = FxAccountsPairingFlow.get(CHANNEL_ID); + const viewErrorObserved = emitter.once("view:Error"); + pairingChannel.simulateIncoming({ + message: "pair:boom", + }); + await viewErrorObserved; + let heartbeat = await simulateIncomingWebChannel( + flow, + "fxaccounts:pair_heartbeat" + ); + Assert.ok(heartbeat.err); +}); + +add_task(async function testUnknownWebChannelCommand() { + const emitter = new EventEmitter(); + const pairingChannel = new MockPairingChannel(); + await FxAccountsPairingFlow.start({ + emitter, + pairingChannel, + fxAccounts, + fxaConfig, + weave, + }); + const flow = FxAccountsPairingFlow.get(CHANNEL_ID); + const viewErrorObserved = emitter.once("view:Error"); + await simulateIncomingWebChannel(flow, "fxaccounts:boom"); + await viewErrorObserved; + let heartbeat = await simulateIncomingWebChannel( + flow, + "fxaccounts:pair_heartbeat" + ); + Assert.ok(heartbeat.err); +}); + +add_task(async function testPairingChannelFailure() { + const emitter = new EventEmitter(); + const pairingChannel = new MockPairingChannel(); + await FxAccountsPairingFlow.start({ + emitter, + pairingChannel, + fxAccounts, + fxaConfig, + weave, + }); + const flow = FxAccountsPairingFlow.get(CHANNEL_ID); + const viewErrorObserved = emitter.once("view:Error"); + sinon.stub(pairingChannel, "send").callsFake(() => { + throw new Error("Boom!"); + }); + pairingChannel.simulateIncoming({ + message: "pair:supp:request", + data: { + client_id: "client_id_1", + state: "mystate", + scope: "profile https://identity.mozilla.com/apps/oldsync", + code_challenge: "chal", + code_challenge_method: "S256", + }, + }); + await viewErrorObserved; + + let heartbeat = await simulateIncomingWebChannel( + flow, + "fxaccounts:pair_heartbeat" + ); + Assert.ok(heartbeat.err); +}); + +add_task(async function testFlowTimeout() { + const emitter = new EventEmitter(); + const pairingChannel = new MockPairingChannel(); + const viewErrorObserved = emitter.once("view:Error"); + await FxAccountsPairingFlow.start({ + emitter, + pairingChannel, + fxAccounts, + fxaConfig, + weave, + flowTimeout: 1, + }); + const flow = FxAccountsPairingFlow.get(CHANNEL_ID); + await viewErrorObserved; + + let heartbeat = await simulateIncomingWebChannel( + flow, + "fxaccounts:pair_heartbeat" + ); + Assert.ok(heartbeat.err.match(/Timeout/)); +}); + +async function simulateIncomingWebChannel(flow, command) { + return flow.onWebChannelMessage(command); +} + +async function promiseOutgoingMessage(pairingChannel) { + return new Promise(res => { + const onMessage = event => { + pairingChannel.removeEventListener("send", onMessage); + res(event.detail.data); + }; + pairingChannel.addEventListener("send", onMessage); + }); +} + +async function generateEphemeralKeypair() { + const keypair = await crypto.subtle.generateKey( + { name: "ECDH", namedCurve: "P-256" }, + true, + ["deriveKey"] + ); + const publicJWK = await crypto.subtle.exportKey("jwk", keypair.publicKey); + const privateJWK = await crypto.subtle.exportKey("jwk", keypair.privateKey); + delete publicJWK.key_ops; + return { + publicJWK, + privateJWK, + }; +} diff --git a/services/fxaccounts/tests/xpcshell/test_profile.js b/services/fxaccounts/tests/xpcshell/test_profile.js new file mode 100644 index 0000000000..f8137b5691 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_profile.js @@ -0,0 +1,677 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ON_PROFILE_CHANGE_NOTIFICATION, log } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); +const { FxAccountsProfileClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsProfileClient.sys.mjs" +); +const { FxAccountsProfile } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsProfile.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let mockClient = function (fxa) { + let options = { + serverURL: "http://127.0.0.1:1111/v1", + fxa, + }; + return new FxAccountsProfileClient(options); +}; + +const ACCOUNT_UID = "abc123"; +const ACCOUNT_EMAIL = "foo@bar.com"; +const ACCOUNT_DATA = { + uid: ACCOUNT_UID, + email: ACCOUNT_EMAIL, +}; + +let mockFxa = function () { + let fxa = { + // helpers to make the tests using this mock less verbose... + set _testProfileCache(profileCache) { + this._internal.currentAccountState._data.profileCache = profileCache; + }, + get _testProfileCache() { + return this._internal.currentAccountState._data.profileCache; + }, + }; + fxa._internal = Object.assign( + {}, + { + currentAccountState: Object.assign( + {}, + { + _data: Object.assign({}, ACCOUNT_DATA), + + get isCurrent() { + return true; + }, + + async getUserAccountData() { + return this._data; + }, + + async updateUserAccountData(data) { + this._data = Object.assign(this._data, data); + }, + } + ), + + withCurrentAccountState(cb) { + return cb(this.currentAccountState); + }, + + async _handleTokenError(err) { + // handleTokenError always rethrows. + throw err; + }, + } + ); + return fxa; +}; + +function CreateFxAccountsProfile(fxa = null, client = null) { + if (!fxa) { + fxa = mockFxa(); + } + let options = { + fxai: fxa._internal, + profileServerUrl: "http://127.0.0.1:1111/v1", + }; + if (client) { + options.profileClient = client; + } + return new FxAccountsProfile(options); +} + +add_test(function cacheProfile_change() { + let setProfileCacheCalled = false; + let fxa = mockFxa(); + fxa._internal.currentAccountState.updateUserAccountData = data => { + setProfileCacheCalled = true; + Assert.equal(data.profileCache.profile.avatar, "myurl"); + Assert.equal(data.profileCache.etag, "bogusetag"); + return Promise.resolve(); + }; + let profile = CreateFxAccountsProfile(fxa); + + makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { + Assert.equal(data, ACCOUNT_DATA.uid); + Assert.ok(setProfileCacheCalled); + run_next_test(); + }); + + return profile._cacheProfile({ + body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myurl" }, + etag: "bogusetag", + }); +}); + +add_test(function fetchAndCacheProfile_ok() { + let client = mockClient(mockFxa()); + client.fetchProfile = function () { + return Promise.resolve({ body: { uid: ACCOUNT_UID, avatar: "myimg" } }); + }; + let profile = CreateFxAccountsProfile(null, client); + profile._cachedAt = 12345; + + profile._cacheProfile = function (toCache) { + Assert.equal(toCache.body.avatar, "myimg"); + return Promise.resolve(toCache.body); + }; + + return profile._fetchAndCacheProfile().then(result => { + Assert.equal(result.avatar, "myimg"); + Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped"); + run_next_test(); + }); +}); + +add_test(function fetchAndCacheProfile_always_bumps_cachedAt() { + let client = mockClient(mockFxa()); + client.fetchProfile = function () { + return Promise.reject(new Error("oops")); + }; + let profile = CreateFxAccountsProfile(null, client); + profile._cachedAt = 12345; + + return profile._fetchAndCacheProfile().then( + result => { + do_throw("Should not succeed"); + }, + err => { + Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped"); + run_next_test(); + } + ); +}); + +add_test(function fetchAndCacheProfile_sendsETag() { + let fxa = mockFxa(); + fxa._testProfileCache = { profile: {}, etag: "bogusETag" }; + let client = mockClient(fxa); + client.fetchProfile = function (etag) { + Assert.equal(etag, "bogusETag"); + return Promise.resolve({ + body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, + }); + }; + let profile = CreateFxAccountsProfile(fxa, client); + + return profile._fetchAndCacheProfile().then(result => { + run_next_test(); + }); +}); + +// Check that a second profile request when one is already in-flight reuses +// the in-flight one. +add_task(async function fetchAndCacheProfileOnce() { + // A promise that remains unresolved while we fire off 2 requests for + // a profile. + let resolveProfile; + let promiseProfile = new Promise(resolve => { + resolveProfile = resolve; + }); + let numFetches = 0; + let client = mockClient(mockFxa()); + client.fetchProfile = function () { + numFetches += 1; + return promiseProfile; + }; + let fxa = mockFxa(); + let profile = CreateFxAccountsProfile(fxa, client); + + let request1 = profile._fetchAndCacheProfile(); + profile._fetchAndCacheProfile(); + await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise) + + // should be one request made to fetch the profile (but the promise returned + // by it remains unresolved) + Assert.equal(numFetches, 1); + + // resolve the promise. + resolveProfile({ + body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, + }); + + // both requests should complete with the same data. + let got1 = await request1; + Assert.equal(got1.avatar, "myimg"); + let got2 = await request1; + Assert.equal(got2.avatar, "myimg"); + + // and still only 1 request was made. + Assert.equal(numFetches, 1); +}); + +// Check that sharing a single fetch promise works correctly when the promise +// is rejected. +add_task(async function fetchAndCacheProfileOnce() { + // A promise that remains unresolved while we fire off 2 requests for + // a profile. + let rejectProfile; + let promiseProfile = new Promise((resolve, reject) => { + rejectProfile = reject; + }); + let numFetches = 0; + let client = mockClient(mockFxa()); + client.fetchProfile = function () { + numFetches += 1; + return promiseProfile; + }; + let fxa = mockFxa(); + let profile = CreateFxAccountsProfile(fxa, client); + + let request1 = profile._fetchAndCacheProfile(); + let request2 = profile._fetchAndCacheProfile(); + await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise) + + // should be one request made to fetch the profile (but the promise returned + // by it remains unresolved) + Assert.equal(numFetches, 1); + + // reject the promise. + rejectProfile("oh noes"); + + // both requests should reject. + try { + await request1; + throw new Error("should have rejected"); + } catch (ex) { + if (ex != "oh noes") { + throw ex; + } + } + try { + await request2; + throw new Error("should have rejected"); + } catch (ex) { + if (ex != "oh noes") { + throw ex; + } + } + + // but a new request should works. + client.fetchProfile = function () { + return Promise.resolve({ + body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, + }); + }; + + let got = await profile._fetchAndCacheProfile(); + Assert.equal(got.avatar, "myimg"); +}); + +add_test(function fetchAndCacheProfile_alreadyCached() { + let cachedUrl = "cachedurl"; + let fxa = mockFxa(); + fxa._testProfileCache = { + profile: { uid: ACCOUNT_UID, avatar: cachedUrl }, + etag: "bogusETag", + }; + let client = mockClient(fxa); + client.fetchProfile = function (etag) { + Assert.equal(etag, "bogusETag"); + return Promise.resolve(null); + }; + + let profile = CreateFxAccountsProfile(fxa, client); + profile._cacheProfile = function (toCache) { + do_throw("This method should not be called."); + }; + + return profile._fetchAndCacheProfile().then(result => { + Assert.equal(result, null); + Assert.equal(fxa._testProfileCache.profile.avatar, cachedUrl); + run_next_test(); + }); +}); + +// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the +// last one doesn't kick off a new request to check the cached copy is fresh. +add_task(async function fetchAndCacheProfileAfterThreshold() { + /* + * This test was observed to cause a timeout for... any timer precision reduction. + * Even 1 us. Exact reason is still undiagnosed. + */ + Services.prefs.setBoolPref("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("privacy.reduceTimerPrecision"); + }); + + let numFetches = 0; + let client = mockClient(mockFxa()); + client.fetchProfile = async function () { + numFetches += 1; + return { + body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, + }; + }; + let profile = CreateFxAccountsProfile(null, client); + profile.PROFILE_FRESHNESS_THRESHOLD = 1000; + + // first fetch should return null as we don't have data. + let p = await profile.getProfile(); + Assert.equal(p, null); + // ensure we kicked off a fetch. + Assert.notEqual(profile._currentFetchPromise, null); + // wait for that fetch to finish + await profile._currentFetchPromise; + Assert.equal(numFetches, 1); + Assert.equal(profile._currentFetchPromise, null); + + await profile.getProfile(); + Assert.equal(numFetches, 1); + Assert.equal(profile._currentFetchPromise, null); + + await new Promise(resolve => { + do_timeout(1000, resolve); + }); + + let origFetchAndCatch = profile._fetchAndCacheProfile; + let backgroundFetchDone = Promise.withResolvers(); + profile._fetchAndCacheProfile = async () => { + await origFetchAndCatch.call(profile); + backgroundFetchDone.resolve(); + }; + await profile.getProfile(); + await backgroundFetchDone.promise; + Assert.equal(numFetches, 2); +}); + +add_task(async function test_ensureProfile() { + let client = new FxAccountsProfileClient({ + serverURL: "http://127.0.0.1:1111/v1", + fxa: mockFxa(), + }); + let profile = CreateFxAccountsProfile(null, client); + + const testCases = [ + // profile retrieval when there is no cached profile info + { + threshold: 1000, + expectsCachedProfileReturned: false, + cachedProfile: null, + fetchedProfile: { + uid: ACCOUNT_UID, + email: ACCOUNT_EMAIL, + avatar: "myimg", + }, + }, + // profile retrieval when the cached profile is recent + { + // Note: The threshold for this test case is being set to an arbitrary value that will + // be greater than Date.now() so the retrieved cached profile will be deemed recent. + threshold: Date.now() + 5000, + expectsCachedProfileReturned: true, + cachedProfile: { + uid: `${ACCOUNT_UID}2`, + email: `${ACCOUNT_EMAIL}2`, + avatar: "myimg2", + }, + }, + // profile retrieval when the cached profile is old and a new profile is fetched + { + threshold: 1000, + expectsCachedProfileReturned: false, + cachedProfile: { + uid: `${ACCOUNT_UID}3`, + email: `${ACCOUNT_EMAIL}3`, + avatar: "myimg3", + }, + fetchAndCacheProfileResolves: true, + fetchedProfile: { + uid: `${ACCOUNT_UID}4`, + email: `${ACCOUNT_EMAIL}4`, + avatar: "myimg4", + }, + }, + // profile retrieval when the cached profile is old and a null profile is fetched + { + threshold: 1000, + expectsCachedProfileReturned: false, + cachedProfile: { + uid: `${ACCOUNT_UID}5`, + email: `${ACCOUNT_EMAIL}5`, + avatar: "myimg5", + }, + fetchAndCacheProfileResolves: true, + fetchedProfile: null, + }, + // profile retrieval when the cached profile is old and fetching a new profile errors + { + threshold: 1000, + expectsCachedProfileReturned: false, + cachedProfile: { + uid: `${ACCOUNT_UID}6`, + email: `${ACCOUNT_EMAIL}6`, + avatar: "myimg6", + }, + fetchAndCacheProfileResolves: false, + }, + // profile retrieval when we've cached a failure to fetch profile data + { + // Note: The threshold for this test case is being set to an arbitrary value that will + // be greater than Date.now() so the retrieved cached profile will be deemed recent. + threshold: Date.now() + 5000, + expectsCachedProfileReturned: false, + cachedProfile: null, + fetchedProfile: { + uid: `${ACCOUNT_UID}7`, + email: `${ACCOUNT_EMAIL}7`, + avatar: "myimg7", + }, + fetchAndCacheProfileResolves: true, + }, + // profile retrieval when the cached profile is old but staleOk is true. + { + threshold: 1000, + expectsCachedProfileReturned: true, + cachedProfile: { + uid: `${ACCOUNT_UID}8`, + email: `${ACCOUNT_EMAIL}8`, + avatar: "myimg8", + }, + fetchAndCacheProfileResolves: false, + options: { staleOk: true }, + }, + // staleOk but no cached profile + { + threshold: 1000, + expectsCachedProfileReturned: false, + cachedProfile: null, + fetchedProfile: { + uid: `${ACCOUNT_UID}9`, + email: `${ACCOUNT_EMAIL}9`, + avatar: "myimg9", + }, + options: { staleOk: true }, + }, + // fresh profile but forceFresh = true + { + // Note: The threshold for this test case is being set to an arbitrary value that will + // be greater than Date.now() so the retrieved cached profile will be deemed recent. + threshold: Date.now() + 5000, + expectsCachedProfileReturned: false, + fetchedProfile: { + uid: `${ACCOUNT_UID}10`, + email: `${ACCOUNT_EMAIL}10`, + avatar: "myimg10", + }, + options: { forceFresh: true }, + }, + ]; + + for (const tc of testCases) { + print(`test case: ${JSON.stringify(tc)}`); + let mockProfile = sinon.mock(profile); + mockProfile + .expects("_getProfileCache") + .once() + .returns( + tc.cachedProfile + ? { + profile: tc.cachedProfile, + } + : null + ); + profile.PROFILE_FRESHNESS_THRESHOLD = tc.threshold; + + let options = tc.options || {}; + if (tc.expectsCachedProfileReturned) { + mockProfile.expects("_fetchAndCacheProfile").never(); + let actualProfile = await profile.ensureProfile(options); + mockProfile.verify(); + Assert.equal(actualProfile, tc.cachedProfile); + } else if (tc.fetchAndCacheProfileResolves) { + mockProfile + .expects("_fetchAndCacheProfile") + .once() + .resolves(tc.fetchedProfile); + + let actualProfile = await profile.ensureProfile(options); + let expectedProfile = tc.fetchedProfile + ? tc.fetchedProfile + : tc.cachedProfile; + mockProfile.verify(); + Assert.equal(actualProfile, expectedProfile); + } else { + mockProfile.expects("_fetchAndCacheProfile").once().rejects(); + + let actualProfile = await profile.ensureProfile(options); + mockProfile.verify(); + Assert.equal(actualProfile, tc.cachedProfile); + } + } +}); + +// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the +// last one *does* kick off a new request if ON_PROFILE_CHANGE_NOTIFICATION +// is sent. +add_task(async function fetchAndCacheProfileBeforeThresholdOnNotification() { + let numFetches = 0; + let client = mockClient(mockFxa()); + client.fetchProfile = async function () { + numFetches += 1; + return { + body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, + }; + }; + let profile = CreateFxAccountsProfile(null, client); + profile.PROFILE_FRESHNESS_THRESHOLD = 1000; + + // first fetch should return null as we don't have data. + let p = await profile.getProfile(); + Assert.equal(p, null); + // ensure we kicked off a fetch. + Assert.notEqual(profile._currentFetchPromise, null); + // wait for that fetch to finish + await profile._currentFetchPromise; + Assert.equal(numFetches, 1); + Assert.equal(profile._currentFetchPromise, null); + + Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION); + + let origFetchAndCatch = profile._fetchAndCacheProfile; + let backgroundFetchDone = Promise.withResolvers(); + profile._fetchAndCacheProfile = async () => { + await origFetchAndCatch.call(profile); + backgroundFetchDone.resolve(); + }; + await profile.getProfile(); + await backgroundFetchDone.promise; + Assert.equal(numFetches, 2); +}); + +add_test(function tearDown_ok() { + let profile = CreateFxAccountsProfile(); + + Assert.ok(!!profile.client); + Assert.ok(!!profile.fxai); + + profile.tearDown(); + Assert.equal(null, profile.fxai); + Assert.equal(null, profile.client); + + run_next_test(); +}); + +add_task(async function getProfile_ok() { + let cachedUrl = "myurl"; + let didFetch = false; + + let fxa = mockFxa(); + fxa._testProfileCache = { profile: { uid: ACCOUNT_UID, avatar: cachedUrl } }; + let profile = CreateFxAccountsProfile(fxa); + + profile._fetchAndCacheProfile = function () { + didFetch = true; + return Promise.resolve(); + }; + + let result = await profile.getProfile(); + + Assert.equal(result.avatar, cachedUrl); + Assert.ok(didFetch); +}); + +add_task(async function getProfile_no_cache() { + let fetchedUrl = "newUrl"; + let fxa = mockFxa(); + let profile = CreateFxAccountsProfile(fxa); + + profile._fetchAndCacheProfileInternal = function () { + return Promise.resolve({ uid: ACCOUNT_UID, avatar: fetchedUrl }); + }; + + await profile.getProfile(); // returns null. + let result = await profile._currentFetchPromise; + Assert.equal(result.avatar, fetchedUrl); +}); + +add_test(function getProfile_has_cached_fetch_deleted() { + let cachedUrl = "myurl"; + + let fxa = mockFxa(); + let client = mockClient(fxa); + client.fetchProfile = function () { + return Promise.resolve({ + body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: null }, + }); + }; + + let profile = CreateFxAccountsProfile(fxa, client); + fxa._testProfileCache = { + profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: cachedUrl }, + }; + + // instead of checking this in a mocked "save" function, just check after the + // observer + makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { + profile.getProfile().then(profileData => { + Assert.equal(null, profileData.avatar); + run_next_test(); + }); + }); + + return profile.getProfile().then(result => { + Assert.equal(result.avatar, "myurl"); + }); +}); + +add_test(function getProfile_fetchAndCacheProfile_throws() { + let fxa = mockFxa(); + fxa._testProfileCache = { + profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, + }; + let profile = CreateFxAccountsProfile(fxa); + + profile._fetchAndCacheProfile = () => Promise.reject(new Error()); + + return profile.getProfile().then(result => { + Assert.equal(result.avatar, "myimg"); + run_next_test(); + }); +}); + +add_test(function getProfile_email_changed() { + let fxa = mockFxa(); + let client = mockClient(fxa); + client.fetchProfile = function () { + return Promise.resolve({ + body: { uid: ACCOUNT_UID, email: "newemail@bar.com" }, + }); + }; + fxa._internal._handleEmailUpdated = email => { + Assert.equal(email, "newemail@bar.com"); + run_next_test(); + }; + + let profile = CreateFxAccountsProfile(fxa, client); + return profile._fetchAndCacheProfile(); +}); + +function makeObserver(aObserveTopic, aObserveFunc) { + let callback = function (aSubject, aTopic, aData) { + log.debug("observed " + aTopic + " " + aData); + if (aTopic == aObserveTopic) { + removeMe(); + aObserveFunc(aSubject, aTopic, aData); + } + }; + + function removeMe() { + log.debug("removing observer for " + aObserveTopic); + Services.obs.removeObserver(callback, aObserveTopic); + } + + Services.obs.addObserver(callback, aObserveTopic); + return removeMe; +} diff --git a/services/fxaccounts/tests/xpcshell/test_profile_client.js b/services/fxaccounts/tests/xpcshell/test_profile_client.js new file mode 100644 index 0000000000..22fcc293f8 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js @@ -0,0 +1,422 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + ERRNO_NETWORK, + ERRNO_PARSE, + ERRNO_UNKNOWN_ERROR, + ERROR_CODE_METHOD_NOT_ALLOWED, + ERROR_MSG_METHOD_NOT_ALLOWED, + ERROR_NETWORK, + ERROR_PARSE, + ERROR_UNKNOWN, +} = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); +const { FxAccountsProfileClient, FxAccountsProfileClientError } = + ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsProfileClient.sys.mjs" + ); + +const STATUS_SUCCESS = 200; + +/** + * Mock request responder + * @param {String} response + * Mocked raw response from the server + * @returns {Function} + */ +let mockResponse = function (response) { + let Request = function (requestUri) { + // Store the request uri so tests can inspect it + Request._requestUri = requestUri; + Request.ifNoneMatchSet = false; + return { + setHeader(header, value) { + if (header == "If-None-Match" && value == "bogusETag") { + Request.ifNoneMatchSet = true; + } + }, + async dispatch(method, payload) { + this.response = response; + return this.response; + }, + }; + }; + + return Request; +}; + +// A simple mock FxA that hands out tokens without checking them and doesn't +// expect tokens to be revoked. We have specific token tests further down that +// has more checks here. +let mockFxaInternal = { + getOAuthToken(options) { + Assert.equal(options.scope, "profile"); + return "token"; + }, +}; + +const PROFILE_OPTIONS = { + serverURL: "http://127.0.0.1:1111/v1", + fxai: mockFxaInternal, +}; + +/** + * Mock request error responder + * @param {Error} error + * Error object + * @returns {Function} + */ +let mockResponseError = function (error) { + return function () { + return { + setHeader() {}, + async dispatch(method, payload) { + throw error; + }, + }; + }; +}; + +add_test(function successfulResponse() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + status: STATUS_SUCCESS, + headers: { etag: "bogusETag" }, + body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}', + }; + + client._Request = new mockResponse(response); + client.fetchProfile().then(function (result) { + Assert.equal( + client._Request._requestUri, + "http://127.0.0.1:1111/v1/profile" + ); + Assert.equal(result.body.email, "someone@restmail.net"); + Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a"); + Assert.equal(result.etag, "bogusETag"); + run_next_test(); + }); +}); + +add_test(function setsIfNoneMatchETagHeader() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + status: STATUS_SUCCESS, + headers: {}, + body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}', + }; + + let req = new mockResponse(response); + client._Request = req; + client.fetchProfile("bogusETag").then(function (result) { + Assert.equal( + client._Request._requestUri, + "http://127.0.0.1:1111/v1/profile" + ); + Assert.equal(result.body.email, "someone@restmail.net"); + Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a"); + Assert.ok(req.ifNoneMatchSet); + run_next_test(); + }); +}); + +add_test(function successful304Response() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + headers: { etag: "bogusETag" }, + status: 304, + }; + + client._Request = new mockResponse(response); + client.fetchProfile().then(function (result) { + Assert.equal(result, null); + run_next_test(); + }); +}); + +add_test(function parseErrorResponse() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + status: STATUS_SUCCESS, + body: "unexpected", + }; + + client._Request = new mockResponse(response); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, STATUS_SUCCESS); + Assert.equal(e.errno, ERRNO_PARSE); + Assert.equal(e.error, ERROR_PARSE); + Assert.equal(e.message, "unexpected"); + run_next_test(); + }); +}); + +add_test(function serverErrorResponse() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + status: 500, + body: '{ "code": 500, "errno": 100, "error": "Bad Request", "message": "Something went wrong", "reason": "Because the internet" }', + }; + + client._Request = new mockResponse(response); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, 500); + Assert.equal(e.errno, 100); + Assert.equal(e.error, "Bad Request"); + Assert.equal(e.message, "Something went wrong"); + run_next_test(); + }); +}); + +// Test that we get a token, then if we get a 401 we revoke it, get a new one +// and retry. +add_test(function server401ResponseThenSuccess() { + // The last token we handed out. + let lastToken = -1; + // The number of times our removeCachedOAuthToken function was called. + let numTokensRemoved = 0; + + let mockFxaWithRemove = { + getOAuthToken(options) { + Assert.equal(options.scope, "profile"); + return "" + ++lastToken; // tokens are strings. + }, + removeCachedOAuthToken(options) { + // This test never has more than 1 token alive at once, so the token + // being revoked must always be the last token we handed out. + Assert.equal(parseInt(options.token), lastToken); + ++numTokensRemoved; + }, + }; + let profileOptions = { + serverURL: "http://127.0.0.1:1111/v1", + fxai: mockFxaWithRemove, + }; + let client = new FxAccountsProfileClient(profileOptions); + + // 2 responses - first one implying the token has expired, second works. + let responses = [ + { + status: 401, + body: '{ "code": 401, "errno": 100, "error": "Token expired", "message": "That token is too old", "reason": "Because security" }', + }, + { + success: true, + status: STATUS_SUCCESS, + headers: {}, + body: '{"avatar":"http://example.com/image.jpg","id":"0d5c1a89b8c54580b8e3e8adadae864a"}', + }, + ]; + + let numRequests = 0; + let numAuthHeaders = 0; + // Like mockResponse but we want access to headers etc. + client._Request = function (requestUri) { + return { + setHeader(name, value) { + if (name == "Authorization") { + numAuthHeaders++; + Assert.equal(value, "Bearer " + lastToken); + } + }, + async dispatch(method, payload) { + this.response = responses[numRequests]; + ++numRequests; + return this.response; + }, + }; + }; + + client.fetchProfile().then(result => { + Assert.equal(result.body.avatar, "http://example.com/image.jpg"); + Assert.equal(result.body.id, "0d5c1a89b8c54580b8e3e8adadae864a"); + // should have been exactly 2 requests and exactly 2 auth headers. + Assert.equal(numRequests, 2); + Assert.equal(numAuthHeaders, 2); + // and we should have seen one token revoked. + Assert.equal(numTokensRemoved, 1); + + run_next_test(); + }); +}); + +// Test that we get a token, then if we get a 401 we revoke it, get a new one +// and retry - but we *still* get a 401 on the retry, so the caller sees that. +add_test(function server401ResponsePersists() { + // The last token we handed out. + let lastToken = -1; + // The number of times our removeCachedOAuthToken function was called. + let numTokensRemoved = 0; + + let mockFxaWithRemove = { + getOAuthToken(options) { + Assert.equal(options.scope, "profile"); + return "" + ++lastToken; // tokens are strings. + }, + removeCachedOAuthToken(options) { + // This test never has more than 1 token alive at once, so the token + // being revoked must always be the last token we handed out. + Assert.equal(parseInt(options.token), lastToken); + ++numTokensRemoved; + }, + }; + let profileOptions = { + serverURL: "http://127.0.0.1:1111/v1", + fxai: mockFxaWithRemove, + }; + let client = new FxAccountsProfileClient(profileOptions); + + let response = { + status: 401, + body: '{ "code": 401, "errno": 100, "error": "It\'s not your token, it\'s you!", "message": "I don\'t like you", "reason": "Because security" }', + }; + + let numRequests = 0; + let numAuthHeaders = 0; + client._Request = function (requestUri) { + return { + setHeader(name, value) { + if (name == "Authorization") { + numAuthHeaders++; + Assert.equal(value, "Bearer " + lastToken); + } + }, + async dispatch(method, payload) { + this.response = response; + ++numRequests; + return this.response; + }, + }; + }; + + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, 401); + Assert.equal(e.errno, 100); + Assert.equal(e.error, "It's not your token, it's you!"); + // should have been exactly 2 requests and exactly 2 auth headers. + Assert.equal(numRequests, 2); + Assert.equal(numAuthHeaders, 2); + // and we should have seen both tokens revoked. + Assert.equal(numTokensRemoved, 2); + run_next_test(); + }); +}); + +add_test(function networkErrorResponse() { + let client = new FxAccountsProfileClient({ + serverURL: "http://domain.dummy", + fxai: mockFxaInternal, + }); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, null); + Assert.equal(e.errno, ERRNO_NETWORK); + Assert.equal(e.error, ERROR_NETWORK); + run_next_test(); + }); +}); + +add_test(function unsupportedMethod() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + + return client._createRequest("/profile", "PUT").catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, ERROR_CODE_METHOD_NOT_ALLOWED); + Assert.equal(e.errno, ERRNO_NETWORK); + Assert.equal(e.error, ERROR_NETWORK); + Assert.equal(e.message, ERROR_MSG_METHOD_NOT_ALLOWED); + run_next_test(); + }); +}); + +add_test(function onCompleteRequestError() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + client._Request = new mockResponseError(new Error("onComplete error")); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, null); + Assert.equal(e.errno, ERRNO_NETWORK); + Assert.equal(e.error, ERROR_NETWORK); + Assert.equal(e.message, "Error: onComplete error"); + run_next_test(); + }); +}); + +add_test(function constructorTests() { + validationHelper( + undefined, + "Error: Missing 'serverURL' configuration option" + ); + + validationHelper({}, "Error: Missing 'serverURL' configuration option"); + + validationHelper({ serverURL: "badUrl" }, "Error: Invalid 'serverURL'"); + + run_next_test(); +}); + +add_test(function errorTests() { + let error1 = new FxAccountsProfileClientError(); + Assert.equal(error1.name, "FxAccountsProfileClientError"); + Assert.equal(error1.code, null); + Assert.equal(error1.errno, ERRNO_UNKNOWN_ERROR); + Assert.equal(error1.error, ERROR_UNKNOWN); + Assert.equal(error1.message, null); + + let error2 = new FxAccountsProfileClientError({ + code: STATUS_SUCCESS, + errno: 1, + error: "Error", + message: "Something", + }); + let fields2 = error2._toStringFields(); + let statusCode = 1; + + Assert.equal(error2.name, "FxAccountsProfileClientError"); + Assert.equal(error2.code, STATUS_SUCCESS); + Assert.equal(error2.errno, statusCode); + Assert.equal(error2.error, "Error"); + Assert.equal(error2.message, "Something"); + + Assert.equal(fields2.name, "FxAccountsProfileClientError"); + Assert.equal(fields2.code, STATUS_SUCCESS); + Assert.equal(fields2.errno, statusCode); + Assert.equal(fields2.error, "Error"); + Assert.equal(fields2.message, "Something"); + + Assert.ok(error2.toString().includes("Something")); + run_next_test(); +}); + +/** + * Quick way to test the "FxAccountsProfileClient" constructor. + * + * @param {Object} options + * FxAccountsProfileClient constructor options + * @param {String} expected + * Expected error message + * @returns {*} + */ +function validationHelper(options, expected) { + // add fxai to options - that missing isn't what we are testing here. + if (options) { + options.fxai = mockFxaInternal; + } + try { + new FxAccountsProfileClient(options); + } catch (e) { + return Assert.equal(e.toString(), expected); + } + throw new Error("Validation helper error"); +} diff --git a/services/fxaccounts/tests/xpcshell/test_push_service.js b/services/fxaccounts/tests/xpcshell/test_push_service.js new file mode 100644 index 0000000000..0441888847 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_push_service.js @@ -0,0 +1,522 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests for the FxA push service. + +/* eslint-disable mozilla/use-chromeutils-generateqi */ + +const { + FXA_PUSH_SCOPE_ACCOUNT_UPDATE, + ONLOGOUT_NOTIFICATION, + ON_ACCOUNT_DESTROYED_NOTIFICATION, + ON_DEVICE_CONNECTED_NOTIFICATION, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + ON_PASSWORD_CHANGED_NOTIFICATION, + ON_PASSWORD_RESET_NOTIFICATION, + ON_PROFILE_CHANGE_NOTIFICATION, + ON_PROFILE_UPDATED_NOTIFICATION, + ON_VERIFY_LOGIN_NOTIFICATION, + log, +} = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +const { FxAccountsPushService } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsPush.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PushService", + "@mozilla.org/push/Service;1", + "nsIPushService" +); + +initTestLogging("Trace"); +log.level = Log.Level.Trace; + +const MOCK_ENDPOINT = "http://mochi.test:8888"; + +// tests do not allow external connections, mock the PushService +let mockPushService = { + pushTopic: PushService.pushTopic, + subscriptionChangeTopic: PushService.subscriptionChangeTopic, + subscribe(scope, principal, cb) { + cb(Cr.NS_OK, { + endpoint: MOCK_ENDPOINT, + }); + }, + unsubscribe(scope, principal, cb) { + cb(Cr.NS_OK, true); + }, +}; + +let mockFxAccounts = { + checkVerificationStatus() {}, + updateDeviceRegistration() {}, +}; + +let mockLog = { + trace() {}, + debug() {}, + warn() {}, + error() {}, +}; + +add_task(async function initialize() { + let pushService = new FxAccountsPushService(); + equal(pushService.initialize(), false); +}); + +add_task(async function registerPushEndpointSuccess() { + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: mockFxAccounts, + }); + + let subscription = await pushService.registerPushEndpoint(); + equal(subscription.endpoint, MOCK_ENDPOINT); +}); + +add_task(async function registerPushEndpointFailure() { + let failPushService = Object.assign(mockPushService, { + subscribe(scope, principal, cb) { + cb(Cr.NS_ERROR_ABORT); + }, + }); + + let pushService = new FxAccountsPushService({ + pushService: failPushService, + fxai: mockFxAccounts, + }); + + let subscription = await pushService.registerPushEndpoint(); + equal(subscription, null); +}); + +add_task(async function unsubscribeSuccess() { + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: mockFxAccounts, + }); + + let result = await pushService.unsubscribe(); + equal(result, true); +}); + +add_task(async function unsubscribeFailure() { + let failPushService = Object.assign(mockPushService, { + unsubscribe(scope, principal, cb) { + cb(Cr.NS_ERROR_ABORT); + }, + }); + + let pushService = new FxAccountsPushService({ + pushService: failPushService, + fxai: mockFxAccounts, + }); + + let result = await pushService.unsubscribe(); + equal(result, null); +}); + +add_test(function observeLogout() { + let customLog = Object.assign(mockLog, { + trace(msg) { + if (msg === "FxAccountsPushService unsubscribe") { + // logout means we unsubscribe + run_next_test(); + } + }, + }); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + log: customLog, + }); + + pushService.observe(null, ONLOGOUT_NOTIFICATION); +}); + +add_test(function observePushTopicVerify() { + let emptyMsg = { + QueryInterface() { + return this; + }, + }; + let customAccounts = Object.assign(mockFxAccounts, { + checkVerificationStatus() { + // checking verification status on push messages without data + run_next_test(); + }, + }); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: customAccounts, + }); + + pushService.observe( + emptyMsg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); + +add_test(function observePushTopicDeviceConnected() { + let msg = { + data: { + json: () => ({ + command: ON_DEVICE_CONNECTED_NOTIFICATION, + data: { + deviceName: "My phone", + }, + }), + }, + QueryInterface() { + return this; + }, + }; + let obs = (subject, topic, data) => { + Services.obs.removeObserver(obs, topic); + run_next_test(); + }; + Services.obs.addObserver(obs, ON_DEVICE_CONNECTED_NOTIFICATION); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: mockFxAccounts, + }); + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); + +add_task(async function observePushTopicDeviceDisconnected_current_device() { + const deviceId = "bogusid"; + let msg = { + data: { + json: () => ({ + command: ON_DEVICE_DISCONNECTED_NOTIFICATION, + data: { + id: deviceId, + }, + }), + }, + QueryInterface() { + return this; + }, + }; + + let signoutCalled = false; + let { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ); + const fxAccountsMock = new FxAccounts({ + newAccountState() { + return { + async getUserAccountData() { + return { device: { id: deviceId } }; + }, + }; + }, + signOut() { + signoutCalled = true; + }, + })._internal; + + const deviceDisconnectedNotificationObserved = new Promise(resolve => { + Services.obs.addObserver(function obs(subject, topic, data) { + Services.obs.removeObserver(obs, topic); + equal(data, JSON.stringify({ isLocalDevice: true })); + resolve(); + }, ON_DEVICE_DISCONNECTED_NOTIFICATION); + }); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: fxAccountsMock, + }); + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); + + await deviceDisconnectedNotificationObserved; + ok(signoutCalled); +}); + +add_task(async function observePushTopicDeviceDisconnected_another_device() { + const deviceId = "bogusid"; + let msg = { + data: { + json: () => ({ + command: ON_DEVICE_DISCONNECTED_NOTIFICATION, + data: { + id: deviceId, + }, + }), + }, + QueryInterface() { + return this; + }, + }; + + let signoutCalled = false; + let { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ); + const fxAccountsMock = new FxAccounts({ + newAccountState() { + return { + async getUserAccountData() { + return { device: { id: "thelocaldevice" } }; + }, + }; + }, + signOut() { + signoutCalled = true; + }, + })._internal; + + const deviceDisconnectedNotificationObserved = new Promise(resolve => { + Services.obs.addObserver(function obs(subject, topic, data) { + Services.obs.removeObserver(obs, topic); + equal(data, JSON.stringify({ isLocalDevice: false })); + resolve(); + }, ON_DEVICE_DISCONNECTED_NOTIFICATION); + }); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: fxAccountsMock, + }); + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); + + await deviceDisconnectedNotificationObserved; + ok(!signoutCalled); +}); + +add_test(function observePushTopicAccountDestroyed() { + const uid = "bogusuid"; + let msg = { + data: { + json: () => ({ + command: ON_ACCOUNT_DESTROYED_NOTIFICATION, + data: { + uid, + }, + }), + }, + QueryInterface() { + return this; + }, + }; + let customAccounts = Object.assign(mockFxAccounts, { + _handleAccountDestroyed() { + // checking verification status on push messages without data + run_next_test(); + }, + }); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: customAccounts, + }); + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); + +add_test(function observePushTopicVerifyLogin() { + let url = "http://localhost/newLogin"; + let title = "bogustitle"; + let body = "bogusbody"; + let msg = { + data: { + json: () => ({ + command: ON_VERIFY_LOGIN_NOTIFICATION, + data: { + body, + title, + url, + }, + }), + }, + QueryInterface() { + return this; + }, + }; + let obs = (subject, topic, data) => { + Services.obs.removeObserver(obs, topic); + Assert.equal(data, JSON.stringify(msg.data.json().data)); + run_next_test(); + }; + Services.obs.addObserver(obs, ON_VERIFY_LOGIN_NOTIFICATION); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: mockFxAccounts, + }); + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); + +add_test(function observePushTopicProfileUpdated() { + let msg = { + data: { + json: () => ({ + command: ON_PROFILE_UPDATED_NOTIFICATION, + }), + }, + QueryInterface() { + return this; + }, + }; + let obs = (subject, topic, data) => { + Services.obs.removeObserver(obs, topic); + run_next_test(); + }; + Services.obs.addObserver(obs, ON_PROFILE_CHANGE_NOTIFICATION); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: mockFxAccounts, + }); + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); + +add_test(function observePushTopicPasswordChanged() { + let msg = { + data: { + json: () => ({ + command: ON_PASSWORD_CHANGED_NOTIFICATION, + }), + }, + QueryInterface() { + return this; + }, + }; + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + }); + + pushService._onPasswordChanged = function () { + run_next_test(); + }; + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); + +add_test(function observePushTopicPasswordReset() { + let msg = { + data: { + json: () => ({ + command: ON_PASSWORD_RESET_NOTIFICATION, + }), + }, + QueryInterface() { + return this; + }, + }; + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + }); + + pushService._onPasswordChanged = function () { + run_next_test(); + }; + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); + +add_task(async function commandReceived() { + let msg = { + data: { + json: () => ({ + command: "fxaccounts:command_received", + data: { + url: "https://api.accounts.firefox.com/auth/v1/account/device/commands?index=42&limit=1", + }, + }), + }, + QueryInterface() { + return this; + }, + }; + + let fxAccountsMock = {}; + const promiseConsumeRemoteMessagesCalled = new Promise(res => { + fxAccountsMock.commands = { + pollDeviceCommands() { + res(); + }, + }; + }); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: fxAccountsMock, + }); + + pushService.observe( + msg, + mockPushService.pushTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); + await promiseConsumeRemoteMessagesCalled; +}); + +add_test(function observeSubscriptionChangeTopic() { + let customAccounts = Object.assign(mockFxAccounts, { + updateDeviceRegistration() { + // subscription change means updating the device registration + run_next_test(); + }, + }); + + let pushService = new FxAccountsPushService({ + pushService: mockPushService, + fxai: customAccounts, + }); + + pushService.observe( + null, + mockPushService.subscriptionChangeTopic, + FXA_PUSH_SCOPE_ACCOUNT_UPDATE + ); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_storage_manager.js b/services/fxaccounts/tests/xpcshell/test_storage_manager.js new file mode 100644 index 0000000000..05c565d2f4 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_storage_manager.js @@ -0,0 +1,586 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests for the FxA storage manager. + +const { FxAccountsStorageManager } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsStorage.sys.mjs" +); +const { DATA_FORMAT_VERSION, log } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); + +initTestLogging("Trace"); +log.level = Log.Level.Trace; + +const DEVICE_REGISTRATION_VERSION = 42; + +// A couple of mocks we can use. +function MockedPlainStorage(accountData) { + let data = null; + if (accountData) { + data = { + version: DATA_FORMAT_VERSION, + accountData, + }; + } + this.data = data; + this.numReads = 0; +} +MockedPlainStorage.prototype = { + async get() { + this.numReads++; + Assert.equal(this.numReads, 1, "should only ever be 1 read of acct data"); + return this.data; + }, + + async set(data) { + this.data = data; + }, +}; + +function MockedSecureStorage(accountData) { + let data = null; + if (accountData) { + data = { + version: DATA_FORMAT_VERSION, + accountData, + }; + } + this.data = data; + this.numReads = 0; +} + +MockedSecureStorage.prototype = { + fetchCount: 0, + locked: false, + /* eslint-disable object-shorthand */ + // This constructor must be declared without + // object shorthand or we get an exception of + // "TypeError: this.STORAGE_LOCKED is not a constructor" + STORAGE_LOCKED: function () {}, + /* eslint-enable object-shorthand */ + async get(uid, email) { + this.fetchCount++; + if (this.locked) { + throw new this.STORAGE_LOCKED(); + } + this.numReads++; + Assert.equal( + this.numReads, + 1, + "should only ever be 1 read of unlocked data" + ); + return this.data; + }, + + async set(uid, contents) { + this.data = contents; + }, +}; + +function add_storage_task(testFunction) { + add_task(async function () { + print("Starting test with secure storage manager"); + await testFunction(new FxAccountsStorageManager()); + }); + add_task(async function () { + print("Starting test with simple storage manager"); + await testFunction(new FxAccountsStorageManager({ useSecure: false })); + }); +} + +// initialized without account data and there's nothing to read. Not logged in. +add_storage_task(async function checkInitializedEmpty(sm) { + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + await sm.initialize(); + Assert.strictEqual(await sm.getAccountData(), null); + await Assert.rejects( + sm.updateAccountData({ scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys } }), + /No user is logged in/ + ); +}); + +// Initialized with account data (ie, simulating a new user being logged in). +// Should reflect the initial data and be written to storage. +add_storage_task(async function checkNewUser(sm) { + let initialAccountData = { + uid: "uid", + email: "someone@somewhere.com", + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + device: { + id: "device id", + }, + }; + sm.plainStorage = new MockedPlainStorage(); + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + await sm.initialize(initialAccountData); + let accountData = await sm.getAccountData(); + Assert.equal(accountData.uid, initialAccountData.uid); + Assert.equal(accountData.email, initialAccountData.email); + Assert.deepEqual(accountData.scopedKeys, initialAccountData.scopedKeys); + Assert.deepEqual(accountData.device, initialAccountData.device); + + // and it should have been written to storage. + Assert.equal(sm.plainStorage.data.accountData.uid, initialAccountData.uid); + Assert.equal( + sm.plainStorage.data.accountData.email, + initialAccountData.email + ); + Assert.deepEqual( + sm.plainStorage.data.accountData.device, + initialAccountData.device + ); + // check secure + if (sm.secureStorage) { + Assert.deepEqual( + sm.secureStorage.data.accountData.scopedKeys, + initialAccountData.scopedKeys + ); + } else { + Assert.deepEqual( + sm.plainStorage.data.accountData.scopedKeys, + initialAccountData.scopedKeys + ); + } +}); + +// Initialized without account data but storage has it available. +add_storage_task(async function checkEverythingRead(sm) { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + device: { + id: "wibble", + registrationVersion: null, + }, + }); + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + await sm.initialize(); + let accountData = await sm.getAccountData(); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.email, "someone@somewhere.com"); + Assert.deepEqual(accountData.device, { + id: "wibble", + registrationVersion: null, + }); + // Update the data - we should be able to fetch it back and it should appear + // in our storage. + await sm.updateAccountData({ + verified: true, + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + device: { + id: "wibble", + registrationVersion: DEVICE_REGISTRATION_VERSION, + }, + }); + accountData = await sm.getAccountData(); + Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); + Assert.deepEqual(accountData.device, { + id: "wibble", + registrationVersion: DEVICE_REGISTRATION_VERSION, + }); + // Check the new value was written to storage. + await sm._promiseStorageComplete; // storage is written in the background. + Assert.equal(sm.plainStorage.data.accountData.verified, true); + Assert.deepEqual(sm.plainStorage.data.accountData.device, { + id: "wibble", + registrationVersion: DEVICE_REGISTRATION_VERSION, + }); + // derive keys are secure + if (sm.secureStorage) { + Assert.deepEqual( + sm.secureStorage.data.accountData.scopedKeys, + MOCK_ACCOUNT_KEYS.scopedKeys + ); + } else { + Assert.deepEqual( + sm.plainStorage.data.accountData.scopedKeys, + MOCK_ACCOUNT_KEYS.scopedKeys + ); + } +}); + +add_storage_task(async function checkInvalidUpdates(sm) { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + if (sm.secureStorage) { + sm.secureStorage = new MockedSecureStorage(null); + } + await sm.initialize(); + + await Assert.rejects( + sm.updateAccountData({ uid: "another" }), + /Can't change uid/ + ); +}); + +add_storage_task(async function checkNullUpdatesRemovedUnlocked(sm) { + if (sm.secureStorage) { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + unwrapBKey: "unwrapBKey", + }); + } else { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + unwrapBKey: "unwrapBKey", + }); + } + await sm.initialize(); + + await sm.updateAccountData({ unwrapBKey: null }); + let accountData = await sm.getAccountData(); + Assert.ok(!accountData.unwrapBKey); + Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); +}); + +add_storage_task(async function checkNullRemovesUnlistedFields(sm) { + // kA and kB are not listed in FXA_PWDMGR_*_FIELDS, but we still want to + // be able to delete them (migration case). + if (sm.secureStorage) { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ kA: "kA", kb: "kB" }); + } else { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + kA: "kA", + kb: "kB", + }); + } + await sm.initialize(); + + await sm.updateAccountData({ kA: null, kB: null }); + let accountData = await sm.getAccountData(); + Assert.ok(!accountData.kA); + Assert.ok(!accountData.kB); +}); + +add_storage_task(async function checkDelete(sm) { + if (sm.secureStorage) { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + }); + } else { + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + }); + } + await sm.initialize(); + + await sm.deleteAccountData(); + // Storage should have been reset to null. + Assert.equal(sm.plainStorage.data, null); + if (sm.secureStorage) { + Assert.equal(sm.secureStorage.data, null); + } + // And everything should reflect no user. + Assert.equal(await sm.getAccountData(), null); +}); + +// Some tests only for the secure storage manager. +add_task(async function checkNullUpdatesRemovedLocked() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + unwrapBKey: "unwrapBKey is another secure value", + }); + sm.secureStorage.locked = true; + await sm.initialize(); + + await sm.updateAccountData({ scopedKeys: null }); + let accountData = await sm.getAccountData(); + // No scopedKeys because it was removed. + Assert.ok(!accountData.scopedKeys); + // No unwrapBKey because we are locked + Assert.ok(!accountData.unwrapBKey); + + // now unlock - should still be no scopedKeys but unwrapBKey should appear. + sm.secureStorage.locked = false; + accountData = await sm.getAccountData(); + Assert.ok(!accountData.scopedKeys); + Assert.equal(accountData.unwrapBKey, "unwrapBKey is another secure value"); + // And secure storage should have been written with our previously-cached + // data. + Assert.strictEqual(sm.secureStorage.data.accountData.scopedKeys, undefined); + Assert.strictEqual( + sm.secureStorage.data.accountData.unwrapBKey, + "unwrapBKey is another secure value" + ); +}); + +add_task(async function checkEverythingReadSecure() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + }); + await sm.initialize(); + + let accountData = await sm.getAccountData(); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.email, "someone@somewhere.com"); + Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); +}); + +add_task(async function checkExplicitGet() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + }); + await sm.initialize(); + + let accountData = await sm.getAccountData(["uid", "scopedKeys"]); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); + // We didn't ask for email so shouldn't have got it. + Assert.strictEqual(accountData.email, undefined); +}); + +add_task(async function checkExplicitGetNoSecureRead() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + }); + await sm.initialize(); + + Assert.equal(sm.secureStorage.fetchCount, 0); + // request 2 fields in secure storage - it should have caused a single fetch. + let accountData = await sm.getAccountData(["email", "uid"]); + Assert.ok(accountData, "read account data"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.email, "someone@somewhere.com"); + Assert.strictEqual(accountData.scopedKeys, undefined); + Assert.equal(sm.secureStorage.fetchCount, 1); +}); + +add_task(async function checkLockedUpdates() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + unwrapBKey: "unwrapBKey", + }); + sm.secureStorage.locked = true; + await sm.initialize(); + + let accountData = await sm.getAccountData(); + // requesting scopedKeys will fail as storage is locked. + Assert.ok(!accountData.scopedKeys); + // While locked we can still update it and see the updated value. + sm.updateAccountData({ unwrapBKey: "new-unwrapBKey" }); + accountData = await sm.getAccountData(); + Assert.equal(accountData.unwrapBKey, "new-unwrapBKey"); + // unlock. + sm.secureStorage.locked = false; + accountData = await sm.getAccountData(); + // should reflect the value we updated and the one we didn't. + Assert.equal(accountData.unwrapBKey, "new-unwrapBKey"); + Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); + // And storage should also reflect it. + Assert.deepEqual( + sm.secureStorage.data.accountData.scopedKeys, + MOCK_ACCOUNT_KEYS.scopedKeys + ); + Assert.strictEqual( + sm.secureStorage.data.accountData.unwrapBKey, + "new-unwrapBKey" + ); +}); + +// Some tests for the "storage queue" functionality. + +// A helper for our queued tests. It creates a StorageManager and then queues +// an unresolved promise. The tests then do additional setup and checks, then +// resolves or rejects the blocked promise. +async function setupStorageManagerForQueueTest() { + let sm = new FxAccountsStorageManager(); + sm.plainStorage = new MockedPlainStorage({ + uid: "uid", + email: "someone@somewhere.com", + }); + sm.secureStorage = new MockedSecureStorage({ + scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, + }); + sm.secureStorage.locked = true; + await sm.initialize(); + + let resolveBlocked, rejectBlocked; + let blockedPromise = new Promise((resolve, reject) => { + resolveBlocked = resolve; + rejectBlocked = reject; + }); + + sm._queueStorageOperation(() => blockedPromise); + return { sm, blockedPromise, resolveBlocked, rejectBlocked }; +} + +// First the general functionality. +add_task(async function checkQueueSemantics() { + let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); + + // We've one unresolved promise in the queue - add another promise. + let resolveSubsequent; + let subsequentPromise = new Promise(resolve => { + resolveSubsequent = resolve; + }); + let subsequentCalled = false; + + sm._queueStorageOperation(() => { + subsequentCalled = true; + resolveSubsequent(); + return subsequentPromise; + }); + + // Our "subsequent" function should not have been called yet. + Assert.ok(!subsequentCalled); + + // Release our blocked promise. + resolveBlocked(); + + // Our subsequent promise should end up resolved. + await subsequentPromise; + Assert.ok(subsequentCalled); + await sm.finalize(); +}); + +// Check that a queued promise being rejected works correctly. +add_task(async function checkQueueSemanticsOnError() { + let { sm, blockedPromise, rejectBlocked } = + await setupStorageManagerForQueueTest(); + + let resolveSubsequent; + let subsequentPromise = new Promise(resolve => { + resolveSubsequent = resolve; + }); + let subsequentCalled = false; + + sm._queueStorageOperation(() => { + subsequentCalled = true; + resolveSubsequent(); + return subsequentPromise; + }); + + // Our "subsequent" function should not have been called yet. + Assert.ok(!subsequentCalled); + + // Reject our blocked promise - the subsequent operations should still work + // correctly. + rejectBlocked("oh no"); + + // Our subsequent promise should end up resolved. + await subsequentPromise; + Assert.ok(subsequentCalled); + + // But the first promise should reflect the rejection. + try { + await blockedPromise; + Assert.ok(false, "expected this promise to reject"); + } catch (ex) { + Assert.equal(ex, "oh no"); + } + await sm.finalize(); +}); + +// And some tests for the specific operations that are queued. +add_task(async function checkQueuedReadAndUpdate() { + let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); + // Mock the underlying operations + // _doReadAndUpdateSecure is queued by _maybeReadAndUpdateSecure + let _doReadCalled = false; + sm._doReadAndUpdateSecure = () => { + _doReadCalled = true; + return Promise.resolve(); + }; + + let resultPromise = sm._maybeReadAndUpdateSecure(); + Assert.ok(!_doReadCalled); + + resolveBlocked(); + await resultPromise; + Assert.ok(_doReadCalled); + await sm.finalize(); +}); + +add_task(async function checkQueuedWrite() { + let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); + // Mock the underlying operations + let __writeCalled = false; + sm.__write = () => { + __writeCalled = true; + return Promise.resolve(); + }; + + let writePromise = sm._write(); + Assert.ok(!__writeCalled); + + resolveBlocked(); + await writePromise; + Assert.ok(__writeCalled); + await sm.finalize(); +}); + +add_task(async function checkQueuedDelete() { + let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); + // Mock the underlying operations + let _deleteCalled = false; + sm._deleteAccountData = () => { + _deleteCalled = true; + return Promise.resolve(); + }; + + let resultPromise = sm.deleteAccountData(); + Assert.ok(!_deleteCalled); + + resolveBlocked(); + await resultPromise; + Assert.ok(_deleteCalled); + await sm.finalize(); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_telemetry.js b/services/fxaccounts/tests/xpcshell/test_telemetry.js new file mode 100644 index 0000000000..3b9d318404 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_telemetry.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { getFxAccountsSingleton } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const fxAccounts = getFxAccountsSingleton(); + +_("Misc tests for FxAccounts.telemetry"); + +const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff"; +const MOCK_DEVICE_ID = "ffeeddccbbaa99887766554433221100"; + +add_task(function test_sanitized_uid() { + Services.prefs.deleteBranch( + "identity.fxaccounts.account.telemetry.sanitized_uid" + ); + + // Returns `null` by default. + Assert.equal(fxAccounts.telemetry.getSanitizedUID(), null); + + // Returns provided value if set. + fxAccounts.telemetry._setHashedUID(MOCK_HASHED_UID); + Assert.equal(fxAccounts.telemetry.getSanitizedUID(), MOCK_HASHED_UID); + + // Reverts to unset for falsey values. + fxAccounts.telemetry._setHashedUID(""); + Assert.equal(fxAccounts.telemetry.getSanitizedUID(), null); +}); + +add_task(function test_sanitize_device_id() { + Services.prefs.deleteBranch( + "identity.fxaccounts.account.telemetry.sanitized_uid" + ); + + // Returns `null` by default. + Assert.equal(fxAccounts.telemetry.sanitizeDeviceId(MOCK_DEVICE_ID), null); + + // Hashes with the sanitized UID if set. + // (test value here is SHA256(MOCK_DEVICE_ID + MOCK_HASHED_UID)) + fxAccounts.telemetry._setHashedUID(MOCK_HASHED_UID); + Assert.equal( + fxAccounts.telemetry.sanitizeDeviceId(MOCK_DEVICE_ID), + "dd7c845006df9baa1c6d756926519c8ce12f91230e11b6057bf8ec65f9b55c1a" + ); + + // Reverts to unset for falsey values. + fxAccounts.telemetry._setHashedUID(""); + Assert.equal(fxAccounts.telemetry.sanitizeDeviceId(MOCK_DEVICE_ID), null); +}); diff --git a/services/fxaccounts/tests/xpcshell/test_web_channel.js b/services/fxaccounts/tests/xpcshell/test_web_channel.js new file mode 100644 index 0000000000..48f043d0b9 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js @@ -0,0 +1,1380 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ON_PROFILE_CHANGE_NOTIFICATION, WEBCHANNEL_ID, log } = + ChromeUtils.importESModule("resource://gre/modules/FxAccountsCommon.sys.mjs"); +const { CryptoUtils } = ChromeUtils.importESModule( + "resource://services-crypto/utils.sys.mjs" +); +const { FxAccountsWebChannel, FxAccountsWebChannelHelpers } = + ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsWebChannel.sys.mjs" + ); + +const URL_STRING = "https://example.com"; + +const mockSendingContext = { + browsingContext: { top: { embedderElement: {} } }, + principal: {}, + eventTarget: {}, +}; + +add_test(function () { + validationHelper(undefined, "Error: Missing configuration options"); + + validationHelper( + { + channel_id: WEBCHANNEL_ID, + }, + "Error: Missing 'content_uri' option" + ); + + validationHelper( + { + content_uri: "bad uri", + channel_id: WEBCHANNEL_ID, + }, + /NS_ERROR_MALFORMED_URI/ + ); + + validationHelper( + { + content_uri: URL_STRING, + }, + "Error: Missing 'channel_id' option" + ); + + run_next_test(); +}); + +add_task(async function test_rejection_reporting() { + Services.prefs.setBoolPref( + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", + false + ); + + let mockMessage = { + command: "fxaccounts:login", + messageId: "1234", + data: { email: "testuser@testuser.com" }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + login(accountData) { + equal( + accountData.email, + "testuser@testuser.com", + "Should forward incoming message data to the helper" + ); + return Promise.reject(new Error("oops")); + }, + }, + }); + + let promiseSend = new Promise(resolve => { + channel._channel.send = (message, context) => { + resolve({ message, context }); + }; + }); + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); + + let { message, context } = await promiseSend; + + equal(context, mockSendingContext, "Should forward the original context"); + equal( + message.command, + "fxaccounts:login", + "Should include the incoming command" + ); + equal(message.messageId, "1234", "Should include the message ID"); + equal( + message.data.error.message, + "Error: oops", + "Should convert the error message to a string" + ); + notStrictEqual( + message.data.error.stack, + null, + "Should include the stack for JS error rejections" + ); +}); + +add_test(function test_exception_reporting() { + let mockMessage = { + command: "fxaccounts:sync_preferences", + messageId: "5678", + data: { entryPoint: "fxa:verification_complete" }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + openSyncPreferences(browser, entryPoint) { + equal( + entryPoint, + "fxa:verification_complete", + "Should forward incoming message data to the helper" + ); + throw new TypeError("splines not reticulated"); + }, + }, + }); + + channel._channel.send = (message, context) => { + equal(context, mockSendingContext, "Should forward the original context"); + equal( + message.command, + "fxaccounts:sync_preferences", + "Should include the incoming command" + ); + equal(message.messageId, "5678", "Should include the message ID"); + equal( + message.data.error.message, + "TypeError: splines not reticulated", + "Should convert the exception to a string" + ); + notStrictEqual( + message.data.error.stack, + null, + "Should include the stack for JS exceptions" + ); + + run_next_test(); + }; + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_error_message_remove_profile_path() { + const errors = { + windows: { + err: new Error( + "Win error 183 during operation rename on file C:\\Users\\Some Computer\\AppData\\Roaming\\" + + "Mozilla\\Firefox\\Profiles\\dbzjmzxa.default\\signedInUser.json (Cannot create a file)" + ), + expected: + "Error: Win error 183 during operation rename on file C:[REDACTED]signedInUser.json (Cannot create a file)", + }, + unix: { + err: new Error( + "Unix error 28 during operation write on file /Users/someuser/Library/Application Support/" + + "Firefox/Profiles/dbzjmzxa.default-release-7/signedInUser.json (No space left on device)" + ), + expected: + "Error: Unix error 28 during operation write on file [REDACTED]signedInUser.json (No space left on device)", + }, + netpath: { + err: new Error( + "Win error 32 during operation rename on file \\\\SVC.LOC\\HOMEDIRS$\\USERNAME\\Mozilla\\" + + "Firefox\\Profiles\\dbzjmzxa.default-release-7\\signedInUser.json (No space left on device)" + ), + expected: + "Error: Win error 32 during operation rename on file [REDACTED]signedInUser.json (No space left on device)", + }, + mount: { + err: new Error( + "Win error 649 during operation rename on file C:\\SnapVolumes\\MountPoints\\" + + "{9e399ec5-0000-0000-0000-100000000000}\\SVROOT\\Users\\username\\AppData\\Roaming\\Mozilla\\Firefox\\" + + "Profiles\\dbzjmzxa.default-release\\signedInUser.json (The create operation failed)" + ), + expected: + "Error: Win error 649 during operation rename on file C:[REDACTED]signedInUser.json " + + "(The create operation failed)", + }, + }; + const mockMessage = { + command: "fxaccounts:sync_preferences", + messageId: "1234", + }; + const channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + }); + + let testNum = 0; + const toTest = Object.keys(errors).length; + for (const key in errors) { + let error = errors[key]; + channel._channel.send = (message, context) => { + equal( + message.data.error.message, + error.expected, + "Should remove the profile path from the error message" + ); + testNum++; + if (testNum === toTest) { + run_next_test(); + } + }; + channel._sendError(error.err, mockMessage, mockSendingContext); + } +}); + +add_test(function test_profile_image_change_message() { + var mockMessage = { + command: "profile:change", + data: { uid: "foo" }, + }; + + makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { + Assert.equal(data, "foo"); + run_next_test(); + }); + + var channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + }); + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_login_message() { + let mockMessage = { + command: "fxaccounts:login", + data: { email: "testuser@testuser.com" }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + login(accountData) { + Assert.equal(accountData.email, "testuser@testuser.com"); + run_next_test(); + return Promise.resolve(); + }, + }, + }); + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_oauth_login() { + const mockData = { + code: "oauth code", + state: "state parameter", + declinedSyncEngines: ["tabs", "creditcards"], + offeredSyncEngines: ["tabs", "creditcards", "history"], + }; + const mockMessage = { + command: "fxaccounts:oauth_login", + data: mockData, + }; + const channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + oauthLogin(data) { + Assert.deepEqual(data, mockData); + run_next_test(); + return Promise.resolve(); + }, + }, + }); + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_logout_message() { + let mockMessage = { + command: "fxaccounts:logout", + data: { uid: "foo" }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + logout(uid) { + Assert.equal(uid, "foo"); + run_next_test(); + return Promise.resolve(); + }, + }, + }); + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_delete_message() { + let mockMessage = { + command: "fxaccounts:delete", + data: { uid: "foo" }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + logout(uid) { + Assert.equal(uid, "foo"); + run_next_test(); + return Promise.resolve(); + }, + }, + }); + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_can_link_account_message() { + let mockMessage = { + command: "fxaccounts:can_link_account", + data: { email: "testuser@testuser.com" }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + shouldAllowRelink(email) { + Assert.equal(email, "testuser@testuser.com"); + run_next_test(); + }, + }, + }); + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_sync_preferences_message() { + let mockMessage = { + command: "fxaccounts:sync_preferences", + data: { entryPoint: "fxa:verification_complete" }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + openSyncPreferences(browser, entryPoint) { + Assert.equal(entryPoint, "fxa:verification_complete"); + Assert.equal( + browser, + mockSendingContext.browsingContext.top.embedderElement + ); + run_next_test(); + }, + }, + }); + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_fxa_status_message() { + let mockMessage = { + command: "fxaccounts:fxa_status", + messageId: 123, + data: { + service: "sync", + context: "fx_desktop_v3", + }, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + helpers: { + async getFxaStatus(service, sendingContext, isPairing, context) { + Assert.equal(service, "sync"); + Assert.equal(sendingContext, mockSendingContext); + Assert.ok(!isPairing); + Assert.equal(context, "fx_desktop_v3"); + return { + signedInUser: { + email: "testuser@testuser.com", + sessionToken: "session-token", + uid: "uid", + verified: true, + }, + capabilities: { + engines: ["creditcards", "addresses"], + }, + }; + }, + }, + }); + + channel._channel = { + send(response, sendingContext) { + Assert.equal(response.command, "fxaccounts:fxa_status"); + Assert.equal(response.messageId, 123); + + let signedInUser = response.data.signedInUser; + Assert.ok(!!signedInUser); + Assert.equal(signedInUser.email, "testuser@testuser.com"); + Assert.equal(signedInUser.sessionToken, "session-token"); + Assert.equal(signedInUser.uid, "uid"); + Assert.equal(signedInUser.verified, true); + + deepEqual(response.data.capabilities.engines, [ + "creditcards", + "addresses", + ]); + + run_next_test(); + }, + }; + + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); +}); + +add_test(function test_unrecognized_message() { + let mockMessage = { + command: "fxaccounts:unrecognized", + data: {}, + }; + + let channel = new FxAccountsWebChannel({ + channel_id: WEBCHANNEL_ID, + content_uri: URL_STRING, + }); + + // no error is expected. + channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); + run_next_test(); +}); + +add_test(function test_helpers_should_allow_relink_same_email() { + let helpers = new FxAccountsWebChannelHelpers(); + + helpers.setPreviousAccountNameHashPref("testuser@testuser.com"); + Assert.ok(helpers.shouldAllowRelink("testuser@testuser.com")); + + run_next_test(); +}); + +add_test(function test_helpers_should_allow_relink_different_email() { + let helpers = new FxAccountsWebChannelHelpers(); + + helpers.setPreviousAccountNameHashPref("testuser@testuser.com"); + + helpers._promptForRelink = acctName => { + return acctName === "allowed_to_relink@testuser.com"; + }; + + Assert.ok(helpers.shouldAllowRelink("allowed_to_relink@testuser.com")); + Assert.ok(!helpers.shouldAllowRelink("not_allowed_to_relink@testuser.com")); + + run_next_test(); +}); + +add_task(async function test_helpers_login_without_customize_sync() { + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + setSignedInUser(accountData) { + return new Promise(resolve => { + // ensure fxAccounts is informed of the new user being signed in. + Assert.equal(accountData.email, "testuser@testuser.com"); + + // verifiedCanLinkAccount should be stripped in the data. + Assert.equal(false, "verifiedCanLinkAccount" in accountData); + + resolve(); + }); + }, + }, + telemetry: { + recordConnection: sinon.spy(), + }, + }, + weaveXPCOM: { + whenLoaded() {}, + Weave: { + Service: { + configure() {}, + }, + }, + }, + }); + + // ensure the previous account pref is overwritten. + helpers.setPreviousAccountNameHashPref("lastuser@testuser.com"); + + await helpers.login({ + email: "testuser@testuser.com", + verifiedCanLinkAccount: true, + customizeSync: false, + }); + Assert.ok( + helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel") + ); +}); + +add_task(async function test_helpers_login_set_previous_account_name_hash() { + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + setSignedInUser(accountData) { + return new Promise(resolve => { + // previously signed in user preference is updated. + Assert.equal( + helpers.getPreviousAccountNameHashPref(), + CryptoUtils.sha256Base64("newuser@testuser.com") + ); + resolve(); + }); + }, + }, + telemetry: { + recordConnection() {}, + }, + }, + weaveXPCOM: { + whenLoaded() {}, + Weave: { + Service: { + configure() {}, + }, + }, + }, + }); + + // ensure the previous account pref is overwritten. + helpers.setPreviousAccountNameHashPref("lastuser@testuser.com"); + + await helpers.login({ + email: "newuser@testuser.com", + verifiedCanLinkAccount: true, + customizeSync: false, + verified: true, + }); +}); + +add_task( + async function test_helpers_login_dont_set_previous_account_name_hash_for_unverified_emails() { + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + setSignedInUser(accountData) { + return new Promise(resolve => { + // previously signed in user preference should not be updated. + Assert.equal( + helpers.getPreviousAccountNameHashPref(), + CryptoUtils.sha256Base64("lastuser@testuser.com") + ); + resolve(); + }); + }, + }, + telemetry: { + recordConnection() {}, + }, + }, + weaveXPCOM: { + whenLoaded() {}, + Weave: { + Service: { + configure() {}, + }, + }, + }, + }); + + // ensure the previous account pref is overwritten. + helpers.setPreviousAccountNameHashPref("lastuser@testuser.com"); + + await helpers.login({ + email: "newuser@testuser.com", + verifiedCanLinkAccount: true, + customizeSync: false, + }); + } +); + +add_task(async function test_helpers_login_with_customize_sync() { + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + setSignedInUser(accountData) { + return new Promise(resolve => { + // ensure fxAccounts is informed of the new user being signed in. + Assert.equal(accountData.email, "testuser@testuser.com"); + + // customizeSync should be stripped in the data. + Assert.equal(false, "customizeSync" in accountData); + + resolve(); + }); + }, + }, + telemetry: { + recordConnection: sinon.spy(), + }, + }, + weaveXPCOM: { + whenLoaded() {}, + Weave: { + Service: { + configure() {}, + }, + }, + }, + }); + + await helpers.login({ + email: "testuser@testuser.com", + verifiedCanLinkAccount: true, + customizeSync: true, + }); + Assert.ok( + helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel") + ); +}); + +add_task( + async function test_helpers_login_with_customize_sync_and_declined_engines() { + let configured = false; + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + setSignedInUser(accountData) { + return new Promise(resolve => { + // ensure fxAccounts is informed of the new user being signed in. + Assert.equal(accountData.email, "testuser@testuser.com"); + + // customizeSync should be stripped in the data. + Assert.equal(false, "customizeSync" in accountData); + Assert.equal(false, "services" in accountData); + resolve(); + }); + }, + }, + telemetry: { + recordConnection: sinon.spy(), + }, + }, + weaveXPCOM: { + whenLoaded() {}, + Weave: { + Service: { + configure() { + configured = true; + }, + }, + }, + }, + }); + + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.addons"), + true + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.bookmarks"), + true + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.history"), + true + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.passwords"), + true + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.prefs"), + true + ); + Assert.equal(Services.prefs.getBoolPref("services.sync.engine.tabs"), true); + await helpers.login({ + email: "testuser@testuser.com", + verifiedCanLinkAccount: true, + customizeSync: true, + services: { + sync: { + offeredEngines: [ + "addons", + "bookmarks", + "history", + "passwords", + "prefs", + ], + declinedEngines: ["addons", "prefs"], + }, + }, + }); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.addons"), + false + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.bookmarks"), + true + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.history"), + true + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.passwords"), + true + ); + Assert.equal( + Services.prefs.getBoolPref("services.sync.engine.prefs"), + false + ); + Assert.equal(Services.prefs.getBoolPref("services.sync.engine.tabs"), true); + Assert.ok(configured, "sync was configured"); + Assert.ok( + helpers._fxAccounts.telemetry.recordConnection.calledWith( + ["sync"], + "webchannel" + ) + ); + } +); + +add_task(async function test_helpers_login_with_offered_sync_engines() { + let helpers; + let configured = false; + const setSignedInUserCalled = new Promise(resolve => { + helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + async setSignedInUser(accountData) { + resolve(accountData); + }, + }, + telemetry: { + recordConnection() {}, + }, + }, + weaveXPCOM: { + whenLoaded() {}, + Weave: { + Service: { + configure() { + configured = true; + }, + }, + }, + }, + }); + }); + + Services.prefs.setBoolPref("services.sync.engine.creditcards", false); + Services.prefs.setBoolPref("services.sync.engine.addresses", false); + + await helpers.login({ + email: "testuser@testuser.com", + verifiedCanLinkAccount: true, + customizeSync: true, + services: { + sync: { + declinedEngines: ["addresses"], + offeredEngines: ["creditcards", "addresses"], + }, + }, + }); + + const accountData = await setSignedInUserCalled; + + // ensure fxAccounts is informed of the new user being signed in. + equal(accountData.email, "testuser@testuser.com"); + + // services should be stripped in the data. + ok(!("services" in accountData)); + // credit cards was offered but not declined. + equal(Services.prefs.getBoolPref("services.sync.engine.creditcards"), true); + // addresses was offered and explicitely declined. + equal(Services.prefs.getBoolPref("services.sync.engine.addresses"), false); + ok(configured); +}); + +add_task(async function test_helpers_login_nothing_offered() { + let helpers; + let configured = false; + const setSignedInUserCalled = new Promise(resolve => { + helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + async setSignedInUser(accountData) { + resolve(accountData); + }, + }, + telemetry: { + recordConnection() {}, + }, + }, + weaveXPCOM: { + whenLoaded() {}, + Weave: { + Service: { + configure() { + configured = true; + }, + }, + }, + }, + }); + }); + + // doesn't really matter if it's *all* engines... + const allEngines = [ + "addons", + "addresses", + "bookmarks", + "creditcards", + "history", + "passwords", + "prefs", + ]; + for (let name of allEngines) { + Services.prefs.clearUserPref("services.sync.engine." + name); + } + + await helpers.login({ + email: "testuser@testuser.com", + verifiedCanLinkAccount: true, + services: { + sync: {}, + }, + }); + + const accountData = await setSignedInUserCalled; + // ensure fxAccounts is informed of the new user being signed in. + equal(accountData.email, "testuser@testuser.com"); + + for (let name of allEngines) { + Assert.ok(!Services.prefs.prefHasUserValue("services.sync.engine." + name)); + } + Assert.ok(configured); +}); + +add_test(function test_helpers_open_sync_preferences() { + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: {}, + }); + + let mockBrowser = { + loadURI(uri) { + Assert.equal( + uri.spec, + "about:preferences?entrypoint=fxa%3Averification_complete#sync" + ); + run_next_test(); + }, + }; + + helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete"); +}); + +add_task(async function test_helpers_getFxAStatus_extra_engines() { + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + getUserAccountData() { + return Promise.resolve({ + email: "testuser@testuser.com", + sessionToken: "sessionToken", + uid: "uid", + verified: true, + }); + }, + }, + }, + privateBrowsingUtils: { + isBrowserPrivate: () => true, + }, + }); + + Services.prefs.setBoolPref( + "services.sync.engine.creditcards.available", + true + ); + // Not defining "services.sync.engine.addresses.available" on purpose. + + let fxaStatus = await helpers.getFxaStatus("sync", mockSendingContext); + ok(!!fxaStatus); + ok(!!fxaStatus.signedInUser); + deepEqual(fxaStatus.capabilities.engines, ["creditcards"]); +}); + +add_task(async function test_helpers_getFxaStatus_allowed_signedInUser() { + let wasCalled = { + getUserAccountData: false, + shouldAllowFxaStatus: false, + }; + + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + getUserAccountData() { + wasCalled.getUserAccountData = true; + return Promise.resolve({ + email: "testuser@testuser.com", + sessionToken: "sessionToken", + uid: "uid", + verified: true, + }); + }, + }, + }, + }); + + helpers.shouldAllowFxaStatus = (service, sendingContext) => { + wasCalled.shouldAllowFxaStatus = true; + Assert.equal(service, "sync"); + Assert.equal(sendingContext, mockSendingContext); + + return true; + }; + + return helpers.getFxaStatus("sync", mockSendingContext).then(fxaStatus => { + Assert.ok(!!fxaStatus); + Assert.ok(wasCalled.getUserAccountData); + Assert.ok(wasCalled.shouldAllowFxaStatus); + + Assert.ok(!!fxaStatus.signedInUser); + let { signedInUser } = fxaStatus; + + Assert.equal(signedInUser.email, "testuser@testuser.com"); + Assert.equal(signedInUser.sessionToken, "sessionToken"); + Assert.equal(signedInUser.uid, "uid"); + Assert.ok(signedInUser.verified); + + // These properties are filtered and should not + // be returned to the requester. + Assert.equal(false, "scopedKeys" in signedInUser); + }); +}); + +add_task(async function test_helpers_getFxaStatus_allowed_no_signedInUser() { + let wasCalled = { + getUserAccountData: false, + shouldAllowFxaStatus: false, + }; + + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + getUserAccountData() { + wasCalled.getUserAccountData = true; + return Promise.resolve(null); + }, + }, + }, + }); + + helpers.shouldAllowFxaStatus = (service, sendingContext) => { + wasCalled.shouldAllowFxaStatus = true; + Assert.equal(service, "sync"); + Assert.equal(sendingContext, mockSendingContext); + + return true; + }; + + return helpers.getFxaStatus("sync", mockSendingContext).then(fxaStatus => { + Assert.ok(!!fxaStatus); + Assert.ok(wasCalled.getUserAccountData); + Assert.ok(wasCalled.shouldAllowFxaStatus); + + Assert.equal(null, fxaStatus.signedInUser); + }); +}); + +add_task(async function test_helpers_getFxaStatus_not_allowed() { + let wasCalled = { + getUserAccountData: false, + shouldAllowFxaStatus: false, + }; + + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + getUserAccountData() { + wasCalled.getUserAccountData = true; + return Promise.resolve(null); + }, + }, + }, + }); + + helpers.shouldAllowFxaStatus = ( + service, + sendingContext, + isPairing, + context + ) => { + wasCalled.shouldAllowFxaStatus = true; + Assert.equal(service, "sync"); + Assert.equal(sendingContext, mockSendingContext); + Assert.ok(!isPairing); + Assert.equal(context, "fx_desktop_v3"); + + return false; + }; + + return helpers + .getFxaStatus("sync", mockSendingContext, false, "fx_desktop_v3") + .then(fxaStatus => { + Assert.ok(!!fxaStatus); + Assert.ok(!wasCalled.getUserAccountData); + Assert.ok(wasCalled.shouldAllowFxaStatus); + + Assert.equal(null, fxaStatus.signedInUser); + }); +}); + +add_task( + async function test_helpers_shouldAllowFxaStatus_sync_service_not_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return false; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "sync", + mockSendingContext, + false + ); + Assert.ok(shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_desktop_context_not_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return false; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "", + mockSendingContext, + false, + "fx_desktop_v3" + ); + Assert.ok(shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_oauth_service_not_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return false; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "dcdb5ae7add825d2", + mockSendingContext, + false + ); + Assert.ok(shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_no_service_not_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return false; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "", + mockSendingContext, + false + ); + Assert.ok(shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_sync_service_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return true; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "sync", + mockSendingContext, + false + ); + Assert.ok(shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_desktop_context_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return true; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "", + mockSendingContext, + false, + "fx_desktop_v3" + ); + Assert.ok(shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_oauth_service_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return true; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "dcdb5ae7add825d2", + mockSendingContext, + false + ); + Assert.ok(!shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_oauth_service_pairing_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return true; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "dcdb5ae7add825d2", + mockSendingContext, + true + ); + Assert.ok(shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task( + async function test_helpers_shouldAllowFxaStatus_no_service_private_browsing() { + let wasCalled = { + isPrivateBrowsingMode: false, + }; + let helpers = new FxAccountsWebChannelHelpers({}); + + helpers.isPrivateBrowsingMode = sendingContext => { + wasCalled.isPrivateBrowsingMode = true; + Assert.equal(sendingContext, mockSendingContext); + return true; + }; + + let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( + "", + mockSendingContext, + false + ); + Assert.ok(!shouldAllowFxaStatus); + Assert.ok(wasCalled.isPrivateBrowsingMode); + } +); + +add_task(async function test_helpers_isPrivateBrowsingMode_private_browsing() { + let wasCalled = { + isBrowserPrivate: false, + }; + let helpers = new FxAccountsWebChannelHelpers({ + privateBrowsingUtils: { + isBrowserPrivate(browser) { + wasCalled.isBrowserPrivate = true; + Assert.equal( + browser, + mockSendingContext.browsingContext.top.embedderElement + ); + return true; + }, + }, + }); + + let isPrivateBrowsingMode = helpers.isPrivateBrowsingMode(mockSendingContext); + Assert.ok(isPrivateBrowsingMode); + Assert.ok(wasCalled.isBrowserPrivate); +}); + +add_task(async function test_helpers_isPrivateBrowsingMode_private_browsing() { + let wasCalled = { + isBrowserPrivate: false, + }; + let helpers = new FxAccountsWebChannelHelpers({ + privateBrowsingUtils: { + isBrowserPrivate(browser) { + wasCalled.isBrowserPrivate = true; + Assert.equal( + browser, + mockSendingContext.browsingContext.top.embedderElement + ); + return false; + }, + }, + }); + + let isPrivateBrowsingMode = helpers.isPrivateBrowsingMode(mockSendingContext); + Assert.ok(!isPrivateBrowsingMode); + Assert.ok(wasCalled.isBrowserPrivate); +}); + +add_task(async function test_helpers_change_password() { + let wasCalled = { + updateUserAccountData: false, + updateDeviceRegistration: false, + }; + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + updateUserAccountData(credentials) { + return new Promise(resolve => { + Assert.ok(credentials.hasOwnProperty("email")); + Assert.ok(credentials.hasOwnProperty("uid")); + Assert.ok(credentials.hasOwnProperty("unwrapBKey")); + Assert.ok(credentials.hasOwnProperty("device")); + Assert.equal(null, credentials.device); + Assert.equal(null, credentials.encryptedSendTabKeys); + // "foo" isn't a field known by storage, so should be dropped. + Assert.ok(!credentials.hasOwnProperty("foo")); + wasCalled.updateUserAccountData = true; + + resolve(); + }); + }, + + updateDeviceRegistration() { + Assert.equal(arguments.length, 0); + wasCalled.updateDeviceRegistration = true; + return Promise.resolve(); + }, + }, + }, + }); + await helpers.changePassword({ + email: "email", + uid: "uid", + unwrapBKey: "unwrapBKey", + foo: "foo", + }); + Assert.ok(wasCalled.updateUserAccountData); + Assert.ok(wasCalled.updateDeviceRegistration); +}); + +add_task(async function test_helpers_change_password_with_error() { + let wasCalled = { + updateUserAccountData: false, + updateDeviceRegistration: false, + }; + let helpers = new FxAccountsWebChannelHelpers({ + fxAccounts: { + _internal: { + updateUserAccountData() { + wasCalled.updateUserAccountData = true; + return Promise.reject(); + }, + + updateDeviceRegistration() { + wasCalled.updateDeviceRegistration = true; + return Promise.resolve(); + }, + }, + }, + }); + try { + await helpers.changePassword({}); + Assert.equal(false, "changePassword should have rejected"); + } catch (_) { + Assert.ok(wasCalled.updateUserAccountData); + Assert.ok(!wasCalled.updateDeviceRegistration); + } +}); + +function makeObserver(aObserveTopic, aObserveFunc) { + let callback = function (aSubject, aTopic, aData) { + log.debug("observed " + aTopic + " " + aData); + if (aTopic == aObserveTopic) { + removeMe(); + aObserveFunc(aSubject, aTopic, aData); + } + }; + + function removeMe() { + log.debug("removing observer for " + aObserveTopic); + Services.obs.removeObserver(callback, aObserveTopic); + } + + Services.obs.addObserver(callback, aObserveTopic); + return removeMe; +} + +function validationHelper(params, expected) { + try { + new FxAccountsWebChannel(params); + } catch (e) { + if (typeof expected === "string") { + return Assert.equal(e.toString(), expected); + } + return Assert.ok(e.toString().match(expected)); + } + throw new Error("Validation helper error"); +} diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.toml b/services/fxaccounts/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..7fc9c60006 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/xpcshell.toml @@ -0,0 +1,49 @@ +[DEFAULT] +head = "head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js" +firefox-appdir = "browser" +skip-if = [ + "os == 'android'", + "appname == 'thunderbird'", +] +support-files = [ + "!/services/common/tests/unit/head_helpers.js", + "!/services/common/tests/unit/head_http.js", +] + +["test_accounts.js"] + +["test_accounts_config.js"] + +["test_accounts_device_registration.js"] + +["test_client.js"] + +["test_commands.js"] + +["test_credentials.js"] + +["test_device.js"] + +["test_keys.js"] + +["test_loginmgr_storage.js"] + +["test_oauth_flow.js"] + +["test_oauth_token_storage.js"] + +["test_oauth_tokens.js"] + +["test_pairing.js"] + +["test_profile.js"] + +["test_profile_client.js"] + +["test_push_service.js"] + +["test_storage_manager.js"] + +["test_telemetry.js"] + +["test_web_channel.js"] diff --git a/services/interfaces/moz.build b/services/interfaces/moz.build new file mode 100644 index 0000000000..18b570f63c --- /dev/null +++ b/services/interfaces/moz.build @@ -0,0 +1,19 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Sync") + +# Services interfaces are shared with other components (like Places and +# WebExtension storage), so we keep them in a separate folder and build them for +# all configurations, regardless of whether we build Sync. + +XPIDL_MODULE = "services" + +XPIDL_SOURCES += [ + "mozIAppServicesLogger.idl", + "mozIBridgedSyncEngine.idl", + "mozIInterruptible.idl", + "mozIServicesLogSink.idl", +] diff --git a/services/interfaces/mozIAppServicesLogger.idl b/services/interfaces/mozIAppServicesLogger.idl new file mode 100644 index 0000000000..17d8c87c3a --- /dev/null +++ b/services/interfaces/mozIAppServicesLogger.idl @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "mozIServicesLogSink.idl" + +[scriptable, uuid(446dd837-fbb0-41e4-8221-f740f672b20d)] +interface mozIAppServicesLogger : nsISupports { + void register(in AString target, in mozIServicesLogSink logger); +}; diff --git a/services/interfaces/mozIBridgedSyncEngine.idl b/services/interfaces/mozIBridgedSyncEngine.idl new file mode 100644 index 0000000000..72683abf22 --- /dev/null +++ b/services/interfaces/mozIBridgedSyncEngine.idl @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozIServicesLogSink.idl" +#include "nsISupports.idl" + +interface nsIVariant; + +// A generic callback called with a result. Variants are automatically unboxed +// in JavaScript: for example, a `UTF8String` will be passed as a string +// argument; an `Int32` or `Int64` as a number. Methods that don't return a +// value, like `setLastSync` or `setUploaded`, will pass a `null` variant to +// `handleSuccess`. For all callback types in this file, either `handleSuccess` +// or `handleError` is guaranteed to be called once. +[scriptable, uuid(9b7dd2a3-df99-4469-9ea9-61b222098695)] +interface mozIBridgedSyncEngineCallback : nsISupports { + void handleSuccess(in nsIVariant result); + void handleError(in nsresult code, in AUTF8String message); +}; + +// A callback called after the engine applies incoming records. This is separate +// from `mozIBridgedSyncEngineCallback` because variants can't hold an +// `Array` type. +[scriptable, uuid(2776cdd5-799a-4009-b2f3-356d940a5244)] +interface mozIBridgedSyncEngineApplyCallback : nsISupports { + // Notifies Sync that the bridged engine has finished applying incoming + // records, and has outgoing records. Sync encrypts and uploads these + // records, and notifies the engine that the upload succeeded by + // calling `engine.setUploaded(uploadedOutgoingRecordIds, ...)`. + void handleSuccess(in Array outgoingEnvelopesAsJSON); + + // Notifies Sync that the bridged engine failed to apply the staged records. + void handleError(in nsresult code, in AUTF8String message); +}; + +// A bridged engine is implemented in Rust. It handles storage internally, and +// exposes a minimal interface for the JS Sync code to control it. +[scriptable, uuid(3b2b80be-c30e-4498-8065-01809cfe8d47)] +interface mozIBridgedSyncEngine : nsISupports { + // The storage version for this engine's collection. If the version in the + // server's `meta/global` record is newer than ours, we'll refuse to sync, + // since we might not understand the data; if it's older, we'll wipe the + // collection on the server, and upload our data as if on a first sync. + readonly attribute long storageVersion; + + // Whether this engine tolerates skipped records, where a "skipped" record + // is one that would cause the server's published limits to be exceeded + // (for example, a single record where the payload is larger than the + // server accepts.) + // If this returns true, we will just skip the record without even attempting + // to upload. If this is false, we'll abort the entire batch. + // If the engine allows this, it will need to detect this scenario by noticing + // the ID is not in the 'success' records reported to `setUploaded`. + // (Note that this is not to be confused with the fact server's can currently + // reject records as part of a POST - but we hope to remove this ability from + // the server API. Note also that this is not bullet-proof - if the count of + // records is high, it's possible that we will have committed a previous + // batch before we hit the relevant limits, so things might have been written. + // We hope to fix this by ensuring batch limits are such that this is + // impossible) + readonly attribute boolean allowSkippedRecord; + + // Wires up the Sync logging machinery to the bridged engine. This can be + // `null`, in which case any logs from the engine will be discarded. + attribute mozIServicesLogSink logger; + + // Returns the last sync time, in milliseconds, for this engine's + // collection. This is used to build the collection URL for fetching + // incoming records, and as the initial value of the `X-I-U-S` header on + // upload. If the engine persists incoming records in a permanent (non-temp) + // table, `getLastSync` can return a "high water mark" that's the newer of + // the collection's last sync time, and the most recent record modification + // time. This avoids redownloading incoming records that were previously + // downloaded, but not applied. + void getLastSync(in mozIBridgedSyncEngineCallback callback); + + // Sets the last sync time, in milliseconds. This is used to fast-forward + // the last sync time for the engine's collection after fetching all + // records, and after each `setUploaded` call with the `X-L-M` header from + // the server. It may be called multiple times per sync. + void setLastSync(in long long lastSyncMillis, + in mozIBridgedSyncEngineCallback callback); + + // Returns the sync ID for this engine's collection. Used for testing; + // Sync only calls `ensureCurrentSyncId` and `resetSyncId`. On success, + // calls `callback.handleSuccess(in AUTF8String currentSyncId)`. + void getSyncId(in mozIBridgedSyncEngineCallback callback); + + // Generates a new sync ID for this engine, and resets all local Sync + // metadata, including the last sync time and any change flags, to start + // over as a first sync. On success, calls + // `callback.handleSuccess(newSyncId)`, where `newSyncId` is + // `AUTF8String` variant. Sync will upload the new sync ID in the + // `meta/global` record. + void resetSyncId(in mozIBridgedSyncEngineCallback callback); + + // Ensures that the local sync ID for the engine matches the sync ID for + // the collection on the server. On a mismatch, the engine can: + // 1. Reset all local Sync state, adopt `newSyncId` as the new sync ID, + // and call `callback.handleSuccess(newSyncId)`. Most engines should + // do this. + // 2. Ignore the given `newSyncId`, use its existing local sync ID + // without resetting any state, and call + // `callback.handleSuccess(existingSyncId)`. This is useful if, for + // example, the underlying database has been restored from a backup, + // and the engine would like to force a reset and first sync on all + // other devices. + // 3. Ignore the given `newSyncId`, reset all local Sync state, and + // generate a fresh sync ID, as if `resetSyncId`. This resets the + // engine's state everywhere, locally and on all other devices. + // If the callback is called with a different sync ID than `newSyncId`, + // Sync will reupload `meta/global` with the different ID. Otherwise, it + // will assume that the engine has adopted the `newSyncId`, and do nothing. + void ensureCurrentSyncId(in AUTF8String newSyncId, + in mozIBridgedSyncEngineCallback callback); + + // Notifies the engine that sync is starting. The engine can use this method + // to set up temp tables for merging, for example. This will only be called + // once per sync, and before any `storeIncoming` calls. + void syncStarted(in mozIBridgedSyncEngineCallback callback); + + // Stages a batch of incoming records, and calls the `callback` when + // done. This method may be called multiple times per sync, once per + // incoming batch, and always after `syncStarted`. Flushing incoming records + // more often incurs more writes to disk, but avoids redownloading and + // reapplying more records if syncing is interrupted. Typically, engines + // will stage incoming records in an SQLite temp table, and merge them with + // the local database when `apply` is called. + void storeIncoming(in Array incomingEnvelopesAsJSON, + in mozIBridgedSyncEngineCallback callback); + + // Applies all the staged records, and calls the `callback` with + // outgoing records to upload. This will always be called after + // `storeIncoming`, and only once per sync. Application should be atomic: + // either all incoming records apply successfully, or none. + void apply(in mozIBridgedSyncEngineApplyCallback callback); + + // Notifies the engine that Sync successfully uploaded the records with the + // given IDs. This method may be called multiple times per sync, once per + // batch upload. This will always be called after `apply`. + void setUploaded(in long long newTimestampMillis, + in Array uploadedIds, + in mozIBridgedSyncEngineCallback callback); + + // Notifies the engine that syncing has finished, and the engine shouldn't + // expect any more `setUploaded` calls. At this point, any outgoing records + // that weren't passed to `setUploaded` should be assumed failed. This is + // guaranteed to be called even if the sync fails. This will only be called + // once per sync. + void syncFinished(in mozIBridgedSyncEngineCallback callback); + + // Resets all local Sync metadata, including the sync ID, last sync time, + // and any change flags, but preserves all data. After a reset, the engine will + // sync as if for the first time. + void reset(in mozIBridgedSyncEngineCallback callback); + + // Erases all locally stored data and metadata for this engine. + void wipe(in mozIBridgedSyncEngineCallback callback); +}; diff --git a/services/interfaces/mozIInterruptible.idl b/services/interfaces/mozIInterruptible.idl new file mode 100644 index 0000000000..2894c428d6 --- /dev/null +++ b/services/interfaces/mozIInterruptible.idl @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +// Interrupts all pending operations on a data store, if possible. This is +// provided as a separate interface because the store may want to hide this +// implementation from callers, as interrupting a write can cause data loss. +[scriptable, uuid(1c06bfd3-76b1-46fa-a64a-db682d478374)] +interface mozIInterruptible : nsISupports { + void interrupt(); +}; diff --git a/services/interfaces/mozIServicesLogSink.idl b/services/interfaces/mozIServicesLogSink.idl new file mode 100644 index 0000000000..ea17750552 --- /dev/null +++ b/services/interfaces/mozIServicesLogSink.idl @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +// Adapts a `Log.sys.mjs` logger so that it can be used from native (Rust) code. +// The synced bookmarks mirror and bridged engines implement this interface +// to hook in to the services `LogManager` infrastructure. +[scriptable, uuid(c92bfe0d-50b7-4a7f-9686-fe5335a696b9)] +interface mozIServicesLogSink : nsISupports { + const short LEVEL_OFF = 0; + const short LEVEL_ERROR = 1; + const short LEVEL_WARN = 2; + const short LEVEL_INFO = 3; + const short LEVEL_DEBUG = 4; + const short LEVEL_TRACE = 5; + + attribute short maxLevel; + + void error(in AString message); + void warn(in AString message); + void debug(in AString message); + void trace(in AString message); + void info(in AString message); +}; diff --git a/services/moz.build b/services/moz.build new file mode 100644 index 0000000000..4ef95d88d2 --- /dev/null +++ b/services/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("moz.build"): + BUG_COMPONENT = ("Firefox Build System", "General") + +DIRS += [ + "common", + "crypto", + "interfaces", + "settings", +] + +# The automation dir is only included in nightlies and debug +if not CONFIG["RELEASE_OR_BETA"] or CONFIG["MOZ_DEBUG"]: + DIRS += ["automation"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": + DIRS += [ + "fxaccounts", + ] + +if CONFIG["MOZ_SERVICES_SYNC"]: + DIRS += ["sync"] + +SPHINX_TREES["/services"] = "docs" diff --git a/services/settings/Attachments.sys.mjs b/services/settings/Attachments.sys.mjs new file mode 100644 index 0000000000..5ddc6bb046 --- /dev/null +++ b/services/settings/Attachments.sys.mjs @@ -0,0 +1,527 @@ +/* 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, { + RemoteSettingsWorker: + "resource://services-settings/RemoteSettingsWorker.sys.mjs", + Utils: "resource://services-settings/Utils.sys.mjs", +}); + +class DownloadError extends Error { + constructor(url, resp) { + super(`Could not download ${url}`); + this.name = "DownloadError"; + this.resp = resp; + } +} + +class BadContentError extends Error { + constructor(path) { + super(`${path} content does not match server hash`); + this.name = "BadContentError"; + } +} + +class ServerInfoError extends Error { + constructor(error) { + super(`Server response is invalid ${error}`); + this.name = "ServerInfoError"; + this.original = error; + } +} + +// Helper for the `download` method for commonly used methods, to help with +// lazily accessing the record and attachment content. +class LazyRecordAndBuffer { + constructor(getRecordAndLazyBuffer) { + this.getRecordAndLazyBuffer = getRecordAndLazyBuffer; + } + + async _ensureRecordAndLazyBuffer() { + if (!this.recordAndLazyBufferPromise) { + this.recordAndLazyBufferPromise = this.getRecordAndLazyBuffer(); + } + return this.recordAndLazyBufferPromise; + } + + /** + * @returns {object} The attachment record, if found. null otherwise. + **/ + async getRecord() { + try { + return (await this._ensureRecordAndLazyBuffer()).record; + } catch (e) { + return null; + } + } + + /** + * @param {object} requestedRecord An attachment record + * @returns {boolean} Whether the requested record matches this record. + **/ + async isMatchingRequestedRecord(requestedRecord) { + const record = await this.getRecord(); + return ( + record && + record.last_modified === requestedRecord.last_modified && + record.attachment.size === requestedRecord.attachment.size && + record.attachment.hash === requestedRecord.attachment.hash + ); + } + + /** + * Generate the return value for the "download" method. + * + * @throws {*} if the record or attachment content is unavailable. + * @returns {Object} An object with two properties: + * buffer: ArrayBuffer with the file content. + * record: Record associated with the bytes. + **/ + async getResult() { + const { record, readBuffer } = await this._ensureRecordAndLazyBuffer(); + if (!this.bufferPromise) { + this.bufferPromise = readBuffer(); + } + return { record, buffer: await this.bufferPromise }; + } +} + +export class Downloader { + static get DownloadError() { + return DownloadError; + } + static get BadContentError() { + return BadContentError; + } + static get ServerInfoError() { + return ServerInfoError; + } + + constructor(...folders) { + this.folders = ["settings", ...folders]; + this._cdnURLs = {}; + } + + /** + * @returns {Object} An object with async "get", "set" and "delete" methods. + * The keys are strings, the values may be any object that + * can be stored in IndexedDB (including Blob). + */ + get cacheImpl() { + throw new Error("This Downloader does not support caching"); + } + + /** + * Download attachment and return the result together with the record. + * If the requested record cannot be downloaded and fallbacks are enabled, the + * returned attachment may have a different record than the input record. + * + * @param {Object} record A Remote Settings entry with attachment. + * If omitted, the attachmentId option must be set. + * @param {Object} options Some download options. + * @param {Number} options.retries Number of times download should be retried (default: `3`) + * @param {Boolean} options.checkHash Check content integrity (default: `true`) + * @param {string} options.attachmentId The attachment identifier to use for + * caching and accessing the attachment. + * (default: `record.id`) + * @param {Boolean} options.fallbackToCache Return the cached attachment when the + * input record cannot be fetched. + * (default: `false`) + * @param {Boolean} options.fallbackToDump Use the remote settings dump as a + * potential source of the attachment. + * (default: `false`) + * @throws {Downloader.DownloadError} if the file could not be fetched. + * @throws {Downloader.BadContentError} if the downloaded content integrity is not valid. + * @throws {Downloader.ServerInfoError} if the server response is not valid. + * @throws {NetworkError} if fetching the server infos and fetching the attachment fails. + * @returns {Object} An object with two properties: + * `buffer` `ArrayBuffer`: the file content. + * `record` `Object`: record associated with the attachment. + * `_source` `String`: identifies the source of the result. Used for testing. + */ + async download(record, options) { + let { + retries, + checkHash, + attachmentId = record?.id, + fallbackToCache = false, + fallbackToDump = false, + } = options || {}; + if (!attachmentId) { + // Check for pre-condition. This should not happen, but it is explicitly + // checked to avoid mixing up attachments, which could be dangerous. + throw new Error( + "download() was called without attachmentId or `record.id`" + ); + } + + const dumpInfo = new LazyRecordAndBuffer(() => + this._readAttachmentDump(attachmentId) + ); + const cacheInfo = new LazyRecordAndBuffer(() => + this._readAttachmentCache(attachmentId) + ); + + // Check if an attachment dump has been packaged with the client. + // The dump is checked before the cache because dumps are expected to match + // the requested record, at least shortly after the release of the client. + if (fallbackToDump && record) { + if (await dumpInfo.isMatchingRequestedRecord(record)) { + try { + return { ...(await dumpInfo.getResult()), _source: "dump_match" }; + } catch (e) { + // Failed to read dump: record found but attachment file is missing. + console.error(e); + } + } + } + + // Check if the requested attachment has already been cached. + if (record) { + if (await cacheInfo.isMatchingRequestedRecord(record)) { + try { + return { ...(await cacheInfo.getResult()), _source: "cache_match" }; + } catch (e) { + // Failed to read cache, e.g. IndexedDB unusable. + console.error(e); + } + } + } + + let errorIfAllFails; + + // There is no local version that matches the requested record. + // Try to download the attachment specified in record. + if (record && record.attachment) { + try { + const newBuffer = await this.downloadAsBytes(record, { + retries, + checkHash, + }); + const blob = new Blob([newBuffer]); + // Store in cache but don't wait for it before returning. + this.cacheImpl + .set(attachmentId, { record, blob }) + .catch(e => console.error(e)); + return { buffer: newBuffer, record, _source: "remote_match" }; + } catch (e) { + // No network, corrupted content, etc. + errorIfAllFails = e; + } + } + + // Unable to find an attachment that matches the record. Consider falling + // back to local versions, even if their attachment hash do not match the + // one from the requested record. + + // Unable to find a valid attachment, fall back to the cached attachment. + const cacheRecord = fallbackToCache && (await cacheInfo.getRecord()); + if (cacheRecord) { + const dumpRecord = fallbackToDump && (await dumpInfo.getRecord()); + if (dumpRecord?.last_modified >= cacheRecord.last_modified) { + // The dump can be more recent than the cache when the client (and its + // packaged dump) is updated. + try { + return { ...(await dumpInfo.getResult()), _source: "dump_fallback" }; + } catch (e) { + // Failed to read dump: record found but attachment file is missing. + console.error(e); + } + } + + try { + return { ...(await cacheInfo.getResult()), _source: "cache_fallback" }; + } catch (e) { + // Failed to read from cache, e.g. IndexedDB unusable. + console.error(e); + } + } + + // Unable to find a valid attachment, fall back to the packaged dump. + if (fallbackToDump && (await dumpInfo.getRecord())) { + try { + return { ...(await dumpInfo.getResult()), _source: "dump_fallback" }; + } catch (e) { + errorIfAllFails = e; + } + } + + if (errorIfAllFails) { + throw errorIfAllFails; + } + + throw new Downloader.DownloadError(attachmentId); + } + + /** + * Is the record downloaded? This does not check if it was bundled. + * + * @param record A Remote Settings entry with attachment. + * @returns {Promise} + */ + isDownloaded(record) { + const cacheInfo = new LazyRecordAndBuffer(() => + this._readAttachmentCache(record.id) + ); + return cacheInfo.isMatchingRequestedRecord(record); + } + + /** + * Delete the record attachment downloaded locally. + * No-op if the attachment does not exist. + * + * @param record A Remote Settings entry with attachment. + * @param {Object} options Some options. + * @param {string} options.attachmentId The attachment identifier to use for + * accessing and deleting the attachment. + * (default: `record.id`) + */ + async deleteDownloaded(record, options) { + let { attachmentId = record?.id } = options || {}; + if (!attachmentId) { + // Check for pre-condition. This should not happen, but it is explicitly + // checked to avoid mixing up attachments, which could be dangerous. + throw new Error( + "deleteDownloaded() was called without attachmentId or `record.id`" + ); + } + return this.cacheImpl.delete(attachmentId); + } + + /** + * Clear the cache from obsolete downloaded attachments. + * + * @param {Array} excludeIds List of attachments IDs to exclude from pruning. + */ + async prune(excludeIds) { + return this.cacheImpl.prune(excludeIds); + } + + /** + * @deprecated See https://bugzilla.mozilla.org/show_bug.cgi?id=1634127 + * + * Download the record attachment into the local profile directory + * and return a file:// URL that points to the local path. + * + * No-op if the file was already downloaded and not corrupted. + * + * @param {Object} record A Remote Settings entry with attachment. + * @param {Object} options Some download options. + * @param {Number} options.retries Number of times download should be retried (default: `3`) + * @throws {Downloader.DownloadError} if the file could not be fetched. + * @throws {Downloader.BadContentError} if the downloaded file integrity is not valid. + * @throws {Downloader.ServerInfoError} if the server response is not valid. + * @throws {NetworkError} if fetching the attachment fails. + * @returns {String} the absolute file path to the downloaded attachment. + */ + async downloadToDisk(record, options = {}) { + const { retries = 3 } = options; + const { + attachment: { filename, size, hash }, + } = record; + const localFilePath = PathUtils.join( + PathUtils.localProfileDir, + ...this.folders, + filename + ); + const localFileUrl = PathUtils.toFileURI(localFilePath); + + await this._makeDirs(); + + let retried = 0; + while (true) { + if ( + await lazy.RemoteSettingsWorker.checkFileHash(localFileUrl, size, hash) + ) { + return localFileUrl; + } + // File does not exist or is corrupted. + if (retried > retries) { + throw new Downloader.BadContentError(localFilePath); + } + try { + // Download and write on disk. + const buffer = await this.downloadAsBytes(record, { + checkHash: false, // Hash will be checked on file. + retries: 0, // Already in a retry loop. + }); + await IOUtils.write(localFilePath, new Uint8Array(buffer), { + tmpPath: `${localFilePath}.tmp`, + }); + } catch (e) { + if (retried >= retries) { + throw e; + } + } + retried++; + } + } + + /** + * Download the record attachment and return its content as bytes. + * + * @param {Object} record A Remote Settings entry with attachment. + * @param {Object} options Some download options. + * @param {Number} options.retries Number of times download should be retried (default: `3`) + * @param {Boolean} options.checkHash Check content integrity (default: `true`) + * @throws {Downloader.DownloadError} if the file could not be fetched. + * @throws {Downloader.BadContentError} if the downloaded content integrity is not valid. + * @returns {ArrayBuffer} the file content. + */ + async downloadAsBytes(record, options = {}) { + const { + attachment: { location, hash, size }, + } = record; + + const remoteFileUrl = (await this._baseAttachmentsURL()) + location; + + const { retries = 3, checkHash = true } = options; + let retried = 0; + while (true) { + try { + const buffer = await this._fetchAttachment(remoteFileUrl); + if (!checkHash) { + return buffer; + } + if ( + await lazy.RemoteSettingsWorker.checkContentHash(buffer, size, hash) + ) { + return buffer; + } + // Content is corrupted. + throw new Downloader.BadContentError(location); + } catch (e) { + if (retried >= retries) { + throw e; + } + } + retried++; + } + } + + /** + * @deprecated See https://bugzilla.mozilla.org/show_bug.cgi?id=1634127 + * + * Delete the record attachment downloaded locally. + * This is the counterpart of `downloadToDisk()`. + * Use `deleteDownloaded()` if `download()` was used to retrieve + * the attachment. + * + * No-op if the related file does not exist. + * + * @param record A Remote Settings entry with attachment. + */ + async deleteFromDisk(record) { + const { + attachment: { filename }, + } = record; + const path = PathUtils.join( + PathUtils.localProfileDir, + ...this.folders, + filename + ); + await IOUtils.remove(path); + await this._rmDirs(); + } + + async _baseAttachmentsURL() { + if (!this._cdnURLs[lazy.Utils.SERVER_URL]) { + const resp = await lazy.Utils.fetch(`${lazy.Utils.SERVER_URL}/`); + let serverInfo; + try { + serverInfo = await resp.json(); + } catch (error) { + throw new Downloader.ServerInfoError(error); + } + // Server capabilities expose attachments configuration. + const { + capabilities: { + attachments: { base_url }, + }, + } = serverInfo; + // Make sure the URL always has a trailing slash. + this._cdnURLs[lazy.Utils.SERVER_URL] = + base_url + (base_url.endsWith("/") ? "" : "/"); + } + return this._cdnURLs[lazy.Utils.SERVER_URL]; + } + + async _fetchAttachment(url) { + const headers = new Headers(); + headers.set("Accept-Encoding", "gzip"); + const resp = await lazy.Utils.fetch(url, { headers }); + if (!resp.ok) { + throw new Downloader.DownloadError(url, resp); + } + return resp.arrayBuffer(); + } + + async _readAttachmentCache(attachmentId) { + const cached = await this.cacheImpl.get(attachmentId); + if (!cached) { + throw new Downloader.DownloadError(attachmentId); + } + return { + record: cached.record, + async readBuffer() { + const buffer = await cached.blob.arrayBuffer(); + const { size, hash } = cached.record.attachment; + if ( + await lazy.RemoteSettingsWorker.checkContentHash(buffer, size, hash) + ) { + return buffer; + } + // Really unexpected, could indicate corruption in IndexedDB. + throw new Downloader.BadContentError(attachmentId); + }, + }; + } + + async _readAttachmentDump(attachmentId) { + async function fetchResource(resourceUrl) { + try { + return await fetch(resourceUrl); + } catch (e) { + throw new Downloader.DownloadError(resourceUrl); + } + } + const resourceUrlPrefix = + Downloader._RESOURCE_BASE_URL + "/" + this.folders.join("/") + "/"; + const recordUrl = `${resourceUrlPrefix}${attachmentId}.meta.json`; + const attachmentUrl = `${resourceUrlPrefix}${attachmentId}`; + const record = await (await fetchResource(recordUrl)).json(); + return { + record, + async readBuffer() { + return (await fetchResource(attachmentUrl)).arrayBuffer(); + }, + }; + } + + // Separate variable to allow tests to override this. + static _RESOURCE_BASE_URL = "resource://app/defaults"; + + async _makeDirs() { + const dirPath = PathUtils.join(PathUtils.localProfileDir, ...this.folders); + await IOUtils.makeDirectory(dirPath, { createAncestors: true }); + } + + async _rmDirs() { + for (let i = this.folders.length; i > 0; i--) { + const dirPath = PathUtils.join( + PathUtils.localProfileDir, + ...this.folders.slice(0, i) + ); + try { + await IOUtils.remove(dirPath); + } catch (e) { + // This could fail if there's something in + // the folder we're not permitted to remove. + break; + } + } + } +} diff --git a/services/settings/Database.sys.mjs b/services/settings/Database.sys.mjs new file mode 100644 index 0000000000..e8a89028bc --- /dev/null +++ b/services/settings/Database.sys.mjs @@ -0,0 +1,602 @@ +/* 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, { + AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", + CommonUtils: "resource://services-common/utils.sys.mjs", + IDBHelpers: "resource://services-settings/IDBHelpers.sys.mjs", + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + Utils: "resource://services-settings/Utils.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "console", () => lazy.Utils.log); + +/** + * Database is a tiny wrapper with the objective + * of providing major kinto-offline-client collection API. + * (with the objective of getting rid of kinto-offline-client) + */ +export class Database { + static destroy() { + return destroyIDB(); + } + + constructor(identifier) { + ensureShutdownBlocker(); + this.identifier = identifier; + } + + async list(options = {}) { + const { filters = {}, order = "" } = options; + let results = []; + try { + await executeIDB( + "records", + (store, rejectTransaction) => { + // Fast-path the (very common) no-filters case + if (lazy.ObjectUtils.isEmpty(filters)) { + const range = IDBKeyRange.only(this.identifier); + const request = store.index("cid").getAll(range); + request.onsuccess = e => { + results = e.target.result; + }; + return; + } + const request = store + .index("cid") + .openCursor(IDBKeyRange.only(this.identifier)); + const objFilters = transformSubObjectFilters(filters); + request.onsuccess = event => { + try { + const cursor = event.target.result; + if (cursor) { + const { value } = cursor; + if (lazy.Utils.filterObject(objFilters, value)) { + results.push(value); + } + cursor.continue(); + } + } catch (ex) { + rejectTransaction(ex); + } + }; + }, + { mode: "readonly" } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError(e, "list()", this.identifier); + } + // Remove IDB key field from results. + for (const result of results) { + delete result._cid; + } + return order ? lazy.Utils.sortObjects(order, results) : results; + } + + async importChanges(metadata, timestamp, records = [], options = {}) { + const { clear = false } = options; + const _cid = this.identifier; + try { + await executeIDB( + ["collections", "timestamps", "records"], + (stores, rejectTransaction) => { + const [storeMetadata, storeTimestamps, storeRecords] = stores; + + if (clear) { + // Our index is over the _cid and id fields. We want to remove + // all of the items in the collection for which the object was + // created, ie with _cid == this.identifier. + // We would like to just tell IndexedDB: + // store.index(IDBKeyRange.only(this.identifier)).delete(); + // to delete all records matching the first part of the 2-part key. + // Unfortunately such an API does not exist. + // While we could iterate over the index with a cursor, we'd do + // a roundtrip to PBackground for each item. Once you have 1000 + // items, the result is very slow because of all the overhead of + // jumping between threads and serializing/deserializing. + // So instead, we tell the store to delete everything between + // "our" _cid identifier, and what would be the next identifier + // (via lexicographical sorting). Unfortunately there does not + // seem to be a way to specify bounds for all items that share + // the same first part of the key using just that first part, hence + // the use of the hypothetical [] for the second part of the end of + // the bounds. + storeRecords.delete( + IDBKeyRange.bound([_cid], [_cid, []], false, true) + ); + } + + // Store or erase metadata. + if (metadata === null) { + storeMetadata.delete(_cid); + } else if (metadata) { + storeMetadata.put({ cid: _cid, metadata }); + } + // Store or erase timestamp. + if (timestamp === null) { + storeTimestamps.delete(_cid); + } else if (timestamp) { + storeTimestamps.put({ cid: _cid, value: timestamp }); + } + + if (!records.length) { + return; + } + + // Separate tombstones from creations/updates. + const toDelete = records.filter(r => r.deleted); + const toInsert = records.filter(r => !r.deleted); + lazy.console.debug( + `${_cid} ${toDelete.length} to delete, ${toInsert.length} to insert` + ); + // Delete local records for each tombstone. + lazy.IDBHelpers.bulkOperationHelper( + storeRecords, + { + reject: rejectTransaction, + completion() { + // Overwrite all other data. + lazy.IDBHelpers.bulkOperationHelper( + storeRecords, + { + reject: rejectTransaction, + }, + "put", + toInsert.map(item => ({ ...item, _cid })) + ); + }, + }, + "delete", + toDelete.map(item => [_cid, item.id]) + ); + }, + { desc: "importChanges() in " + _cid } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError(e, "importChanges()", _cid); + } + } + + async getLastModified() { + let entry = null; + try { + await executeIDB( + "timestamps", + store => { + store.get(this.identifier).onsuccess = e => (entry = e.target.result); + }, + { mode: "readonly" } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError( + e, + "getLastModified()", + this.identifier + ); + } + if (!entry) { + return null; + } + // Some distributions where released with a modified dump that did not + // contain timestamps for last_modified. Work around this here, and return + // the timestamp as zero, so that the entries should get updated. + if (isNaN(entry.value)) { + lazy.console.warn(`Local timestamp is NaN for ${this.identifier}`); + return 0; + } + return entry.value; + } + + async getMetadata() { + let entry = null; + try { + await executeIDB( + "collections", + store => { + store.get(this.identifier).onsuccess = e => (entry = e.target.result); + }, + { mode: "readonly" } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError( + e, + "getMetadata()", + this.identifier + ); + } + return entry ? entry.metadata : null; + } + + async getAttachment(attachmentId) { + let entry = null; + try { + await executeIDB( + "attachments", + store => { + store.get([this.identifier, attachmentId]).onsuccess = e => { + entry = e.target.result; + }; + }, + { mode: "readonly" } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError( + e, + "getAttachment()", + this.identifier + ); + } + return entry ? entry.attachment : null; + } + + async saveAttachment(attachmentId, attachment) { + try { + await executeIDB( + "attachments", + store => { + if (attachment) { + store.put({ cid: this.identifier, attachmentId, attachment }); + } else { + store.delete([this.identifier, attachmentId]); + } + }, + { desc: "saveAttachment(" + attachmentId + ") in " + this.identifier } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError( + e, + "saveAttachment()", + this.identifier + ); + } + } + + /** + * Delete all attachments which don't match any record. + * + * Attachments are linked to records, except when a fixed `attachmentId` is used. + * A record can be updated or deleted, potentially by deleting a record and restoring an updated version + * of the record with the same ID. Potentially leaving orphaned attachments in the database. + * Since we run the pruning logic after syncing, any attachment without a + * matching record can be discarded as they will be unreachable forever. + * + * @param {Array} excludeIds List of attachments IDs to exclude from pruning. + */ + async pruneAttachments(excludeIds) { + const _cid = this.identifier; + let deletedCount = 0; + try { + await executeIDB( + ["attachments", "records"], + async (stores, rejectTransaction) => { + const [attachmentsStore, recordsStore] = stores; + + // List all stored attachments. + // All keys ≥ [_cid, ..] && < [_cid, []]. See comment in `importChanges()` + const rangeAllKeys = IDBKeyRange.bound( + [_cid], + [_cid, []], + false, + true + ); + const allAttachments = await new Promise((resolve, reject) => { + const request = attachmentsStore.getAll(rangeAllKeys); + request.onsuccess = e => resolve(e.target.result); + request.onerror = e => reject(e); + }); + if (!allAttachments.length) { + lazy.console.debug( + `${this.identifier} No attachments in IDB cache. Nothing to do.` + ); + return; + } + + // List all stored records. + const allRecords = await new Promise((resolve, reject) => { + const rangeAllIndexed = IDBKeyRange.only(_cid); + const request = recordsStore.index("cid").getAll(rangeAllIndexed); + request.onsuccess = e => resolve(e.target.result); + request.onerror = e => reject(e); + }); + + // Compare known records IDs to those stored along the attachments. + const currentRecordsIDs = new Set(allRecords.map(r => r.id)); + const attachmentsToDelete = allAttachments.reduce((acc, entry) => { + // Skip excluded attachments. + if (excludeIds.includes(entry.attachmentId)) { + return acc; + } + // Delete attachment if associated record does not exist. + if (!currentRecordsIDs.has(entry.attachment.record.id)) { + acc.push([_cid, entry.attachmentId]); + } + return acc; + }, []); + + // Perform a bulk delete of all obsolete attachments. + lazy.console.debug( + `${this.identifier} Bulk delete ${attachmentsToDelete.length} obsolete attachments` + ); + lazy.IDBHelpers.bulkOperationHelper( + attachmentsStore, + { + reject: rejectTransaction, + }, + "delete", + attachmentsToDelete + ); + deletedCount = attachmentsToDelete.length; + }, + { desc: "pruneAttachments() in " + this.identifier } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError( + e, + "pruneAttachments()", + this.identifier + ); + } + return deletedCount; + } + + async clear() { + try { + await this.importChanges(null, null, [], { clear: true }); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError(e, "clear()", this.identifier); + } + } + + /* + * Methods used by unit tests. + */ + + async create(record) { + if (!("id" in record)) { + record = { ...record, id: lazy.CommonUtils.generateUUID() }; + } + try { + await executeIDB( + "records", + store => { + store.add({ ...record, _cid: this.identifier }); + }, + { desc: "create() in " + this.identifier } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError(e, "create()", this.identifier); + } + return record; + } + + async update(record) { + try { + await executeIDB( + "records", + store => { + store.put({ ...record, _cid: this.identifier }); + }, + { desc: "update() in " + this.identifier } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError(e, "update()", this.identifier); + } + } + + async delete(recordId) { + try { + await executeIDB( + "records", + store => { + store.delete([this.identifier, recordId]); // [_cid, id] + }, + { desc: "delete() in " + this.identifier } + ); + } catch (e) { + throw new lazy.IDBHelpers.IndexedDBError(e, "delete()", this.identifier); + } + } +} + +let gDB = null; +let gDBPromise = null; + +/** + * This function attempts to ensure `gDB` points to a valid database value. + * If gDB is already a database, it will do no-op (but this may take a + * microtask or two). + * If opening the database fails, it will throw an IndexedDBError. + */ +async function openIDB() { + // We can be called multiple times in a race; always ensure that when + // we complete, `gDB` is no longer null, but avoid doing the actual + // IndexedDB work more than once. + if (!gDBPromise) { + // Open and initialize/upgrade if needed. + gDBPromise = lazy.IDBHelpers.openIDB(); + } + let db = await gDBPromise; + if (!gDB) { + gDB = db; + } +} + +const gPendingReadOnlyTransactions = new Set(); +const gPendingWriteOperations = new Set(); +/** + * Helper to wrap some IDBObjectStore operations into a promise. + * + * @param {IDBDatabase} db + * @param {String|String[]} storeNames - either a string or an array of strings. + * @param {function} callback + * @param {Object} options + * @param {String} options.mode + * @param {String} options.desc for shutdown tracking. + */ +async function executeIDB(storeNames, callback, options = {}) { + if (!gDB) { + // Check if we're shutting down. Services.startup.shuttingDown will + // be true sooner, but is never true in xpcshell tests, so we check + // both that and a bool we set ourselves when `profile-before-change` + // starts. + if (gShutdownStarted || Services.startup.shuttingDown) { + throw new lazy.IDBHelpers.ShutdownError( + "The application is shutting down", + "execute()" + ); + } + await openIDB(); + } else { + // Even if we have a db, wait a tick to avoid making IndexedDB sad. + // We should be able to remove this once bug 1626935 is fixed. + await Promise.resolve(); + } + + // Check for shutdown again as we've await'd something... + if (!gDB && (gShutdownStarted || Services.startup.shuttingDown)) { + throw new lazy.IDBHelpers.ShutdownError( + "The application is shutting down", + "execute()" + ); + } + + // Start the actual transaction: + const { mode = "readwrite", desc = "" } = options; + let { promise, transaction } = lazy.IDBHelpers.executeIDB( + gDB, + storeNames, + mode, + callback, + desc + ); + + // We track all readonly transactions and abort them at shutdown. + // We track all readwrite ones and await their completion at shutdown + // (to avoid dataloss when writes fail). + // We use a `.finally()` clause for this; it'll run the function irrespective + // of whether the promise resolves or rejects, and the promise it returns + // will resolve/reject with the same value. + let finishedFn; + if (mode == "readonly") { + gPendingReadOnlyTransactions.add(transaction); + finishedFn = () => gPendingReadOnlyTransactions.delete(transaction); + } else { + let obj = { promise, desc }; + gPendingWriteOperations.add(obj); + finishedFn = () => gPendingWriteOperations.delete(obj); + } + return promise.finally(finishedFn); +} + +async function destroyIDB() { + if (gDB) { + if (gShutdownStarted || Services.startup.shuttingDown) { + throw new lazy.IDBHelpers.ShutdownError( + "The application is shutting down", + "destroyIDB()" + ); + } + + // This will return immediately; the actual close will happen once + // there are no more running transactions. + gDB.close(); + const allTransactions = new Set([ + ...gPendingWriteOperations, + ...gPendingReadOnlyTransactions, + ]); + for (let transaction of Array.from(allTransactions)) { + try { + transaction.abort(); + } catch (ex) { + // Ignore errors to abort transactions, we'll destroy everything. + } + } + } + gDB = null; + gDBPromise = null; + return lazy.IDBHelpers.destroyIDB(); +} + +function makeNestedObjectFromArr(arr, val, nestedFiltersObj) { + const last = arr.length - 1; + return arr.reduce((acc, cv, i) => { + if (i === last) { + return (acc[cv] = val); + } else if (Object.prototype.hasOwnProperty.call(acc, cv)) { + return acc[cv]; + } + return (acc[cv] = {}); + }, nestedFiltersObj); +} + +function transformSubObjectFilters(filtersObj) { + const transformedFilters = {}; + for (const [key, val] of Object.entries(filtersObj)) { + const keysArr = key.split("."); + makeNestedObjectFromArr(keysArr, val, transformedFilters); + } + return transformedFilters; +} + +// We need to expose this wrapper function so we can test +// shutdown handling. +Database._executeIDB = executeIDB; + +let gShutdownStarted = false; +// Test-only helper to be able to test shutdown multiple times: +Database._cancelShutdown = () => { + gShutdownStarted = false; +}; + +let gShutdownBlocker = false; +Database._shutdownHandler = () => { + gShutdownStarted = true; + const NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR = 0x80660006; + // Duplicate the list (to avoid it being modified) and then + // abort all read-only transactions. + for (let transaction of Array.from(gPendingReadOnlyTransactions)) { + try { + transaction.abort(); + } catch (ex) { + // Ensure we don't throw/break, because either way we're in shutdown. + + // In particular, `transaction.abort` can throw if the transaction + // is complete, ie if we manage to get called in between the + // transaction completing, and our completion handler being called + // to remove the item from the set. We don't care about that. + if (ex.result != NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR) { + // Report any other errors: + console.error(ex); + } + } + } + if (gDB) { + // This will return immediately; the actual close will happen once + // there are no more running transactions. + gDB.close(); + gDB = null; + } + gDBPromise = null; + return Promise.allSettled( + Array.from(gPendingWriteOperations).map(op => op.promise) + ); +}; + +function ensureShutdownBlocker() { + if (gShutdownBlocker) { + return; + } + gShutdownBlocker = true; + lazy.AsyncShutdown.profileBeforeChange.addBlocker( + "RemoteSettingsClient - finish IDB access.", + Database._shutdownHandler, + { + fetchState() { + return Array.from(gPendingWriteOperations).map(op => op.desc); + }, + } + ); +} diff --git a/services/settings/IDBHelpers.sys.mjs b/services/settings/IDBHelpers.sys.mjs new file mode 100644 index 0000000000..6f2ef6937d --- /dev/null +++ b/services/settings/IDBHelpers.sys.mjs @@ -0,0 +1,212 @@ +/* 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 DB_NAME = "remote-settings"; +const DB_VERSION = 3; + +/** + * Wrap IndexedDB errors to catch them more easily. + */ +class IndexedDBError extends Error { + constructor(error, method = "", identifier = "") { + if (typeof error == "string") { + error = new Error(error); + } + super(`IndexedDB: ${identifier} ${method} ${error && error.message}`); + this.name = error.name; + this.stack = error.stack; + } +} + +class ShutdownError extends IndexedDBError { + constructor(error, method = "", identifier = "") { + super(error, method, identifier); + } +} + +// We batch operations in order to reduce round-trip latency to the IndexedDB +// database thread. The trade-offs are that the more records in the batch, the +// more time we spend on this thread in structured serialization, and the +// greater the chance to jank PBackground and this thread when the responses +// come back. The initial choice of 250 was made targeting 2-3ms on a fast +// machine and 10-15ms on a slow machine. +// Every chunk waits for success before starting the next, and +// the final chunk's completion will fire transaction.oncomplete . +function bulkOperationHelper( + store, + { reject, completion }, + operation, + list, + listIndex = 0 +) { + try { + const CHUNK_LENGTH = 250; + const max = Math.min(listIndex + CHUNK_LENGTH, list.length); + let request; + for (; listIndex < max; listIndex++) { + request = store[operation](list[listIndex]); + } + if (listIndex < list.length) { + // On error, `transaction.onerror` is called. + request.onsuccess = bulkOperationHelper.bind( + null, + store, + { reject, completion }, + operation, + list, + listIndex + ); + } else if (completion) { + completion(); + } + // otherwise, we're done, and the transaction will complete on its own. + } catch (e) { + // The executeIDB callsite has a try... catch, but it will not catch + // errors in subsequent bulkOperationHelper calls chained through + // request.onsuccess above. We do want to catch those, so we have to + // feed them through manually. We cannot use an async function with + // promises, because if we wait a microtask after onsuccess fires to + // put more requests on the transaction, the transaction will auto-commit + // before we can add more requests. + reject(e); + } +} + +/** + * Helper to wrap some IDBObjectStore operations into a promise. + * + * @param {IDBDatabase} db + * @param {String|String[]} storeNames - either a string or an array of strings. + * @param {String} mode + * @param {function} callback + * @param {String} description of the operation for error handling purposes. + */ +function executeIDB(db, storeNames, mode, callback, desc) { + if (!Array.isArray(storeNames)) { + storeNames = [storeNames]; + } + const transaction = db.transaction(storeNames, mode); + let promise = new Promise((resolve, reject) => { + let stores = storeNames.map(name => transaction.objectStore(name)); + let result; + let rejectWrapper = e => { + reject(new IndexedDBError(e, desc || "execute()", storeNames.join(", "))); + try { + transaction.abort(); + } catch (ex) { + console.error(ex); + } + }; + // Add all the handlers before using the stores. + transaction.onerror = event => + reject(new IndexedDBError(event.target.error, desc || "execute()")); + transaction.onabort = event => + reject( + new IndexedDBError( + event.target.error || transaction.error || "IDBTransaction aborted", + desc || "execute()" + ) + ); + transaction.oncomplete = event => resolve(result); + // Simplify access to a single datastore: + if (stores.length == 1) { + stores = stores[0]; + } + try { + // Although this looks sync, once the callback places requests + // on the datastore, it can independently keep the transaction alive and + // keep adding requests. Even once we exit this try.. catch, we may + // therefore experience errors which should abort the transaction. + // This is why we pass the rejection handler - then the consumer can + // continue to ensure that errors are handled appropriately. + // In theory, exceptions thrown from onsuccess handlers should also + // cause IndexedDB to abort the transaction, so this is a belt-and-braces + // approach. + result = callback(stores, rejectWrapper); + } catch (e) { + rejectWrapper(e); + } + }); + return { promise, transaction }; +} + +/** + * Helper to wrap indexedDB.open() into a promise. + */ +async function openIDB(allowUpgrades = true) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = event => { + if (!allowUpgrades) { + reject( + new Error( + `IndexedDB: Error accessing ${DB_NAME} IDB at version ${DB_VERSION}` + ) + ); + return; + } + // When an upgrade is needed, a transaction is started. + const transaction = event.target.transaction; + transaction.onabort = event => { + const error = + event.target.error || + transaction.error || + new DOMException("The operation has been aborted", "AbortError"); + reject(new IndexedDBError(error, "open()")); + }; + + const db = event.target.result; + db.onerror = event => reject(new IndexedDBError(event.target.error)); + + if (event.oldVersion < 1) { + // Records store + const recordsStore = db.createObjectStore("records", { + keyPath: ["_cid", "id"], + }); + // An index to obtain all the records in a collection. + recordsStore.createIndex("cid", "_cid"); + // Last modified field + recordsStore.createIndex("last_modified", ["_cid", "last_modified"]); + // Timestamps store + db.createObjectStore("timestamps", { + keyPath: "cid", + }); + } + if (event.oldVersion < 2) { + // Collections store + db.createObjectStore("collections", { + keyPath: "cid", + }); + } + if (event.oldVersion < 3) { + // Attachment store + db.createObjectStore("attachments", { + keyPath: ["cid", "attachmentId"], + }); + } + }; + request.onerror = event => reject(new IndexedDBError(event.target.error)); + request.onsuccess = event => { + const db = event.target.result; + resolve(db); + }; + }); +} + +function destroyIDB() { + const request = indexedDB.deleteDatabase(DB_NAME); + return new Promise((resolve, reject) => { + request.onerror = event => reject(new IndexedDBError(event.target.error)); + request.onsuccess = () => resolve(); + }); +} + +export var IDBHelpers = { + bulkOperationHelper, + executeIDB, + openIDB, + destroyIDB, + IndexedDBError, + ShutdownError, +}; diff --git a/services/settings/RemoteSettings.worker.mjs b/services/settings/RemoteSettings.worker.mjs new file mode 100644 index 0000000000..66228f226e --- /dev/null +++ b/services/settings/RemoteSettings.worker.mjs @@ -0,0 +1,196 @@ +/* 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/. */ + +/** + * A worker dedicated to Remote Settings. + */ + +// These files are imported into the worker scope, and are not shared singletons +// with the main thread. +/* eslint-disable mozilla/reject-import-system-module-from-non-system */ +import { CanonicalJSON } from "resource://gre/modules/CanonicalJSON.sys.mjs"; +import { IDBHelpers } from "resource://services-settings/IDBHelpers.sys.mjs"; +import { SharedUtils } from "resource://services-settings/SharedUtils.sys.mjs"; +import { jsesc } from "resource://gre/modules/third_party/jsesc/jsesc.mjs"; +/* eslint-enable mozilla/reject-import-system-module-from-non-system */ + +const IDB_RECORDS_STORE = "records"; +const IDB_TIMESTAMPS_STORE = "timestamps"; + +let gShutdown = false; + +const Agent = { + /** + * Return the canonical JSON serialization of the specified records. + * It has to match what is done on the server (See Kinto/kinto-signer). + * + * @param {Array} records + * @param {String} timestamp + * @returns {String} + */ + async canonicalStringify(records, timestamp) { + // Sort list by record id. + let allRecords = records.sort((a, b) => { + if (a.id < b.id) { + return -1; + } + return a.id > b.id ? 1 : 0; + }); + // All existing records are replaced by the version from the server + // and deleted records are removed. + for (let i = 0; i < allRecords.length /* no increment! */; ) { + const rec = allRecords[i]; + const next = allRecords[i + 1]; + if ((next && rec.id == next.id) || rec.deleted) { + allRecords.splice(i, 1); // remove local record + } else { + i++; + } + } + const toSerialize = { + last_modified: "" + timestamp, + data: allRecords, + }; + return CanonicalJSON.stringify(toSerialize, jsesc); + }, + + /** + * If present, import the JSON file into the Remote Settings IndexedDB + * for the specified bucket and collection. + * (eg. blocklists/certificates, main/onboarding) + * @param {String} bucket + * @param {String} collection + * @returns {int} Number of records loaded from dump or -1 if no dump found. + */ + async importJSONDump(bucket, collection) { + const { data: records, timestamp } = await SharedUtils.loadJSONDump( + bucket, + collection + ); + if (records === null) { + // Return -1 if file is missing. + return -1; + } + if (gShutdown) { + throw new Error("Can't import when we've started shutting down."); + } + await importDumpIDB(bucket, collection, records, timestamp); + return records.length; + }, + + /** + * Check that the specified file matches the expected size and SHA-256 hash. + * @param {String} fileUrl file URL to read from + * @param {Number} size expected file size + * @param {String} size expected file SHA-256 as hex string + * @returns {boolean} + */ + async checkFileHash(fileUrl, size, hash) { + let resp; + try { + resp = await fetch(fileUrl); + } catch (e) { + // File does not exist. + return false; + } + const buffer = await resp.arrayBuffer(); + return SharedUtils.checkContentHash(buffer, size, hash); + }, + + async prepareShutdown() { + gShutdown = true; + // Ensure we can iterate and abort (which may delete items) by cloning + // the list. + let transactions = Array.from(gPendingTransactions); + for (let transaction of transactions) { + try { + transaction.abort(); + } catch (ex) { + // We can hit this case if the transaction has finished but + // we haven't heard about it yet. + } + } + }, + + _test_only_import(bucket, collection, records, timestamp) { + return importDumpIDB(bucket, collection, records, timestamp); + }, +}; + +/** + * Wrap worker invocations in order to return the `callbackId` along + * the result. This will allow to transform the worker invocations + * into promises in `RemoteSettingsWorker.sys.mjs`. + */ +self.onmessage = event => { + const { callbackId, method, args = [] } = event.data; + Agent[method](...args) + .then(result => { + self.postMessage({ callbackId, result }); + }) + .catch(error => { + console.log(`RemoteSettingsWorker error: ${error}`); + self.postMessage({ callbackId, error: "" + error }); + }); +}; + +let gPendingTransactions = new Set(); + +/** + * Import the records into the Remote Settings Chrome IndexedDB. + * + * Note: This duplicates some logics from `kinto-offline-client.js`. + * + * @param {String} bucket + * @param {String} collection + * @param {Array} records + * @param {Number} timestamp + */ +async function importDumpIDB(bucket, collection, records, timestamp) { + // Open the DB. It will exist since if we are running this, it means + // we already tried to read the timestamp in `remote-settings.sys.mjs` + const db = await IDBHelpers.openIDB(false /* do not allow upgrades */); + + // try...finally to ensure we always close the db. + try { + if (gShutdown) { + throw new Error("Can't import when we've started shutting down."); + } + + // Each entry of the dump will be stored in the records store. + // They are indexed by `_cid`. + const cid = bucket + "/" + collection; + // We can just modify the items in-place, as we got them from SharedUtils.loadJSONDump(). + records.forEach(item => { + item._cid = cid; + }); + // Store the collection timestamp. + let { transaction, promise } = IDBHelpers.executeIDB( + db, + [IDB_RECORDS_STORE, IDB_TIMESTAMPS_STORE], + "readwrite", + ([recordsStore, timestampStore], rejectTransaction) => { + // Wipe before loading + recordsStore.delete(IDBKeyRange.bound([cid], [cid, []], false, true)); + IDBHelpers.bulkOperationHelper( + recordsStore, + { + reject: rejectTransaction, + completion() { + timestampStore.put({ cid, value: timestamp }); + }, + }, + "put", + records + ); + } + ); + gPendingTransactions.add(transaction); + promise = promise.finally(() => gPendingTransactions.delete(transaction)); + await promise; + } finally { + // Close now that we're done. + db.close(); + } +} diff --git a/services/settings/RemoteSettingsClient.sys.mjs b/services/settings/RemoteSettingsClient.sys.mjs new file mode 100644 index 0000000000..7e95b9baab --- /dev/null +++ b/services/settings/RemoteSettingsClient.sys.mjs @@ -0,0 +1,1281 @@ +/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +import { Downloader } from "resource://services-settings/Attachments.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ClientEnvironmentBase: + "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs", + Database: "resource://services-settings/Database.sys.mjs", + IDBHelpers: "resource://services-settings/IDBHelpers.sys.mjs", + KintoHttpClient: "resource://services-common/kinto-http-client.sys.mjs", + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + RemoteSettingsWorker: + "resource://services-settings/RemoteSettingsWorker.sys.mjs", + SharedUtils: "resource://services-settings/SharedUtils.sys.mjs", + UptakeTelemetry: "resource://services-common/uptake-telemetry.sys.mjs", + Utils: "resource://services-settings/Utils.sys.mjs", +}); + +const TELEMETRY_COMPONENT = "remotesettings"; + +ChromeUtils.defineLazyGetter(lazy, "console", () => lazy.Utils.log); + +/** + * cacheProxy returns an object Proxy that will memoize properties of the target. + * @param {Object} target the object to wrap. + * @returns {Proxy} + */ +function cacheProxy(target) { + const cache = new Map(); + return new Proxy(target, { + get(target, prop, receiver) { + if (!cache.has(prop)) { + cache.set(prop, target[prop]); + } + return cache.get(prop); + }, + }); +} + +/** + * Minimalist event emitter. + * + * Note: we don't use `toolkit/modules/EventEmitter` because **we want** to throw + * an error when a listener fails to execute. + */ +class EventEmitter { + constructor(events) { + this._listeners = new Map(); + for (const event of events) { + this._listeners.set(event, []); + } + } + + /** + * Event emitter: will execute the registered listeners in the order and + * sequentially. + * + * @param {string} event the event name + * @param {Object} payload the event payload to call the listeners with + */ + async emit(event, payload) { + const callbacks = this._listeners.get(event); + let lastError; + for (const cb of callbacks) { + try { + await cb(payload); + } catch (e) { + lastError = e; + } + } + if (lastError) { + throw lastError; + } + } + + hasListeners(event) { + return this._listeners.has(event) && !!this._listeners.get(event).length; + } + + on(event, callback) { + if (!this._listeners.has(event)) { + throw new Error(`Unknown event type ${event}`); + } + this._listeners.get(event).push(callback); + } + + off(event, callback) { + if (!this._listeners.has(event)) { + throw new Error(`Unknown event type ${event}`); + } + const callbacks = this._listeners.get(event); + const i = callbacks.indexOf(callback); + if (i < 0) { + throw new Error(`Unknown callback`); + } else { + callbacks.splice(i, 1); + } + } +} + +class APIError extends Error {} + +class NetworkError extends APIError { + constructor(e) { + super(`Network error: ${e}`, { cause: e }); + this.name = "NetworkError"; + } +} + +class NetworkOfflineError extends APIError { + constructor() { + super("Network is offline"); + this.name = "NetworkOfflineError"; + } +} + +class ServerContentParseError extends APIError { + constructor(e) { + super(`Cannot parse server content: ${e}`, { cause: e }); + this.name = "ServerContentParseError"; + } +} + +class BackendError extends APIError { + constructor(e) { + super(`Backend error: ${e}`, { cause: e }); + this.name = "BackendError"; + } +} + +class BackoffError extends APIError { + constructor(e) { + super(`Server backoff: ${e}`, { cause: e }); + this.name = "BackoffError"; + } +} + +class TimeoutError extends APIError { + constructor(e) { + super(`API timeout: ${e}`, { cause: e }); + this.name = "TimeoutError"; + } +} + +class StorageError extends Error { + constructor(e) { + super(`Storage error: ${e}`, { cause: e }); + this.name = "StorageError"; + } +} + +class InvalidSignatureError extends Error { + constructor(cid, x5u) { + let message = `Invalid content signature (${cid})`; + if (x5u) { + const chain = x5u.split("/").pop(); + message += ` using '${chain}'`; + } + super(message); + this.name = "InvalidSignatureError"; + } +} + +class MissingSignatureError extends InvalidSignatureError { + constructor(cid) { + super(cid); + this.message = `Missing signature (${cid})`; + this.name = "MissingSignatureError"; + } +} + +class CorruptedDataError extends InvalidSignatureError { + constructor(cid) { + super(cid); + this.message = `Corrupted local data (${cid})`; + this.name = "CorruptedDataError"; + } +} + +class UnknownCollectionError extends Error { + constructor(cid) { + super(`Unknown Collection "${cid}"`); + this.name = "UnknownCollectionError"; + } +} + +class AttachmentDownloader extends Downloader { + constructor(client) { + super(client.bucketName, client.collectionName); + this._client = client; + } + + get cacheImpl() { + const cacheImpl = { + get: async attachmentId => { + return this._client.db.getAttachment(attachmentId); + }, + set: async (attachmentId, attachment) => { + return this._client.db.saveAttachment(attachmentId, attachment); + }, + delete: async attachmentId => { + return this._client.db.saveAttachment(attachmentId, null); + }, + prune: async excludeIds => { + return this._client.db.pruneAttachments(excludeIds); + }, + }; + Object.defineProperty(this, "cacheImpl", { value: cacheImpl }); + return cacheImpl; + } + + /** + * Download attachment and report Telemetry on failure. + * + * @see Downloader.download + */ + async download(record, options) { + try { + // Explicitly await here to ensure we catch a network error. + return await super.download(record, options); + } catch (err) { + // Report download error. + let status = lazy.UptakeTelemetry.STATUS.DOWNLOAD_ERROR; + if (lazy.Utils.isOffline) { + status = lazy.UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR; + } else if (/NetworkError/.test(err.message)) { + status = lazy.UptakeTelemetry.STATUS.NETWORK_ERROR; + } + // If the file failed to be downloaded, report it as such in Telemetry. + await lazy.UptakeTelemetry.report(TELEMETRY_COMPONENT, status, { + source: this._client.identifier, + }); + throw err; + } + } + + /** + * Delete all downloaded records attachments. + * + * Note: the list of attachments to be deleted is based on the + * current list of records. + */ + async deleteAll() { + let allRecords = await this._client.db.list(); + return Promise.all( + allRecords + .filter(r => !!r.attachment) + .map(r => + Promise.all([this.deleteDownloaded(r), this.deleteFromDisk(r)]) + ) + ); + } +} + +export class RemoteSettingsClient extends EventEmitter { + static get APIError() { + return APIError; + } + static get NetworkError() { + return NetworkError; + } + static get NetworkOfflineError() { + return NetworkOfflineError; + } + static get ServerContentParseError() { + return ServerContentParseError; + } + static get BackendError() { + return BackendError; + } + static get BackoffError() { + return BackoffError; + } + static get TimeoutError() { + return TimeoutError; + } + static get StorageError() { + return StorageError; + } + static get InvalidSignatureError() { + return InvalidSignatureError; + } + static get MissingSignatureError() { + return MissingSignatureError; + } + static get CorruptedDataError() { + return CorruptedDataError; + } + static get UnknownCollectionError() { + return UnknownCollectionError; + } + + constructor( + collectionName, + { + bucketName = AppConstants.REMOTE_SETTINGS_DEFAULT_BUCKET, + signerName, + filterFunc, + localFields = [], + keepAttachmentsIds = [], + lastCheckTimePref, + } = {} + ) { + // Remote Settings cannot be used in child processes (no access to disk, + // easily killed, isolated observer notifications etc.). + // Since our goal here is to prevent consumers to instantiate while developing their + // feature, throwing in Nightly only is enough, and prevents unexpected crashes + // in release or beta. + if ( + !AppConstants.RELEASE_OR_BETA && + Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT + ) { + throw new Error( + "Cannot instantiate Remote Settings client in child processes." + ); + } + + super(["sync"]); // emitted events + + this.collectionName = collectionName; + // Client is constructed with the raw bucket name (eg. "main", "security-state", "blocklist") + // The `bucketName` will contain the `-preview` suffix if the preview mode is enabled. + this.bucketName = lazy.Utils.actualBucketName(bucketName); + this.signerName = signerName; + this.filterFunc = filterFunc; + this.localFields = localFields; + this.keepAttachmentsIds = keepAttachmentsIds; + this._lastCheckTimePref = lastCheckTimePref; + this._verifier = null; + this._syncRunning = false; + + // This attribute allows signature verification to be disabled, when running tests + // or when pulling data from a dev server. + this.verifySignature = AppConstants.REMOTE_SETTINGS_VERIFY_SIGNATURE; + + ChromeUtils.defineLazyGetter( + this, + "db", + () => new lazy.Database(this.identifier) + ); + + ChromeUtils.defineLazyGetter( + this, + "attachments", + () => new AttachmentDownloader(this) + ); + } + + /** + * Internal method to refresh the client bucket name after the preview mode + * was toggled. + * + * See `RemoteSettings.enabledPreviewMode()`. + */ + refreshBucketName() { + this.bucketName = lazy.Utils.actualBucketName(this.bucketName); + this.db.identifier = this.identifier; + } + + get identifier() { + return `${this.bucketName}/${this.collectionName}`; + } + + get lastCheckTimePref() { + return ( + this._lastCheckTimePref || + `services.settings.${this.bucketName}.${this.collectionName}.last_check` + ); + } + + httpClient() { + const api = new lazy.KintoHttpClient(lazy.Utils.SERVER_URL, { + fetchFunc: lazy.Utils.fetch, // Use fetch() wrapper. + }); + return api.bucket(this.bucketName).collection(this.collectionName); + } + + /** + * Retrieve the collection timestamp for the last synchronization. + * This is an opaque and comparable value assigned automatically by + * the server. + * + * @returns {number} + * The timestamp in milliseconds, returns -1 if retrieving + * the timestamp from the kinto collection fails. + */ + async getLastModified() { + let timestamp = -1; + try { + timestamp = await this.db.getLastModified(); + } catch (err) { + lazy.console.warn( + `Error retrieving the getLastModified timestamp from ${this.identifier} RemoteSettingsClient`, + err + ); + } + + return timestamp; + } + + /** + * Lists settings. + * + * @param {Object} options The options object. + * @param {Object} options.filters Filter the results (default: `{}`). + * @param {String} options.order The order to apply (eg. `"-last_modified"`). + * @param {boolean} options.dumpFallback Fallback to dump data if read of local DB fails (default: `true`). + * @param {boolean} options.emptyListFallback Fallback to empty list if no dump data and read of local DB fails (default: `true`). + * @param {boolean} options.loadDumpIfNewer Use dump data if it is newer than local data (default: `true`). + * @param {boolean} options.forceSync Always synchronize from server before returning results (default: `false`). + * @param {boolean} options.syncIfEmpty Synchronize from server if local data is empty (default: `true`). + * @param {boolean} options.verifySignature Verify the signature of the local data (default: `false`). + * @return {Promise} + */ + async get(options = {}) { + const { + filters = {}, + order = "", // not sorted by default. + dumpFallback = true, + emptyListFallback = true, + forceSync = false, + loadDumpIfNewer = true, + syncIfEmpty = true, + } = options; + let { verifySignature = false } = options; + + const hasParallelCall = !!this._importingPromise; + let data; + try { + let lastModified = forceSync ? null : await this.db.getLastModified(); + let hasLocalData = lastModified !== null; + + if (forceSync) { + if (!this._importingPromise) { + this._importingPromise = (async () => { + await this.sync({ sendEvents: false, trigger: "forced" }); + return true; // No need to re-verify signature after sync. + })(); + } + } else if (syncIfEmpty && !hasLocalData) { + // .get() was called before we had the chance to synchronize the local database. + // We'll try to avoid returning an empty list. + if (!this._importingPromise) { + // Prevent parallel loading when .get() is called multiple times. + this._importingPromise = (async () => { + const importedFromDump = lazy.Utils.LOAD_DUMPS + ? await this._importJSONDump() + : -1; + if (importedFromDump < 0) { + // There is no JSON dump to load, force a synchronization from the server. + // We don't want the "sync" event to be sent, since some consumers use `.get()` + // in "sync" callbacks. See Bug 1761953 + lazy.console.debug( + `${this.identifier} Local DB is empty, pull data from server` + ); + await this.sync({ loadDump: false, sendEvents: false }); + } + // Return `true` to indicate we don't need to `verifySignature`, + // since a trusted dump was loaded or a signature verification + // happened during synchronization. + return true; + })(); + } else { + lazy.console.debug(`${this.identifier} Awaiting existing import.`); + } + } else if (hasLocalData && loadDumpIfNewer) { + // Check whether the local data is older than the packaged dump. + // If it is, load the packaged dump (which overwrites the local data). + let lastModifiedDump = await lazy.Utils.getLocalDumpLastModified( + this.bucketName, + this.collectionName + ); + if (lastModified < lastModifiedDump) { + lazy.console.debug( + `${this.identifier} Local DB is stale (${lastModified}), using dump instead (${lastModifiedDump})` + ); + if (!this._importingPromise) { + // As part of importing, any existing data is wiped. + this._importingPromise = (async () => { + const importedFromDump = await this._importJSONDump(); + // Return `true` to skip signature verification if a dump was found. + // The dump can be missing if a collection is listed in the timestamps summary file, + // because its dump is present in the source tree, but the dump was not + // included in the `package-manifest.in` file. (eg. android, thunderbird) + return importedFromDump >= 0; + })(); + } else { + lazy.console.debug(`${this.identifier} Awaiting existing import.`); + } + } + } + + if (this._importingPromise) { + try { + if (await this._importingPromise) { + // No need to verify signature, because either we've just loaded a trusted + // dump (here or in a parallel call), or it was verified during sync. + verifySignature = false; + } + } catch (e) { + if (!hasParallelCall) { + // Sync or load dump failed. Throw. + throw e; + } + // Report error, but continue because there could have been data + // loaded from a parallel call. + console.error(e); + } finally { + // then delete this promise again, as now we should have local data: + delete this._importingPromise; + } + } + + // Read from the local DB. + data = await this.db.list({ filters, order }); + } catch (e) { + // If the local DB cannot be read (for unknown reasons, Bug 1649393) + // or sync failed, we fallback to the packaged data, and filter/sort in memory. + if (!dumpFallback) { + throw e; + } + console.error(e); + let { data } = await lazy.SharedUtils.loadJSONDump( + this.bucketName, + this.collectionName + ); + if (data !== null) { + lazy.console.info(`${this.identifier} falling back to JSON dump`); + } else if (emptyListFallback) { + lazy.console.info( + `${this.identifier} no dump fallback, return empty list` + ); + data = []; + } else { + // Obtaining the records failed, there is no dump, and we don't fallback + // to an empty list. Throw the original error. + throw e; + } + if (!lazy.ObjectUtils.isEmpty(filters)) { + data = data.filter(r => lazy.Utils.filterObject(filters, r)); + } + if (order) { + data = lazy.Utils.sortObjects(order, data); + } + // No need to verify signature on JSON dumps. + // If local DB cannot be read, then we don't even try to do anything, + // we return results early. + return this._filterEntries(data); + } + + lazy.console.debug( + `${this.identifier} ${data.length} records before filtering.` + ); + + if (verifySignature) { + lazy.console.debug( + `${this.identifier} verify signature of local data on read` + ); + const allData = lazy.ObjectUtils.isEmpty(filters) + ? data + : await this.db.list(); + const localRecords = allData.map(r => this._cleanLocalFields(r)); + const timestamp = await this.db.getLastModified(); + let metadata = await this.db.getMetadata(); + if (syncIfEmpty && lazy.ObjectUtils.isEmpty(metadata)) { + // No sync occured yet, may have records from dump but no metadata. + // We don't want the "sync" event to be sent, since some consumers use `.get()` + // in "sync" callbacks. See Bug 1761953 + await this.sync({ loadDump: false, sendEvents: false }); + metadata = await this.db.getMetadata(); + } + // Will throw MissingSignatureError if no metadata and `syncIfEmpty` is false. + await this._validateCollectionSignature( + localRecords, + timestamp, + metadata + ); + } + + // Filter the records based on `this.filterFunc` results. + const final = await this._filterEntries(data); + lazy.console.debug( + `${this.identifier} ${final.length} records after filtering.` + ); + return final; + } + + /** + * Synchronize the local database with the remote server. + * + * @param {Object} options See #maybeSync() options. + */ + async sync(options) { + if (lazy.Utils.shouldSkipRemoteActivityDueToTests) { + return; + } + + // We want to know which timestamp we are expected to obtain in order to leverage + // cache busting. We don't provide ETag because we don't want a 304. + const { changes } = await lazy.Utils.fetchLatestChanges( + lazy.Utils.SERVER_URL, + { + filters: { + collection: this.collectionName, + bucket: this.bucketName, + }, + } + ); + if (changes.length === 0) { + throw new RemoteSettingsClient.UnknownCollectionError(this.identifier); + } + // According to API, there will be one only (fail if not). + const [{ last_modified: expectedTimestamp }] = changes; + + await this.maybeSync(expectedTimestamp, { ...options, trigger: "forced" }); + } + + /** + * Synchronize the local database with the remote server, **only if necessary**. + * + * @param {int} expectedTimestamp the lastModified date (on the server) for the remote collection. + * This will be compared to the local timestamp, and will be used for + * cache busting if local data is out of date. + * @param {Object} options additional advanced options. + * @param {bool} options.loadDump load initial dump from disk on first sync (default: true if server is prod) + * @param {bool} options.sendEvents send `"sync"` events (default: `true`) + * @param {string} options.trigger label to identify what triggered this sync (eg. ``"timer"``, default: `"manual"`) + * @return {Promise} which rejects on sync or process failure. + */ + async maybeSync(expectedTimestamp, options = {}) { + // Should the clients try to load JSON dump? (mainly disabled in tests) + const { + loadDump = lazy.Utils.LOAD_DUMPS, + trigger = "manual", + sendEvents = true, + } = options; + + // Make sure we don't run several synchronizations in parallel, mainly + // in order to avoid race conditions in "sync" events listeners. + if (this._syncRunning) { + lazy.console.warn(`${this.identifier} sync already running`); + return; + } + + // Prevent network requests and IndexedDB calls to be initiated + // during shutdown. + if (Services.startup.shuttingDown) { + lazy.console.warn(`${this.identifier} sync interrupted by shutdown`); + return; + } + + this._syncRunning = true; + + let importedFromDump = []; + const startedAt = new Date(); + let reportStatus = null; + let thrownError = null; + try { + // If network is offline, we can't synchronize. + if (lazy.Utils.isOffline) { + throw new RemoteSettingsClient.NetworkOfflineError(); + } + + // Read last timestamp and local data before sync. + let collectionLastModified = await this.db.getLastModified(); + const allData = await this.db.list(); + // Local data can contain local fields, strip them. + let localRecords = allData.map(r => this._cleanLocalFields(r)); + const localMetadata = await this.db.getMetadata(); + + // If there is no data currently in the collection, attempt to import + // initial data from the application defaults. + // This allows to avoid synchronizing the whole collection content on + // cold start. + if (!collectionLastModified && loadDump) { + try { + const imported = await this._importJSONDump(); + // The worker only returns an integer. List the imported records to build the sync event. + if (imported > 0) { + lazy.console.debug( + `${this.identifier} ${imported} records loaded from JSON dump` + ); + importedFromDump = await this.db.list(); + // Local data is the data loaded from dump. We will need this later + // to compute the sync result. + localRecords = importedFromDump; + } + collectionLastModified = await this.db.getLastModified(); + } catch (e) { + // Report but go-on. + console.error(e); + } + } + let syncResult; + try { + // Is local timestamp up to date with the server? + if (expectedTimestamp == collectionLastModified) { + lazy.console.debug(`${this.identifier} local data is up-to-date`); + reportStatus = lazy.UptakeTelemetry.STATUS.UP_TO_DATE; + + // If the data is up-to-date but don't have metadata (records loaded from dump), + // we fetch them and validate the signature immediately. + if (this.verifySignature && lazy.ObjectUtils.isEmpty(localMetadata)) { + lazy.console.debug(`${this.identifier} pull collection metadata`); + const metadata = await this.httpClient().getData({ + query: { _expected: expectedTimestamp }, + }); + await this.db.importChanges(metadata); + // We don't bother validating the signature if the dump was just loaded. We do + // if the dump was loaded at some other point (eg. from .get()). + if (this.verifySignature && !importedFromDump.length) { + lazy.console.debug( + `${this.identifier} verify signature of local data` + ); + await this._validateCollectionSignature( + localRecords, + collectionLastModified, + metadata + ); + } + } + + // Since the data is up-to-date, if we didn't load any dump then we're done here. + if (!importedFromDump.length) { + return; + } + // Otherwise we want to continue with sending the sync event to notify about the created records. + syncResult = { + current: importedFromDump, + created: importedFromDump, + updated: [], + deleted: [], + }; + } else { + // Local data is either outdated or tampered. + // In both cases we will fetch changes from server, + // and make sure we overwrite local data. + syncResult = await this._importChanges( + localRecords, + collectionLastModified, + localMetadata, + expectedTimestamp + ); + if (sendEvents && this.hasListeners("sync")) { + // If we have listeners for the "sync" event, then compute the lists of changes. + // The records imported from the dump should be considered as "created" for the + // listeners. + const importedById = importedFromDump.reduce((acc, r) => { + acc.set(r.id, r); + return acc; + }, new Map()); + // Deleted records should not appear as created. + syncResult.deleted.forEach(r => importedById.delete(r.id)); + // Records from dump that were updated should appear in their newest form. + syncResult.updated.forEach(u => { + if (importedById.has(u.old.id)) { + importedById.set(u.old.id, u.new); + } + }); + syncResult.created = syncResult.created.concat( + Array.from(importedById.values()) + ); + } + + // When triggered from the daily timer, and if the sync was successful, and once + // all sync listeners have been executed successfully, we prune potential + // obsolete attachments that may have been left in the local cache. + if (trigger == "timer") { + const deleted = await this.attachments.prune( + this.keepAttachmentsIds + ); + if (deleted > 0) { + lazy.console.warn( + `${this.identifier} Pruned ${deleted} obsolete attachments` + ); + } + } + } + } catch (e) { + if (e instanceof InvalidSignatureError) { + // Signature verification failed during synchronization. + reportStatus = + e instanceof CorruptedDataError + ? lazy.UptakeTelemetry.STATUS.CORRUPTION_ERROR + : lazy.UptakeTelemetry.STATUS.SIGNATURE_ERROR; + // If sync fails with a signature error, it's likely that our + // local data has been modified in some way. + // We will attempt to fix this by retrieving the whole + // remote collection. + try { + lazy.console.warn( + `${this.identifier} Signature verified failed. Retry from scratch` + ); + syncResult = await this._importChanges( + localRecords, + collectionLastModified, + localMetadata, + expectedTimestamp, + { retry: true } + ); + } catch (e) { + // If the signature fails again, or if an error occured during wiping out the + // local data, then we report it as a *signature retry* error. + reportStatus = lazy.UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR; + throw e; + } + } else { + // The sync has thrown for other reason than signature verification. + // Obtain a more precise error than original one. + const adjustedError = this._adjustedError(e); + // Default status for errors at this step is SYNC_ERROR. + reportStatus = this._telemetryFromError(adjustedError, { + default: lazy.UptakeTelemetry.STATUS.SYNC_ERROR, + }); + throw adjustedError; + } + } + if (sendEvents) { + // Filter the synchronization results using `filterFunc` (ie. JEXL). + const filteredSyncResult = await this._filterSyncResult(syncResult); + // If every changed entry is filtered, we don't even fire the event. + if (filteredSyncResult) { + try { + await this.emit("sync", { data: filteredSyncResult }); + } catch (e) { + reportStatus = lazy.UptakeTelemetry.STATUS.APPLY_ERROR; + throw e; + } + } else { + lazy.console.info( + `All changes are filtered by JEXL expressions for ${this.identifier}` + ); + } + } + } catch (e) { + thrownError = e; + // Obtain a more precise error than original one. + const adjustedError = this._adjustedError(e); + // If browser is shutting down, then we can report a specific status. + // (eg. IndexedDB will abort transactions) + if (Services.startup.shuttingDown) { + reportStatus = lazy.UptakeTelemetry.STATUS.SHUTDOWN_ERROR; + } + // If no Telemetry status was determined yet (ie. outside sync step), + // then introspect error, default status at this step is UNKNOWN. + else if (reportStatus == null) { + reportStatus = this._telemetryFromError(adjustedError, { + default: lazy.UptakeTelemetry.STATUS.UNKNOWN_ERROR, + }); + } + throw e; + } finally { + const durationMilliseconds = new Date() - startedAt; + // No error was reported, this is a success! + if (reportStatus === null) { + reportStatus = lazy.UptakeTelemetry.STATUS.SUCCESS; + } + // Report success/error status to Telemetry. + let reportArgs = { + source: this.identifier, + trigger, + duration: durationMilliseconds, + }; + // In Bug 1617133, we will try to break down specific errors into + // more precise statuses by reporting the JavaScript error name + // ("TypeError", etc.) to Telemetry on Nightly. + const channel = lazy.UptakeTelemetry.Policy.getChannel(); + if ( + thrownError !== null && + channel == "nightly" && + [ + lazy.UptakeTelemetry.STATUS.SYNC_ERROR, + lazy.UptakeTelemetry.STATUS.CUSTOM_1_ERROR, // IndexedDB. + lazy.UptakeTelemetry.STATUS.UNKNOWN_ERROR, + lazy.UptakeTelemetry.STATUS.SHUTDOWN_ERROR, + ].includes(reportStatus) + ) { + // List of possible error names for IndexedDB: + // https://searchfox.org/mozilla-central/rev/49ed791/dom/base/DOMException.cpp#28-53 + reportArgs = { ...reportArgs, errorName: thrownError.name }; + } + + await lazy.UptakeTelemetry.report( + TELEMETRY_COMPONENT, + reportStatus, + reportArgs + ); + + lazy.console.debug(`${this.identifier} sync status is ${reportStatus}`); + this._syncRunning = false; + } + } + + /** + * Return a more precise error instance, based on the specified + * error and its message. + * @param {Error} e the original error + * @returns {Error} + */ + _adjustedError(e) { + if (/unparseable/.test(e.message)) { + return new RemoteSettingsClient.ServerContentParseError(e); + } + if (/NetworkError/.test(e.message)) { + return new RemoteSettingsClient.NetworkError(e); + } + if (/Timeout/.test(e.message)) { + return new RemoteSettingsClient.TimeoutError(e); + } + if (/HTTP 5??/.test(e.message)) { + return new RemoteSettingsClient.BackendError(e); + } + if (/Backoff/.test(e.message)) { + return new RemoteSettingsClient.BackoffError(e); + } + if ( + // Errors from kinto.js IDB adapter. + e instanceof lazy.IDBHelpers.IndexedDBError || + // Other IndexedDB errors (eg. RemoteSettingsWorker). + /IndexedDB/.test(e.message) + ) { + return new RemoteSettingsClient.StorageError(e); + } + return e; + } + + /** + * Determine the Telemetry uptake status based on the specified + * error. + */ + _telemetryFromError(e, options = { default: null }) { + let reportStatus = options.default; + + if (e instanceof RemoteSettingsClient.NetworkOfflineError) { + reportStatus = lazy.UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR; + } else if (e instanceof lazy.IDBHelpers.ShutdownError) { + reportStatus = lazy.UptakeTelemetry.STATUS.SHUTDOWN_ERROR; + } else if (e instanceof RemoteSettingsClient.ServerContentParseError) { + reportStatus = lazy.UptakeTelemetry.STATUS.PARSE_ERROR; + } else if (e instanceof RemoteSettingsClient.NetworkError) { + reportStatus = lazy.UptakeTelemetry.STATUS.NETWORK_ERROR; + } else if (e instanceof RemoteSettingsClient.TimeoutError) { + reportStatus = lazy.UptakeTelemetry.STATUS.TIMEOUT_ERROR; + } else if (e instanceof RemoteSettingsClient.BackendError) { + reportStatus = lazy.UptakeTelemetry.STATUS.SERVER_ERROR; + } else if (e instanceof RemoteSettingsClient.BackoffError) { + reportStatus = lazy.UptakeTelemetry.STATUS.BACKOFF; + } else if (e instanceof RemoteSettingsClient.StorageError) { + reportStatus = lazy.UptakeTelemetry.STATUS.CUSTOM_1_ERROR; + } + + return reportStatus; + } + + /** + * Import the JSON files from services/settings/dump into the local DB. + */ + async _importJSONDump() { + lazy.console.info(`${this.identifier} try to restore dump`); + const result = await lazy.RemoteSettingsWorker.importJSONDump( + this.bucketName, + this.collectionName + ); + if (result < 0) { + lazy.console.debug(`${this.identifier} no dump available`); + } else { + lazy.console.info( + `${this.identifier} imported ${result} records from dump` + ); + } + return result; + } + + /** + * Fetch the signature info from the collection metadata and verifies that the + * local set of records has the same. + * + * @param {Array} records The list of records to validate. + * @param {int} timestamp The timestamp associated with the list of remote records. + * @param {Object} metadata The collection metadata, that contains the signature payload. + * @returns {Promise} + */ + async _validateCollectionSignature(records, timestamp, metadata) { + if (!metadata?.signature) { + throw new MissingSignatureError(this.identifier); + } + + if (!this._verifier) { + this._verifier = Cc[ + "@mozilla.org/security/contentsignatureverifier;1" + ].createInstance(Ci.nsIContentSignatureVerifier); + } + + // This is a content-signature field from an autograph response. + const { + signature: { x5u, signature }, + } = metadata; + const certChain = await (await lazy.Utils.fetch(x5u)).text(); + // Merge remote records with local ones and serialize as canonical JSON. + const serialized = await lazy.RemoteSettingsWorker.canonicalStringify( + records, + timestamp + ); + + lazy.console.debug(`${this.identifier} verify signature using ${x5u}`); + if ( + !(await this._verifier.asyncVerifyContentSignature( + serialized, + "p384ecdsa=" + signature, + certChain, + this.signerName, + lazy.Utils.CERT_CHAIN_ROOT_IDENTIFIER + )) + ) { + throw new InvalidSignatureError(this.identifier, x5u); + } + } + + /** + * This method is in charge of fetching data from server, applying the diff-based + * changes to the local DB, validating the signature, and computing a synchronization + * result with the list of creation, updates, and deletions. + * + * @param {Array} localRecords Current list of records in local DB. + * @param {int} localTimestamp Current timestamp in local DB. + * @param {Object} localMetadata Current metadata in local DB. + * @param {int} expectedTimestamp Cache busting of collection metadata + * @param {Object} options + * @param {bool} options.retry Whether this method is called in the + * retry situation. + * + * @returns {Promise} the computed sync result. + */ + async _importChanges( + localRecords, + localTimestamp, + localMetadata, + expectedTimestamp, + options = {} + ) { + const { retry = false } = options; + const since = retry || !localTimestamp ? undefined : `"${localTimestamp}"`; + + // Fetch collection metadata and list of changes from server. + lazy.console.debug( + `${this.identifier} Fetch changes from server (expected=${expectedTimestamp}, since=${since})` + ); + const { metadata, remoteTimestamp, remoteRecords } = + await this._fetchChangeset(expectedTimestamp, since); + + // We build a sync result, based on remote changes. + const syncResult = { + current: localRecords, + created: [], + updated: [], + deleted: [], + }; + // If data wasn't changed, return empty sync result. + // This can happen when we update the signature but not the data. + lazy.console.debug( + `${this.identifier} local timestamp: ${localTimestamp}, remote: ${remoteTimestamp}` + ); + if (localTimestamp && remoteTimestamp < localTimestamp) { + return syncResult; + } + + await this.db.importChanges(metadata, remoteTimestamp, remoteRecords, { + clear: retry, + }); + + // Read the new local data, after updating. + const newLocal = await this.db.list(); + const newRecords = newLocal.map(r => this._cleanLocalFields(r)); + // And verify the signature on what is now stored. + if (this.verifySignature) { + try { + await this._validateCollectionSignature( + newRecords, + remoteTimestamp, + metadata + ); + } catch (e) { + lazy.console.error( + `${this.identifier} Signature failed ${retry ? "again" : ""} ${e}` + ); + if (!(e instanceof InvalidSignatureError)) { + // If it failed for any other kind of error (eg. shutdown) + // then give up quickly. + throw e; + } + + // In order to distinguish signature errors that happen + // during sync, from hijacks of local DBs, we will verify + // the signature on the data that we had before syncing. + let localTrustworthy = false; + lazy.console.debug(`${this.identifier} verify data before sync`); + try { + await this._validateCollectionSignature( + localRecords, + localTimestamp, + localMetadata + ); + localTrustworthy = true; + } catch (sigerr) { + if (!(sigerr instanceof InvalidSignatureError)) { + // If it fails for other reason, keep original error and give up. + throw sigerr; + } + lazy.console.debug(`${this.identifier} previous data was invalid`); + } + + if (!localTrustworthy && !retry) { + // Signature failed, clear local DB because it contains + // bad data (local + remote changes). + lazy.console.debug(`${this.identifier} clear local data`); + await this.db.clear(); + // Local data was tampered, throw and it will retry from empty DB. + lazy.console.error(`${this.identifier} local data was corrupted`); + throw new CorruptedDataError(this.identifier); + } else if (retry) { + // We retried already, we will restore the previous local data + // before throwing eventually. + if (localTrustworthy) { + await this.db.importChanges( + localMetadata, + localTimestamp, + localRecords, + { + clear: true, // clear before importing. + } + ); + } else { + // Restore the dump if available (no-op if no dump) + const imported = await this._importJSONDump(); + // _importJSONDump() only clears DB if dump is available, + // therefore do it here! + if (imported < 0) { + await this.db.clear(); + } + } + } + throw e; + } + } else { + lazy.console.warn(`${this.identifier} has signature disabled`); + } + + if (this.hasListeners("sync")) { + // If we have some listeners for the "sync" event, + // Compute the changes, comparing records before and after. + syncResult.current = newRecords; + const oldById = new Map(localRecords.map(e => [e.id, e])); + for (const r of newRecords) { + const old = oldById.get(r.id); + if (old) { + oldById.delete(r.id); + if (r.last_modified != old.last_modified) { + syncResult.updated.push({ old, new: r }); + } + } else { + syncResult.created.push(r); + } + } + syncResult.deleted = syncResult.deleted.concat( + Array.from(oldById.values()) + ); + lazy.console.debug( + `${this.identifier} ${syncResult.created.length} created. ${syncResult.updated.length} updated. ${syncResult.deleted.length} deleted.` + ); + } + + return syncResult; + } + + /** + * Fetch information from changeset endpoint. + * + * @param expectedTimestamp cache busting value + * @param since timestamp of last sync (optional) + */ + async _fetchChangeset(expectedTimestamp, since) { + const client = this.httpClient(); + const { + metadata, + timestamp: remoteTimestamp, + changes: remoteRecords, + } = await client.execute( + { + path: `/buckets/${this.bucketName}/collections/${this.collectionName}/changeset`, + }, + { + query: { + _expected: expectedTimestamp, + _since: since, + }, + } + ); + return { + remoteTimestamp, + metadata, + remoteRecords, + }; + } + + /** + * Use the filter func to filter the lists of changes obtained from synchronization, + * and return them along with the filtered list of local records. + * + * If the filtered lists of changes are all empty, we return null (and thus don't + * bother listing local DB). + * + * @param {Object} syncResult Synchronization result without filtering. + * + * @returns {Promise} the filtered list of local records, plus the filtered + * list of created, updated and deleted records. + */ + async _filterSyncResult(syncResult) { + // Handle the obtained records (ie. apply locally through events). + // Build the event data list. It should be filtered (ie. by application target) + const { + current: allData, + created: allCreated, + updated: allUpdated, + deleted: allDeleted, + } = syncResult; + const [created, deleted, updatedFiltered] = await Promise.all( + [allCreated, allDeleted, allUpdated.map(e => e.new)].map( + this._filterEntries.bind(this) + ) + ); + // For updates, keep entries whose updated form matches the target. + const updatedFilteredIds = new Set(updatedFiltered.map(e => e.id)); + const updated = allUpdated.filter(({ new: { id } }) => + updatedFilteredIds.has(id) + ); + + if (!created.length && !updated.length && !deleted.length) { + return null; + } + // Read local collection of records (also filtered). + const current = await this._filterEntries(allData); + return { created, updated, deleted, current }; + } + + /** + * Filter entries for which calls to `this.filterFunc` returns null. + * + * @param {Array} data + * @returns {Array} + */ + async _filterEntries(data) { + if (!this.filterFunc) { + return data; + } + const environment = cacheProxy(lazy.ClientEnvironmentBase); + const dataPromises = data.map(e => this.filterFunc(e, environment)); + const results = await Promise.all(dataPromises); + return results.filter(Boolean); + } + + /** + * Remove the fields from the specified record + * that are not present on server. + * + * @param {Object} record + */ + _cleanLocalFields(record) { + const keys = ["_status"].concat(this.localFields); + const result = { ...record }; + for (const key of keys) { + delete result[key]; + } + return result; + } +} diff --git a/services/settings/RemoteSettingsComponents.sys.mjs b/services/settings/RemoteSettingsComponents.sys.mjs new file mode 100644 index 0000000000..9f7687514f --- /dev/null +++ b/services/settings/RemoteSettingsComponents.sys.mjs @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); + +export var RemoteSettingsTimer = function () {}; +RemoteSettingsTimer.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]), + classID: Components.ID("{5e756573-234a-49ea-bbe4-59ec7a70657d}"), + contractID: "@mozilla.org/services/settings;1", + + // By default, this timer fires once every 24 hours. See the "services.settings.poll_interval" pref. + notify(timer) { + lazy.RemoteSettings.pollChanges({ trigger: "timer" }).catch(e => + console.error(e) + ); + }, +}; diff --git a/services/settings/RemoteSettingsWorker.sys.mjs b/services/settings/RemoteSettingsWorker.sys.mjs new file mode 100644 index 0000000000..57bfb3f3d5 --- /dev/null +++ b/services/settings/RemoteSettingsWorker.sys.mjs @@ -0,0 +1,233 @@ +/* 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/. */ + +/** + * Interface to a dedicated thread handling for Remote Settings heavy operations. + */ +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gMaxIdleMilliseconds", + "services.settings.worker_idle_max_milliseconds", + 30 * 1000 // Default of 30 seconds. +); + +ChromeUtils.defineESModuleGetters(lazy, { + AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", + SharedUtils: "resource://services-settings/SharedUtils.sys.mjs", +}); + +// Note: we currently only ever construct one instance of Worker. +// If it stops being a singleton, the AsyncShutdown code at the bottom +// of this file, as well as these globals, will need adjusting. +let gShutdown = false; +let gShutdownResolver = null; + +class RemoteSettingsWorkerError extends Error { + constructor(message) { + super(message); + this.name = "RemoteSettingsWorkerError"; + } +} + +class Worker { + constructor(source) { + if (gShutdown) { + console.error("Can't create worker once shutdown has started"); + } + this.source = source; + this.worker = null; + + this.callbacks = new Map(); + this.lastCallbackId = 0; + this.idleTimeoutId = null; + } + + async _execute(method, args = [], options = {}) { + // Check if we're shutting down. + if (gShutdown && method != "prepareShutdown") { + throw new RemoteSettingsWorkerError("Remote Settings has shut down."); + } + // Don't instantiate the worker to shut it down. + if (method == "prepareShutdown" && !this.worker) { + return null; + } + + const { mustComplete = false } = options; + // (Re)instantiate the worker if it was terminated. + if (!this.worker) { + this.worker = new ChromeWorker(this.source, { type: "module" }); + this.worker.onmessage = this._onWorkerMessage.bind(this); + this.worker.onerror = error => { + // Worker crashed. Reject each pending callback. + for (const { reject } of this.callbacks.values()) { + reject(error); + } + this.callbacks.clear(); + // And terminate it. + this.stop(); + }; + } + // New activity: reset the idle timer. + if (this.idleTimeoutId) { + clearTimeout(this.idleTimeoutId); + } + let identifier = method + "-"; + // Include the collection details in the importJSONDump case. + if (identifier == "importJSONDump-") { + identifier += `${args[0]}-${args[1]}-`; + } + return new Promise((resolve, reject) => { + const callbackId = `${identifier}${++this.lastCallbackId}`; + this.callbacks.set(callbackId, { resolve, reject, mustComplete }); + this.worker.postMessage({ callbackId, method, args }); + }); + } + + _onWorkerMessage(event) { + const { callbackId, result, error } = event.data; + // If we're shutting down, we may have already rejected this operation + // and removed its callback from our map: + if (!this.callbacks.has(callbackId)) { + return; + } + const { resolve, reject } = this.callbacks.get(callbackId); + if (error) { + reject(new RemoteSettingsWorkerError(error)); + } else { + resolve(result); + } + this.callbacks.delete(callbackId); + + // Terminate the worker when it's unused for some time. + // But don't terminate it if an operation is pending. + if (!this.callbacks.size) { + if (gShutdown) { + this.stop(); + if (gShutdownResolver) { + gShutdownResolver(); + } + } else { + this.idleTimeoutId = setTimeout(() => { + this.stop(); + }, lazy.gMaxIdleMilliseconds); + } + } + } + + /** + * Called at shutdown to abort anything the worker is doing that isn't + * critical. + */ + _abortCancelableRequests() { + // End all tasks that we can. + const callbackCopy = Array.from(this.callbacks.entries()); + const error = new Error("Shutdown, aborting read-only worker requests."); + for (const [id, { reject, mustComplete }] of callbackCopy) { + if (!mustComplete) { + this.callbacks.delete(id); + reject(error); + } + } + // There might be nothing left now: + if (!this.callbacks.size) { + this.stop(); + if (gShutdownResolver) { + gShutdownResolver(); + } + } + // If there was something left, we'll stop as soon as we get messages from + // those tasks, too. + // Let's hurry them along a bit: + this._execute("prepareShutdown"); + } + + stop() { + this.worker.terminate(); + this.worker = null; + this.idleTimeoutId = null; + } + + async canonicalStringify(localRecords, remoteRecords, timestamp) { + return this._execute("canonicalStringify", [ + localRecords, + remoteRecords, + timestamp, + ]); + } + + async importJSONDump(bucket, collection) { + return this._execute("importJSONDump", [bucket, collection], { + mustComplete: true, + }); + } + + async checkFileHash(filepath, size, hash) { + return this._execute("checkFileHash", [filepath, size, hash]); + } + + async checkContentHash(buffer, size, hash) { + // The implementation does little work on the current thread, so run the + // task on the current thread instead of the worker thread. + return lazy.SharedUtils.checkContentHash(buffer, size, hash); + } +} + +// Now, first add a shutdown blocker. If that fails, we must have +// shut down already. +// We're doing this here rather than in the Worker constructor because in +// principle having just 1 shutdown blocker for the entire file should be +// fine. If we ever start creating more than one Worker instance, this +// code will need adjusting to deal with that. +try { + lazy.AsyncShutdown.profileBeforeChange.addBlocker( + "Remote Settings profile-before-change", + async () => { + // First, indicate we've shut down. + gShutdown = true; + // Then, if we have no worker or no callbacks, we're done. + if ( + !RemoteSettingsWorker.worker || + !RemoteSettingsWorker.callbacks.size + ) { + return null; + } + // Otherwise, there's something left to do. Set up a promise: + let finishedPromise = new Promise(resolve => { + gShutdownResolver = resolve; + }); + + // Try to cancel most of the work: + RemoteSettingsWorker._abortCancelableRequests(); + + // Return a promise that the worker will resolve. + return finishedPromise; + }, + { + fetchState() { + const remainingCallbacks = RemoteSettingsWorker.callbacks; + const details = Array.from(remainingCallbacks.keys()).join(", "); + return `Remaining: ${remainingCallbacks.size} callbacks (${details}).`; + }, + } + ); +} catch (ex) { + console.error( + "Couldn't add shutdown blocker, assuming shutdown has started." + ); + console.error(ex); + // If AsyncShutdown throws, `profileBeforeChange` has already fired. Ignore it + // and mark shutdown. Constructing the worker will report an error and do + // nothing. + gShutdown = true; +} + +export var RemoteSettingsWorker = new Worker( + "resource://services-settings/RemoteSettings.worker.mjs" +); diff --git a/services/settings/SharedUtils.sys.mjs b/services/settings/SharedUtils.sys.mjs new file mode 100644 index 0000000000..1eeaf0bed9 --- /dev/null +++ b/services/settings/SharedUtils.sys.mjs @@ -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/. */ + +/** + * Common logic shared by RemoteSettingsWorker.js (Worker) and the main thread. + */ + +export var SharedUtils = { + /** + * Check that the specified content matches the expected size and SHA-256 hash. + * @param {ArrayBuffer} buffer binary content + * @param {Number} size expected file size + * @param {String} size expected file SHA-256 as hex string + * @returns {boolean} + */ + async checkContentHash(buffer, size, hash) { + const bytes = new Uint8Array(buffer); + // Has expected size? (saves computing hash) + if (bytes.length !== size) { + return false; + } + // Has expected content? + const hashBuffer = await crypto.subtle.digest("SHA-256", bytes); + const hashBytes = new Uint8Array(hashBuffer); + const toHex = b => b.toString(16).padStart(2, "0"); + const hashStr = Array.from(hashBytes, toHex).join(""); + return hashStr == hash; + }, + + /** + * Load (from disk) the JSON file distributed with the release for this collection. + * @param {String} bucket + * @param {String} collection + */ + async loadJSONDump(bucket, collection) { + // When using the preview bucket, we still want to load the main dump. + // But we store it locally in the preview bucket. + const jsonBucket = bucket.replace("-preview", ""); + const fileURI = `resource://app/defaults/settings/${jsonBucket}/${collection}.json`; + let response; + try { + response = await fetch(fileURI); + } catch (e) { + // Return null if file is missing. + return { data: null, timestamp: null }; + } + // Will throw if JSON is invalid. + return response.json(); + }, +}; diff --git a/services/settings/SyncHistory.sys.mjs b/services/settings/SyncHistory.sys.mjs new file mode 100644 index 0000000000..f609eb26f7 --- /dev/null +++ b/services/settings/SyncHistory.sys.mjs @@ -0,0 +1,120 @@ +/* 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, { + KeyValueService: "resource://gre/modules/kvstore.sys.mjs", +}); + +/** + * A helper to keep track of synchronization statuses. + * + * We rely on a different storage backend than for storing Remote Settings data, + * because the eventual goal is to be able to detect `IndexedDB` issues and act + * accordingly. + */ +export class SyncHistory { + // Internal reference to underlying rkv store. + #store; + + /** + * @param {String} source the synchronization source (eg. `"settings-sync"`) + * @param {Object} options + * @param {int} options.size Maximum number of entries per source. + */ + constructor(source, { size } = { size: 100 }) { + this.source = source; + this.size = size; + } + + /** + * Store the synchronization status. The ETag is converted and stored as + * a millisecond epoch timestamp. + * The entries with the oldest timestamps will be deleted to maintain the + * history size under the configured maximum. + * + * @param {String} etag the ETag value from the server (eg. `"1647961052593"`) + * @param {String} status the synchronization status (eg. `"success"`) + * @param {Object} infos optional additional information to keep track of + */ + async store(etag, status, infos = {}) { + const rkv = await this.#init(); + const timestamp = parseInt(etag.replace('"', ""), 10); + if (Number.isNaN(timestamp)) { + throw new Error(`Invalid ETag value ${etag}`); + } + const key = `v1-${this.source}\t${timestamp}`; + const value = { timestamp, status, infos }; + await rkv.put(key, JSON.stringify(value)); + // Trim old entries. + const allEntries = await this.list(); + for (let i = this.size; i < allEntries.length; i++) { + let { timestamp } = allEntries[i]; + await rkv.delete(`v1-${this.source}\t${timestamp}`); + } + } + + /** + * Retrieve the stored history entries for a certain source, sorted by + * timestamp descending. + * + * @returns {Array} a list of objects + */ + async list() { + const rkv = await this.#init(); + const entries = []; + // The "from" and "to" key parameters to nsIKeyValueStore.enumerate() + // are inclusive and exclusive, respectively, and keys are tuples + // of source and datetime joined by a tab (\t), which is character code 9; + // so enumerating ["source", "source\n"), where the line feed (\n) + // is character code 10, enumerates all pairs with the given source. + for (const { value } of await rkv.enumerate( + `v1-${this.source}`, + `v1-${this.source}\n` + )) { + try { + const stored = JSON.parse(value); + entries.push({ ...stored, datetime: new Date(stored.timestamp) }); + } catch (e) { + // Ignore malformed entries. + console.error(e); + } + } + // Sort entries by `timestamp` descending. + entries.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)); + return entries; + } + + /** + * Return the most recent entry. + */ + async last() { + // List is sorted from newer to older. + return (await this.list())[0]; + } + + /** + * Wipe out the **whole** store. + */ + async clear() { + const rkv = await this.#init(); + await rkv.clear(); + } + + /** + * Initialize the rkv store in the user profile. + * + * @returns {Object} the underlying `KeyValueService` instance. + */ + async #init() { + if (!this.#store) { + // Get and cache a handle to the kvstore. + const dir = PathUtils.join(PathUtils.profileDir, "settings"); + await IOUtils.makeDirectory(dir); + this.#store = await lazy.KeyValueService.getOrCreate(dir, "synchistory"); + } + return this.#store; + } +} diff --git a/services/settings/Utils.sys.mjs b/services/settings/Utils.sys.mjs new file mode 100644 index 0000000000..73c83e526b --- /dev/null +++ b/services/settings/Utils.sys.mjs @@ -0,0 +1,504 @@ +/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { ServiceRequest } from "resource://gre/modules/ServiceRequest.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + SharedUtils: "resource://services-settings/SharedUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "CaptivePortalService", + "@mozilla.org/network/captive-portal-service;1", + "nsICaptivePortalService" +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gNetworkLinkService", + "@mozilla.org/network/network-link-service;1", + "nsINetworkLinkService" +); + +// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref. +// See LOG_LEVELS in Console.sys.mjs. Common examples: "all", "debug", "info", +// "warn", "error". +const log = (() => { + const { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + return new ConsoleAPI({ + maxLogLevel: "warn", + maxLogLevelPref: "services.settings.loglevel", + prefix: "services.settings", + }); +})(); + +ChromeUtils.defineLazyGetter(lazy, "isRunningTests", () => { + if (Services.env.get("MOZ_DISABLE_NONLOCAL_CONNECTIONS") === "1") { + // Allow to override the server URL if non-local connections are disabled, + // usually true when running tests. + return true; + } + return false; +}); + +// Overriding the server URL is normally disabled on Beta and Release channels, +// except under some conditions. +ChromeUtils.defineLazyGetter(lazy, "allowServerURLOverride", () => { + if (!AppConstants.RELEASE_OR_BETA) { + // Always allow to override the server URL on Nightly/DevEdition. + return true; + } + + if (lazy.isRunningTests) { + return true; + } + + if (Services.env.get("MOZ_REMOTE_SETTINGS_DEVTOOLS") === "1") { + // Allow to override the server URL when using remote settings devtools. + return true; + } + + if (lazy.gServerURL != AppConstants.REMOTE_SETTINGS_SERVER_URL) { + log.warn("Ignoring preference override of remote settings server"); + log.warn( + "Allow by setting MOZ_REMOTE_SETTINGS_DEVTOOLS=1 in the environment" + ); + } + + return false; +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gServerURL", + "services.settings.server", + AppConstants.REMOTE_SETTINGS_SERVER_URL +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gPreviewEnabled", + "services.settings.preview_enabled", + false +); + +function _isUndefined(value) { + return typeof value === "undefined"; +} + +export var Utils = { + get SERVER_URL() { + return lazy.allowServerURLOverride + ? lazy.gServerURL + : AppConstants.REMOTE_SETTINGS_SERVER_URL; + }, + + CHANGES_PATH: "/buckets/monitor/collections/changes/changeset", + + /** + * Logger instance. + */ + log, + + get shouldSkipRemoteActivityDueToTests() { + return ( + (lazy.isRunningTests || Cu.isInAutomation) && + this.SERVER_URL == "data:,#remote-settings-dummy/v1" + ); + }, + + get CERT_CHAIN_ROOT_IDENTIFIER() { + if (this.SERVER_URL == AppConstants.REMOTE_SETTINGS_SERVER_URL) { + return Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot; + } + if (this.SERVER_URL.includes("allizom.")) { + return Ci.nsIContentSignatureVerifier.ContentSignatureStageRoot; + } + if (this.SERVER_URL.includes("dev.")) { + return Ci.nsIContentSignatureVerifier.ContentSignatureDevRoot; + } + if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { + return Ci.nsIX509CertDB.AppXPCShellRoot; + } + return Ci.nsIContentSignatureVerifier.ContentSignatureLocalRoot; + }, + + get LOAD_DUMPS() { + // Load dumps only if pulling data from the production server, or in tests. + return ( + this.SERVER_URL == AppConstants.REMOTE_SETTINGS_SERVER_URL || + lazy.isRunningTests + ); + }, + + get PREVIEW_MODE() { + // We want to offer the ability to set preview mode via a preference + // for consumers who want to pull from the preview bucket on startup. + if (_isUndefined(this._previewModeEnabled) && lazy.allowServerURLOverride) { + return lazy.gPreviewEnabled; + } + return !!this._previewModeEnabled; + }, + + /** + * Internal method to enable pulling data from preview buckets. + * @param enabled + */ + enablePreviewMode(enabled) { + const bool2str = v => + // eslint-disable-next-line no-nested-ternary + _isUndefined(v) ? "unset" : v ? "enabled" : "disabled"; + this.log.debug( + `Preview mode: ${bool2str(this._previewModeEnabled)} -> ${bool2str( + enabled + )}` + ); + this._previewModeEnabled = enabled; + }, + + /** + * Returns the actual bucket name to be used. When preview mode is enabled, + * this adds the *preview* suffix. + * + * See also `SharedUtils.loadJSONDump()` which strips the preview suffix to identify + * the packaged JSON file. + * + * @param bucketName the client bucket + * @returns the final client bucket depending whether preview mode is enabled. + */ + actualBucketName(bucketName) { + let actual = bucketName.replace("-preview", ""); + if (this.PREVIEW_MODE) { + actual += "-preview"; + } + return actual; + }, + + /** + * Check if network is down. + * + * Note that if this returns false, it does not guarantee + * that network is up. + * + * @return {bool} Whether network is down or not. + */ + get isOffline() { + try { + return ( + Services.io.offline || + lazy.CaptivePortalService.state == + lazy.CaptivePortalService.LOCKED_PORTAL || + !lazy.gNetworkLinkService.isLinkUp + ); + } catch (ex) { + log.warn("Could not determine network status.", ex); + } + return false; + }, + + /** + * A wrapper around `ServiceRequest` that behaves like `fetch()`. + * + * Use this in order to leverage the `beConservative` flag, for + * example to avoid using HTTP3 to fetch critical data. + * + * @param input a resource + * @param init request options + * @returns a Response object + */ + async fetch(input, init = {}) { + return new Promise(function (resolve, reject) { + const request = new ServiceRequest(); + function fallbackOrReject(err) { + if ( + // At most one recursive Utils.fetch call (bypassProxy=false to true). + bypassProxy || + Services.startup.shuttingDown || + Utils.isOffline || + !request.isProxied || + !request.bypassProxyEnabled + ) { + reject(err); + return; + } + ServiceRequest.logProxySource(request.channel, "remote-settings"); + resolve(Utils.fetch(input, { ...init, bypassProxy: true })); + } + + request.onerror = () => + fallbackOrReject(new TypeError("NetworkError: Network request failed")); + request.ontimeout = () => + fallbackOrReject(new TypeError("Timeout: Network request failed")); + request.onabort = () => + fallbackOrReject(new DOMException("Aborted", "AbortError")); + request.onload = () => { + // Parse raw response headers into `Headers` object. + const headers = new Headers(); + const rawHeaders = request.getAllResponseHeaders(); + rawHeaders + .trim() + .split(/[\r\n]+/) + .forEach(line => { + const parts = line.split(": "); + const header = parts.shift(); + const value = parts.join(": "); + headers.set(header, value); + }); + + const responseAttributes = { + status: request.status, + statusText: request.statusText, + url: request.responseURL, + headers, + }; + resolve(new Response(request.response, responseAttributes)); + }; + + const { method = "GET", headers = {}, bypassProxy = false } = init; + + request.open(method, input, { bypassProxy }); + // By default, XMLHttpRequest converts the response based on the + // Content-Type header, or UTF-8 otherwise. This may mangle binary + // responses. Avoid that by requesting the raw bytes. + request.responseType = "arraybuffer"; + + for (const [name, value] of Object.entries(headers)) { + request.setRequestHeader(name, value); + } + + request.send(); + }); + }, + + /** + * Check if local data exist for the specified client. + * + * @param {RemoteSettingsClient} client + * @return {bool} Whether it exists or not. + */ + async hasLocalData(client) { + const timestamp = await client.db.getLastModified(); + return timestamp !== null; + }, + + /** + * Check if we ship a JSON dump for the specified bucket and collection. + * + * @param {String} bucket + * @param {String} collection + * @return {bool} Whether it is present or not. + */ + async hasLocalDump(bucket, collection) { + try { + await fetch( + `resource://app/defaults/settings/${bucket}/${collection}.json`, + { + method: "HEAD", + } + ); + return true; + } catch (e) { + return false; + } + }, + + /** + * Look up the last modification time of the JSON dump. + * + * @param {String} bucket + * @param {String} collection + * @return {int} The last modification time of the dump. -1 if non-existent. + */ + async getLocalDumpLastModified(bucket, collection) { + if (!this._dumpStats) { + if (!this._dumpStatsInitPromise) { + this._dumpStatsInitPromise = (async () => { + try { + let res = await fetch( + "resource://app/defaults/settings/last_modified.json" + ); + this._dumpStats = await res.json(); + } catch (e) { + log.warn(`Failed to load last_modified.json: ${e}`); + this._dumpStats = {}; + } + delete this._dumpStatsInitPromise; + })(); + } + await this._dumpStatsInitPromise; + } + const identifier = `${bucket}/${collection}`; + let lastModified = this._dumpStats[identifier]; + if (lastModified === undefined) { + const { timestamp: dumpTimestamp } = await lazy.SharedUtils.loadJSONDump( + bucket, + collection + ); + // Client recognize -1 as missing dump. + lastModified = dumpTimestamp ?? -1; + this._dumpStats[identifier] = lastModified; + } + return lastModified; + }, + + /** + * Fetch the list of remote collections and their timestamp. + * ``` + * { + * "timestamp": 1486545678, + * "changes":[ + * { + * "host":"kinto-ota.dev.mozaws.net", + * "last_modified":1450717104423, + * "bucket":"blocklists", + * "collection":"certificates" + * }, + * ... + * ], + * "metadata": {} + * } + * ``` + * @param {String} serverUrl The server URL (eg. `https://server.org/v1`) + * @param {int} expectedTimestamp The timestamp that the server is supposed to return. + * We obtained it from the Megaphone notification payload, + * and we use it only for cache busting (Bug 1497159). + * @param {String} lastEtag (optional) The Etag of the latest poll to be matched + * by the server (eg. `"123456789"`). + * @param {Object} filters + */ + async fetchLatestChanges(serverUrl, options = {}) { + const { expectedTimestamp, lastEtag = "", filters = {} } = options; + + let url = serverUrl + Utils.CHANGES_PATH; + const params = { + ...filters, + _expected: expectedTimestamp ?? 0, + }; + if (lastEtag != "") { + params._since = lastEtag; + } + if (params) { + url += + "?" + + Object.entries(params) + .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) + .join("&"); + } + const response = await Utils.fetch(url); + + if (response.status >= 500) { + throw new Error(`Server error ${response.status} ${response.statusText}`); + } + + const is404FromCustomServer = + response.status == 404 && + Services.prefs.prefHasUserValue("services.settings.server"); + + const ct = response.headers.get("Content-Type"); + if (!is404FromCustomServer && (!ct || !ct.includes("application/json"))) { + throw new Error(`Unexpected content-type "${ct}"`); + } + + let payload; + try { + payload = await response.json(); + } catch (e) { + payload = e.message; + } + + if (!payload.hasOwnProperty("changes")) { + // If the server is failing, the JSON response might not contain the + // expected data. For example, real server errors (Bug 1259145) + // or dummy local server for tests (Bug 1481348) + if (!is404FromCustomServer) { + throw new Error( + `Server error ${url} ${response.status} ${ + response.statusText + }: ${JSON.stringify(payload)}` + ); + } + } + + const { changes = [], timestamp } = payload; + + let serverTimeMillis = Date.parse(response.headers.get("Date")); + // Since the response is served via a CDN, the Date header value could have been cached. + const cacheAgeSeconds = response.headers.has("Age") + ? parseInt(response.headers.get("Age"), 10) + : 0; + serverTimeMillis += cacheAgeSeconds * 1000; + + // Age of data (time between publication and now). + const ageSeconds = (serverTimeMillis - timestamp) / 1000; + + // Check if the server asked the clients to back off. + let backoffSeconds; + if (response.headers.has("Backoff")) { + const value = parseInt(response.headers.get("Backoff"), 10); + if (!isNaN(value)) { + backoffSeconds = value; + } + } + + return { + changes, + currentEtag: `"${timestamp}"`, + serverTimeMillis, + backoffSeconds, + ageSeconds, + }; + }, + + /** + * Test if a single object matches all given filters. + * + * @param {Object} filters The filters object. + * @param {Object} entry The object to filter. + * @return {Boolean} + */ + filterObject(filters, entry) { + return Object.entries(filters).every(([filter, value]) => { + if (Array.isArray(value)) { + return value.some(candidate => candidate === entry[filter]); + } else if (typeof value === "object") { + return Utils.filterObject(value, entry[filter]); + } else if (!Object.prototype.hasOwnProperty.call(entry, filter)) { + console.error(`The property ${filter} does not exist`); + return false; + } + return entry[filter] === value; + }); + }, + + /** + * Sorts records in a list according to a given ordering. + * + * @param {String} order The ordering, eg. `-last_modified`. + * @param {Array} list The collection to order. + * @return {Array} + */ + sortObjects(order, list) { + const hasDash = order[0] === "-"; + const field = hasDash ? order.slice(1) : order; + const direction = hasDash ? -1 : 1; + return list.slice().sort((a, b) => { + if (a[field] && _isUndefined(b[field])) { + return direction; + } + if (b[field] && _isUndefined(a[field])) { + return -direction; + } + if (_isUndefined(a[field]) && _isUndefined(b[field])) { + return 0; + } + return a[field] > b[field] ? direction : -direction; + }); + }, +}; diff --git a/services/settings/components.conf b/services/settings/components.conf new file mode 100644 index 0000000000..fbc4df9bfc --- /dev/null +++ b/services/settings/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{5e756573-234a-49ea-bbe4-59ec7a70657d}', + 'contract_ids': ['@mozilla.org/services/settings;1'], + 'esModule': 'resource://services-settings/RemoteSettingsComponents.sys.mjs', + 'constructor': 'RemoteSettingsTimer', + }, +] diff --git a/services/settings/docs/index.rst b/services/settings/docs/index.rst new file mode 100644 index 0000000000..fb9cdc1f49 --- /dev/null +++ b/services/settings/docs/index.rst @@ -0,0 +1,552 @@ +.. _services/remotesettings: + +=============== +Remote Settings +=============== + +The :searchfox:`remote-settings.sys.mjs ` module offers the ability to fetch remote settings that are kept in sync with Mozilla servers. + + +Usage +===== + +The ``get()`` method returns the list of entries for a specific key. Each entry can have arbitrary attributes, and can only be modified on the server. + +.. code-block:: js + + const { RemoteSettings } = ChromeUtils.import("resource://services-settings/remote-settings.sys.mjs"); + + const data = await RemoteSettings("a-key").get(); + + /* + data == [ + {label: "Yahoo", enabled: true, weight: 10, id: "d0782d8d", last_modified: 1522764475905}, + {label: "Google", enabled: true, weight: 20, id: "8883955f", last_modified: 1521539068414}, + {label: "Ecosia", enabled: false, weight: 5, id: "337c865d", last_modified: 1520527480321}, + ] + */ + + for (const entry of data) { + // Do something with entry... + // await InternalAPI.load(entry.id, entry.label, entry.weight); + }); + +.. note:: + The ``id`` and ``last_modified`` (timestamp) attributes are assigned by the server. + + +Empty local database +-------------------- + +On new user profiles or for recently added use-cases, the local database will be empty until a synchronization with the server happens. Synchronizations are managed internally, and can sometimes be triggered minutes after browser starts. + +By default, if ``.get()`` is called before the local database had the chance to be synchronized, and if no initial data was provided (:ref:`see below `), then the settings will be pulled from the server in order to avoid returning an empty list. In that case, the first call to ``.get()`` will thus take longer than the following ones. + +This behaviour can be disabled using the ``syncIfEmpty`` option. + +.. important:: + + If the implicit synchronization fails (e.g network is not available) then errors are silent and **an empty list is returned**. :ref:`Uptake Telemetry ` status is sent though. + + +Options +------- + +* ``filters``, ``order``: The list can optionally be filtered or ordered: + + .. code-block:: js + + const subset = await RemoteSettings("a-key").get({ + filters: { + property: "value" + }, + order: "-weight" + }); + +* ``syncIfEmpty``: if ``true`` and no local data is present, then look for a packaged dump to load. If none, then pull records from network. + Set it to ``false`` to skip loading of packaged dump and network activity. Use this only if your use-case can tolerate an empty list until the first synchronization happens (default: ``true``). + + .. code-block:: js + + await RemoteSettings("a-key").get({ syncIfEmpty: false }); + +* ``verifySignature``: verify the content signature of the local data (default: ``false``). + An error is thrown if the local data was altered. This hurts performance, but can be used if your use case needs to be secure from local tampering. + +* ``emptyListFallback``: return an empty list if obtaining the records fails (default: ``true``). + If an error occurs during the reading of local data, or during synchronization, then an empty list is returned. + + +Events +------ + +The ``on()`` function registers handlers to be triggered when events occur. + +The ``"sync"`` event allows to be notified when the remote settings are changed on the server side. Your handler is given an ``event`` object that contains a ``data`` attribute that has information about the changes: + +- ``current``: current list of entries (after changes were applied); +- ``created``, ``updated``, ``deleted``: list of entries that were created/updated/deleted respectively. + +.. code-block:: js + + RemoteSettings("a-key").on("sync", event => { + const { data: { current } } = event; + for (const entry of current) { + // Do something with entry... + // await InternalAPI.reload(entry.id, entry.label, entry.weight); + } + }); + +.. note:: + + Currently, the synchronization of remote settings is triggered via push notifications, and also by its own timer every 24H (see the preference ``services.settings.poll_interval`` ). + + +File attachments +---------------- + +When an entry has a file attached to it, it has an ``attachment`` attribute, which contains the file related information (url, hash, size, mimetype, etc.). + +Remote files are not downloaded automatically. In order to keep attachments in sync, the provided helper can be leveraged like this: + +.. code-block:: js + + const client = RemoteSettings("a-key"); + + client.on("sync", async ({ data: { created, updated, deleted } }) => { + const toDelete = deleted.filter(d => d.attachment); + const toDownload = created + .concat(updated.map(u => u.new)) + .filter(d => d.attachment); + + // Remove local files of deleted records + await Promise.all( + toDelete.map(record => client.attachments.deleteDownloaded(record)) + ); + + // Download new attachments + const fileContents = await Promise.all( + toDownload.map(async record => { + const { buffer } = await client.attachments.download(record); + return buffer; + }); + ); + }); + +The provided helper will: + - fetch the remote binary content + - write the file in the local IndexedDB + - check the file size + - check the content SHA256 hash + - do nothing if the attachment was already present and sound locally. + +.. important:: + + The following aspects are not taken care of (yet! help welcome): + + - check available disk space + - preserve bandwidth + - resume downloads of large files + +.. note:: + + The ``download()`` method supports the following options: + + - ``retries`` (default: ``3``): number of retries on network errors + - ``fallbackToCache`` (default: ``false``): allows callers to fall back to the cached file and record, if the requested record's attachment fails to download. + This enables callers to always have a valid pair of attachment and record, + provided that the attachment has been retrieved at least once. + - ``fallbackToDump`` (default: ``false``): activates a fallback to a dump that has been + packaged with the client, when other ways to load the attachment have failed. + See :ref:`services/packaging-attachments ` for more information. + +.. note:: + + A ``downloadAsBytes()`` method returning an ``ArrayBuffer`` is also available, if writing the attachment locally is not necessary. + + Some ``downloadToDisk()`` and ``deleteFromDisk()`` methods are also available but generally discouraged, since they are prone to leaving extraneous files + in the profile directory (see `Bug 1634127 `_). + + +.. _services/initial-data: + +Initial data +------------ + +It is possible to package a dump of the server records that will be loaded into the local database when no synchronization has happened yet. + +The JSON dump will serve as the default dataset for ``.get()``, instead of doing a round-trip to pull the latest data. It will also reduce the amount of data to be downloaded on the first synchronization. + +#. Place the JSON dump of the server records in the ``services/settings/dumps/main/`` folder. Using this command: + :: + + CID="your-collection" + curl "https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/${CID}/changeset?_expected=0" | jq '{"data": .changes, "timestamp": .timestamp}' > services/settings/dumps/main/${CID}.json`` + +#. Add the filename to the ``FINAL_TARGET_FILES`` list in ``services/settings/dumps/main/moz.build`` +#. Add the filename to the ``[browser]`` section of ``mobile/android/installer/package-manifest.in`` IF the file should be bundled with Android. + +Now, when ``RemoteSettings("some-key").get()`` is called from an empty profile, the ``some-key.json`` file is going to be loaded before the results are returned. + +JSON dumps in the tree are periodically updated by ``taskcluster/docker/periodic-updates/scripts/periodic_file_updates.sh``. +If your collection's in-tree dump should not be kept up to date by this automation, place the JSON file in ``services/settings/static-dumps/`` instead. + +.. note:: + + The example above uses "main" because that's the default bucket name. + If you have customized the bucket name, use the actual bucket name instead of "main". + +.. _services/packaging-attachments: + +Packaging attachments +~~~~~~~~~~~~~~~~~~~~~ + +Attachments are not included in the JSON dumps by default. You may choose to package the attachment +with the client, for example if it is important to have the data available at the first startup +without requiring network activity. Or if most users would download the attachment anyway. +Only package attachments if needed, since they increase the file size of the Firefox installer. + +To package an attachment for consumers of the `download()` method: + +#. Select the desired attachment record from the JSON dump of the server records, and place it at + ``services/settings/dumps///.meta.json``. + The ```` defaults to the ``id`` field of the record. If this ``id`` is not fixed, + you must choose a custom ID that can be relied upon as a long-term attachment identifier. See + the notes below for more details. +#. Download the attachment associated with the record, and place it at + ``services/settings/dumps///``. +#. Update ``taskcluster/docker/periodic-updates/scripts/periodic_file_updates.sh`` and add the attachment, + by editing the ``compare_remote_settings_files`` function and describing the attachment. + Unlike JSON dumps, attachments must explicitly be listed in that update script, because the + attachment selection logic needs to be codified in a ``jq`` filter in the script. + For an example, see `Bug 1636158 `_. +#. Register the location of the ``.meta.json`` and ```` in the + ``moz.build`` file of the collection folder, and possibly ``package-manifest.in``, + as described in `the previous section about registering JSON dumps `. + +.. note:: + + ```` is used to derive the file names of the packaged attachment dump, and as the + key for the cache where attachment updates from the network are saved. + The attachment identifier is expected to be fixed across client application updates. + If that expectation cannot be met, the ``attachmentId`` option of the ``download`` method of the + attachment downloader should be used to override the attachment ID with a custom (stable) value. + In order to keep track of the cached attachment, and prevent it from being pruned automatically, + the attachment identifier will have to be explicitly listed in the ``keepAttachmentsIds = []`` + option of the RemoteSettings client constructor. + +.. note:: + + The contents of the ``.meta.json`` file is already contained within the records, but separated + from the main set of records to ensure the availability of the original record with the data, + independently of the packaged or downloaded records. + This file may become optional in a future update, see `Bug 1640059 `_. + + +Synchronization Process +======================= + +The synchronization process consists in pulling the recent changes, merging them with the local data, and verifying the integrity of the result. + +.. image:: synchronization-flow.svg + +.. Source of diagram +.. https://mermaid-js.github.io/mermaid-live-editor/ +.. When using this tool, please remove xlink prefix from attributes in the resulting SVG file. +.. See bug 1481470. +.. +.. graph TD +.. 0[Sync] --> pull; +.. pull[Pull changes] --> merge[Merge with local] +.. merge --> valid{Is signature valid?}; +.. valid -->|Yes| Success; +.. valid -->|No| retried{Retried?}; +.. retried --> |Yes| validchanges{Valid without changes?}; +.. retried --> |No| valid2{Valid without changes?}; +.. validchanges -->|Yes| restoredata[Restore previous data]; +.. validchanges -->|No| clear[Clear local]; +.. restore --> Failure; +.. valid2 --> |No| clear2[Clear local]; +.. valid2 --> |Yes| Retry; +.. Retry --> |Retry| pull; +.. clear2 --> Retry; +.. clear --> restore[Restore packaged dump]; +.. restoredata --> Failure; +.. style 0 fill:#00ff00; +.. style Success fill:#00ff00; +.. style Failure fill:#ff0000; + +.. important:: + + As shown above, we can end-up in situations where synchronization fails and will leave the local DB in an empty state. + + +Targets and A/B testing +======================= + +In order to deliver settings to subsets of the population, you can set targets on entries (platform, language, channel, version range, preferences values, samples, etc.) when editing records on the server. + +From the client API standpoint, this is completely transparent: the ``.get()`` method — as well as the event data — will always filter the entries on which the target matches. + +.. note:: + + The remote settings targets follow the same approach as the :ref:`Normandy recipe client ` (ie. JEXL filter expressions). + + +.. _services/settings/uptake-telemetry: + +Uptake Telemetry +================ + +Some :ref:`uptake telemetry ` is collected in order to monitor how remote settings are propagated. + +It is submitted to a single :ref:`keyed histogram ` whose id is ``UPTAKE_REMOTE_CONTENT_RESULT_1`` and the keys are prefixed with ``main/`` (eg. ``main/a-key`` in the above example). + + +Create new remote settings +========================== + +Staff members can create new kinds of remote settings, following `this documentation `_. + +It basically consists in: + +#. Choosing a key (eg. ``search-providers``) +#. Assigning collaborators to editors and reviewers groups +#. (*optional*) Define a JSONSchema to validate entries +#. (*optional*) Allow attachments on entries + +And once done: + +#. Create, modify or delete entries and let reviewers approve the changes +#. Wait for Firefox to pick-up the changes for your settings key + + +Global Notifications +==================== + +The polling for changes process sends two notifications that observers can register to: + +* ``remote-settings:changes-poll-start``: Polling for changes is starting. triggered either by the scheduled timer or a push broadcast. +* ``remote-settings:changes-poll-end``: Polling for changes has ended +* ``remote-settings:sync-error``: A synchronization error occurred. Notification subject provides information about the error and affected + collection in the ``wrappedJSObject`` attribute. +* ``remote-settings:broken-sync-error``: Synchronization seems to be consistently failing. Profile is at risk. + +.. code-block:: javascript + + const observer = { + observe(aSubject, aTopic, aData) { + Services.obs.removeObserver(this, "remote-settings:changes-poll-start"); + + const { expectedTimestamp } = JSON.parse(aData); + console.log("Polling started", expectedTimestamp ? "from push broadcast" : "by scheduled trigger"); + }, + }; + Services.obs.addObserver(observer, "remote-settings:changes-poll-start"); + + +Advanced Options +================ + +``localFields``: records fields that remain local +------------------------------------------------- + +During synchronization, the local database is compared with the server data. Any difference will be overwritten by the remote version. + +In some use-cases it's necessary to store some state using extra attributes on records. The ``localFields`` options allows to specify which records field names should be preserved on records during synchronization. + +.. code-block:: javascript + + const client = RemoteSettings("a-collection", { + localFields: [ "userNotified", "userResponse" ], + }); + + +``filterFunc``: custom filtering function +----------------------------------------- + +By default, the entries returned by ``.get()`` are filtered based on the JEXL expression result from the ``filter_expression`` field. The ``filterFunc`` option allows to execute a custom filter (async) function, that should return the record (modified or not) if kept or a falsy value if filtered out. + +.. code-block:: javascript + + const client = RemoteSettings("a-collection", { + filterFunc: (record, environment) => { + const { enabled, ...entry } = record; + return enabled ? entry : null; + } + }); + + +Debugging and manual testing +============================ + +Logging +------- + +In order to enable verbose logging, set the log level preference to ``debug``. + +.. code-block:: javascript + + Services.prefs.setStringPref("services.settings.loglevel", "debug"); + +Remote Settings Dev Tools +------------------------- + +The Remote Settings Dev Tools extension provides some tooling to inspect synchronization statuses, to change the remote server or to switch to *preview* mode in order to sign-off pending changes. `More information on the dedicated repository `_. + +Preview Mode +------------ + +Enable the preview mode in order to preview changes to be reviewed on the server. This can be achieved using the *Remote Settings Dev Tools*, or programmatically with: + +.. code-block:: javascript + + RemoteSettings.enablePreviewMode(true); + +In order to pull preview data **on startup**, or in order to persist it across restarts, set ``services.settings.preview_enabled`` to ``true`` in the profile preferences (ie. ``user.js``). +For release and ESR, for security reasons, you would have to run the application with the ``MOZ_REMOTE_SETTINGS_DEVTOOLS=1`` environment variable for the preference to be taken into account. Note that toggling the preference won't have any effect until restart. + +Trigger a synchronization manually +---------------------------------- + +The synchronization of every known remote settings clients can be triggered manually with ``pollChanges()``: + +.. code-block:: js + + await RemoteSettings.pollChanges() + +In order to ignore last synchronization status during polling for changes, set the ``full`` option: + +.. code-block:: js + + await RemoteSettings.pollChanges({ full: true }) + +The synchronization of a single client can be forced with the ``.sync()`` method: + +.. code-block:: js + + await RemoteSettings("a-key").sync(); + +.. important:: + + The above methods are only relevant during development or debugging and should never be called in production code. + + +Inspect local data +------------------ + +The internal IndexedDB of Remote Settings can be accessed via the Storage Inspector in the `browser toolbox `_. + +For example, the local data of the ``"key"`` collection can be accessed in the ``remote-settings`` database at *Browser Toolbox* > *Storage* > *IndexedDB* > *chrome*, in the ``records`` store. + + +Delete all local data +--------------------- + +All local data, of **every collection**, including downloaded attachments, can be deleted with: + +.. code-block:: js + + await RemoteSettings.clearAll(); + + +Unit Tests +========== + +As a foreword, we would like to underline the fact that your tests should not test Remote Settings itself. Your tests should assume Remote Settings works, and should only run assertions on the integration part. For example, if you see yourself mocking the server responses, your tests may go over their responsibility. + +If your code relies on the ``"sync"`` event, you are likely to be interested in faking this event and make sure your code runs as expected. If it relies on ``.get()``, you will probably want to insert some fake local data. + + +Simulate ``"sync"`` events +-------------------------- + +You can forge a ``payload`` that contains the events attributes as described above, and emit it :) + +.. code-block:: js + + const payload = { + current: [{ id: "abc", age: 43 }], + created: [], + updated: [{ old: { id: "abc", age: 42 }, new: { id: "abc", age: 43 }}], + deleted: [], + }; + + await RemoteSettings("a-key").emit("sync", { data: payload }); + + +Manipulate local data +--------------------- + +A handle on the underlying database can be obtained through the ``.db`` attribute. + +.. code-block:: js + + const db = RemoteSettings("a-key").db; + +And records can be created manually (as if they were synchronized from the server): + +.. code-block:: js + + const record = await db.create({ + id: "a-custom-string-or-uuid", + domain: "website.com", + usernameSelector: "#login-account", + passwordSelector: "#pass-signin", + }); + +If no timestamp is set, any call to ``.get()`` will trigger the load of initial data (JSON dump) if any, or a synchronization will be triggered. To avoid that, store a fake timestamp. We use ``Date.now()`` instead of an arbitrary number, to make sure it's higher than the dump's, and thus prevent its load from the test. + +.. code-block:: js + + await db.importChanges({}, Date.now()); + +In order to bypass the potential target filtering of ``RemoteSettings("key").get()``, the low-level listing of records can be obtained with ``collection.list()``: + +.. code-block:: js + + const { data: subset } = await db.list({ + filters: { + "property": "value" + } + }); + +The local data can be flushed with ``clear()``: + +.. code-block:: js + + await db.clear() + + +Misc +==== + +We host more documentation on https://remote-settings.readthedocs.io/, on how to run a server locally, manage attachments, or use the REST API etc. + +About blocklists +---------------- + +The security settings, as well as addons, plugins, and GFX blocklists were the first use-cases of remote settings, and thus have some specificities. + +For example, they leverage advanced customization options (bucket, content-signature certificate, target filtering etc.). In order to get a reference to these clients, their initialization code must be executed first. + +.. code-block:: js + + const {RemoteSecuritySettings} = ChromeUtils.import("resource://gre/modules/psm/RemoteSecuritySettings.jsm"); + + RemoteSecuritySettings.init(); + + + const {BlocklistPrivate} = ChromeUtils.import("resource://gre/modules/Blocklist.jsm"); + + BlocklistPrivate.ExtensionBlocklistRS._ensureInitialized(); + BlocklistPrivate.PluginBlocklistRS._ensureInitialized(); + BlocklistPrivate.GfxBlocklistRS._ensureInitialized(); + +Then, in order to access a specific client instance, the ``bucketName`` must be specified: + +.. code-block:: js + + const client = RemoteSettings("onecrl", { bucketName: "security-state" }); + +And in the storage inspector, the IndexedDB internal store will be prefixed with ``security-state`` instead of ``main`` (eg. ``security-state/onecrl``). diff --git a/services/settings/docs/synchronization-flow.svg b/services/settings/docs/synchronization-flow.svg new file mode 100644 index 0000000000..f8cd66e42e --- /dev/null +++ b/services/settings/docs/synchronization-flow.svg @@ -0,0 +1,4 @@ + +
Yes
No
Yes
No
Yes
No
No
Yes
Retry
Sync
Pull changes
Merge with local
Is signature valid?
Success
Retried?
Valid without changes?
Valid without changes?
Restore previous data
Clear local
Restore packaged dump
Failure
Clear local
Retry
\ No newline at end of file diff --git a/services/settings/dumps/blocklists/addons-bloomfilters.json b/services/settings/dumps/blocklists/addons-bloomfilters.json new file mode 100644 index 0000000000..b7fadac629 --- /dev/null +++ b/services/settings/dumps/blocklists/addons-bloomfilters.json @@ -0,0 +1,243 @@ +{ + "data": [ + { + "stash": { + "blocked": [ + "{c897d01a-2f32-4d90-b583-0db861a92a4d}:1.3" + ], + "unblocked": [] + }, + "schema": 1710851757588, + "key_format": "{guid}:{version}", + "stash_time": 1710938104519, + "id": "80fa9b68-a5a3-45b1-9e95-0efd98906b8c", + "last_modified": 1710938159550 + }, + { + "stash": { + "blocked": [ + "{d2a9c975-7b89-4227-8baf-72dbaa9bffe2}:1.0.2", + "{d2a9c975-7b89-4227-8baf-72dbaa9bffe2}:1.0.1", + "{d2a9c975-7b89-4227-8baf-72dbaa9bffe2}:1.0.0" + ], + "unblocked": [] + }, + "schema": 1710837932773, + "key_format": "{guid}:{version}", + "stash_time": 1710851704213, + "id": "becac4a1-48bb-4202-99f8-e3d479e6d6cd", + "last_modified": 1710851757468 + }, + { + "stash": { + "blocked": [ + "queapps@dev:2024.3.7.1" + ], + "unblocked": [] + }, + "schema": 1710776572419, + "key_format": "{guid}:{version}", + "stash_time": 1710786904359, + "id": "cc6d970f-1272-403a-9682-ff9885b9a17d", + "last_modified": 1710786957409 + }, + { + "stash": { + "blocked": [ + "{e2780f13-3c0d-4551-8080-c53e88ea7fe2}:1.2" + ], + "unblocked": [] + }, + "schema": 1709717395157, + "key_format": "{guid}:{version}", + "stash_time": 1709836504187, + "id": "07fb40eb-63ea-4e7e-aafa-bbc91270eadd", + "last_modified": 1709836557282 + }, + { + "stash": { + "blocked": [ + "{7bc8e402-0d0f-4daa-b639-12638167b89b}:1.0.0", + "{7ffc9179-b13b-43c7-a629-e912d1bd2d41}:1.0.1", + "{0bfd044e-fd33-43c5-963e-79bc37677ef0}:1.0", + "{9081d218-23b6-4fb0-b0e2-9584cfd9ec67}:1.0.2", + "{8a0f8be7-462c-4ac4-9aaa-e6942ceba2de}:1.0.1", + "{947e6a31-2445-4943-a470-759a4503c677}:1.0.1", + "adblockdefender@ext:1.0.1", + "{0a9351b5-c0a5-4e05-9f95-bb76da94599d}:1.0.1", + "{49b5c131-1f79-4917-b6a7-aa210cd686b0}:1.0.1", + "cinematic-compass@addon:1.0", + "rank-master@addon:1.0.4", + "rank-master@addon:1.0.1", + "{4a25756a-7fc7-44ac-82b3-5afc323e80d0}:1.0.1", + "{7f7ac8a6-57ec-4d91-99dc-8674cf1e367b}:1.0.3", + "rank-tracker@addon:1.0.1", + "{ff21bd20-17e1-48da-ba2b-9d8f1f7b3bed}:1.0.0", + "{947e6a31-2445-4943-a470-759a4503c677}:1.0.0", + "{e3894e65-24c3-4a49-ab0f-a3cd87eedd35}:1.0.1", + "{a1e65c0a-c949-4a85-9b33-e9c6c0f249be}:1.2.1", + "{9081d218-23b6-4fb0-b0e2-9584cfd9ec67}:1.0.0", + "{3e55091d-709e-4b91-a4ac-09bbbf169bff}:1.0.1", + "adlockernet@extension:1.0.0", + "adsoff@addon:1.0.0", + "{c8d534aa-5add-4870-8925-34d39f4ffd2c}:1.0.0", + "adblckhero@ext:1.0.0", + "{40014e6f-37c2-4e84-8b0f-219464496aec}:1.0.0", + "adblockdefender@ext:1.0.0", + "{0a9351b5-c0a5-4e05-9f95-bb76da94599d}:1.0.0", + "{13db8986-7bd0-41b1-a9de-af3541ec5df1}:1.1.1", + "{105b0afa-c723-4347-b945-7cf283dc69da}:1.0.0", + "{13db8986-7bd0-41b1-a9de-af3541ec5df1}:1.0.1", + "cinematic-compass@addon:1.0.3", + "streamflicks@addon:1.0", + "{ff21bd20-17e1-48da-ba2b-9d8f1f7b3bed}:1.0.1", + "{0a9351b5-c0a5-4e05-9f95-bb76da94599d}:1.0.2", + "rank-master@addon:1.0.3", + "{bc426494-9c10-4462-aeb0-9bfba371ac1c}:1.0", + "{2dc2a242-51eb-4c47-a257-2df052e83390}:1.0.1", + "rank-master@addon:1.0.2", + "{a401a93a-c77a-4677-ad91-70ffd90a116b}:1.0", + "musicz@addons:1.0.0", + "{d3a98f6b-f202-4735-ac51-21fb6249af4b}:1.0.1" + ], + "unblocked": [] + }, + "schema": 1708713360872, + "key_format": "{guid}:{version}", + "stash_time": 1709037304339, + "id": "e84f82b8-12d4-44de-b392-47c90d07e20c", + "last_modified": 1709037359688 + }, + { + "stash": { + "blocked": [ + "{baf760b3-2a5b-4e38-878a-139bf10380c2}:1.0.1", + "purebrowse@ext:1.0.1", + "purebrowse@ext:1.0.0", + "purebrowse@ext:1.0.2", + "autoadsoff@addon:1.0.0", + "purebrowse@ext:1.0.3", + "{798674d6-2dd2-43e3-bccc-2080301379bb}:1.0.0" + ], + "unblocked": [] + }, + "schema": 1708699544620, + "key_format": "{guid}:{version}", + "stash_time": 1708713304742, + "id": "82b01b1f-d543-4966-b48d-0fac1fec9b06", + "last_modified": 1708713360745 + }, + { + "stash": { + "blocked": [ + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.36", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.55", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.31", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.70", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.11", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.56", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.77", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.7", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.57", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.51", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.5", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.63", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.62", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.42", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.49", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.25", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.13", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.52", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.76", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.8", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.1", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.35", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.54", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.15", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.21", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.38", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.18", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.64", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.46", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.58", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.53", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.3", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.30", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.71", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.41", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.37", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.66", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.72", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.24", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.10", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.60", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.40", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.12", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.4", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.39", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.22", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.43", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.9", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.23", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.28", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.59", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.48", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.2", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.17", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.69", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.6", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.68", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.20", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.44", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.29", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.19", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.14", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.50", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.61", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.67", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.65", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.16", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.47", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.27", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.73", + "Aternity-WebExt-12.2.3@aternity.com:12.2.3.45" + ], + "unblocked": [] + }, + "schema": 1707904676002, + "key_format": "{guid}:{version}", + "stash_time": 1708000504358, + "id": "be2e380d-39cf-4ccc-8050-b7171f4ecd11", + "last_modified": 1708000561099 + }, + { + "stash": { + "blocked": [ + "{626b88aa-0df6-4213-a87c-8cbfc77a820b}:1.5.9" + ], + "unblocked": [] + }, + "schema": 1707395854884, + "key_format": "{guid}:{version}", + "stash_time": 1707482104424, + "id": "69852edb-d1a7-4383-8906-5fd2fa09de22", + "last_modified": 1707482157549 + }, + { + "schema": 1707395854274, + "attachment": { + "hash": "3cd2ee400a959dc53fd60776cbbe220aa752903b658b262788d2be974f341fc8", + "size": 828655, + "filename": "filter.bin", + "location": "staging/addons-bloomfilters/a7c51df8-f2fb-4367-a086-46dc6c3cf3f8.bin", + "mimetype": "application/octet-stream" + }, + "key_format": "{guid}:{version}", + "attachment_type": "bloomfilter-base", + "generation_time": 1707395704435, + "id": "ebce7331-8908-45c6-bc19-652e3eff840d", + "last_modified": 1707395854769 + } + ], + "timestamp": 1710938159550 +} diff --git a/services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin b/services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin new file mode 100644 index 0000000000..f0273eef6e Binary files /dev/null and b/services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin differ diff --git a/services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin.meta.json b/services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin.meta.json new file mode 100644 index 0000000000..45bcfea771 --- /dev/null +++ b/services/settings/dumps/blocklists/addons-bloomfilters/addons-mlbf.bin.meta.json @@ -0,0 +1 @@ +{"schema":1707395854274,"attachment":{"hash":"3cd2ee400a959dc53fd60776cbbe220aa752903b658b262788d2be974f341fc8","size":828655,"filename":"filter.bin","location":"staging/addons-bloomfilters/a7c51df8-f2fb-4367-a086-46dc6c3cf3f8.bin","mimetype":"application/octet-stream"},"key_format":"{guid}:{version}","attachment_type":"bloomfilter-base","generation_time":1707395704435,"id":"ebce7331-8908-45c6-bc19-652e3eff840d","last_modified":1707395854769} \ No newline at end of file diff --git a/services/settings/dumps/blocklists/addons.json b/services/settings/dumps/blocklists/addons.json new file mode 100644 index 0000000000..ae72c837d5 --- /dev/null +++ b/services/settings/dumps/blocklists/addons.json @@ -0,0 +1,21961 @@ +{ + "data": [ + { + "guid": "{b557cf1f-1399-4beb-b395-71e03757e427}", + "prefs": [], + "schema": 1604933286063, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1676184", + "why": "This add-on violates Mozilla's add-on policies by not providing the necessary information to facilitate a review.", + "name": "super-sync-env" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7b2ff754-e3fd-4c7d-8d6f-b4fd87c71473", + "last_modified": 1604940558744 + }, + { + "guid": "/^((\\{00894eeb-db15-499e-98cc-9d352ee42198\\})|(\\{30e6bd98-94c0-41b7-b085-119effe56451\\})|(\\{5f4b8d5b-f8e8-4caf-86e6-151bbff7dead\\})|(\\{6e97c492-7efa-43f2-8ed8-c20c0275cc99\\})|(\\{a04a7d52-1e5a-4608-9d89-6f3a408b92c1\\})|(\\{afc030c0-261a-478e-8074-6e1aeeb5ec45\\}))$/", + "prefs": [], + "schema": 1604396204131, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1674888", + "why": "These add-ons violate Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Multiple crypto wallets" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "394eae28-5619-45b9-b4bd-b34f58a44015", + "last_modified": 1604503468321 + }, + { + "guid": "{d4e528a9-a6bf-4e35-82b5-cd1ad4b7d331}", + "prefs": [], + "schema": 1604497062898, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1675201", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "TronWallet" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c786e676-ff43-409d-83ea-abf222058e11", + "last_modified": 1604503468313 + }, + { + "guid": "/^((\\{cc8d4d26-b615-4880-863a-586af45dc7e7\\})|(\\{ebd1d9ad-d43b-4105-b1f7-c78a7772b431\\}))$/", + "prefs": [], + "schema": 1604324581859, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1674798", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Coin Add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ca05039e-fd10-4587-979b-d0415c727580", + "last_modified": 1604396203679 + }, + { + "guid": "sourcegraph-for-firefox@sourcegraph.com", + "prefs": [], + "schema": 1604163572646, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1541010", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Sourcegraph for Firefox" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "20.5.20.1516", + "minVersion": "0" + } + ], + "id": "9fde5729-9be6-4955-9627-67742c5ed62a", + "last_modified": 1604324581391 + }, + { + "guid": "{446c7519-9e32-4f9a-b562-447f4421ec9a}", + "prefs": [], + "schema": 1604059105250, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1671618", + "why": "This add-on violates Mozilla's add-on policies by severely circumventing user consent or control.", + "name": "Search add-ons without consent" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "64adb8cf-42d2-4a72-9ad4-df09db0d0d7c", + "last_modified": 1604089130722 + }, + { + "guid": "/^((\\{3d66c55b-ed06-47fe-a823-d49301568ad3\\})|(\\{527d060d-9eaa-4670-8dea-7c152f0b8dcd\\})|(\\{ab64584a-98db-4661-b14a-d6286aed36b2\\})|(\\{b0a0f872-a93b-439d-a783-44690ee6ba4a\\})|(\\{fd299ce1-1602-4490-b659-f45504f9324c\\})|(\\{0362578d-c9c2-4a85-8a37-eab60242c5bb\\})|(\\{2451ecb9-6260-4564-a546-8532f04b587a\\})|(\\{39790485-930b-40a5-8268-69222363ff80\\}))$/", + "prefs": [], + "schema": 1604011389667, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1671618", + "why": "This add-on, up to version 1.0.4, violates Mozilla's add-on policies by severely circumventing user consent or control.", + "name": "Search add-ons without consent" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.4", + "minVersion": "0" + } + ], + "id": "2109c966-8e2f-49df-9df0-3126e5a2bbb7", + "last_modified": 1604089130714 + }, + { + "guid": "/^((\\{827fad0c-8f73-449b-8376-31f69d4e56cf\\})|(\\{a98d27c2-e251-11ea-b905-02429d9dd6dd\\}))$/", + "prefs": [], + "schema": 1603927364672, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1674014", + "why": "These add-ons violate Mozilla's add-on policies by making use of obfuscated code.", + "name": "Go HTTPS" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "acbea144-d931-46b5-9043-44e3ab71d9bf", + "last_modified": 1603931811186 + }, + { + "guid": "/^((\\{328a3fee-69c3-45b8-9281-73b7ca637dd3\\})|(\\{59a08eca-2078-4b2b-b529-3515987bb7ae\\}))$/", + "prefs": [], + "schema": 1603284433270, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1672733", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake coin add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "415d9c2a-ed6f-4729-87b7-664127435a53", + "last_modified": 1603467453042 + }, + { + "guid": "/^((\\{19b0820f-5a37-4f9e-a38e-842f88fe8a15\\})|(\\{30315127-a73b-4f9e-84a2-d04c85f40843\\})|(\\{346c7c17-98da-4351-aa66-989c0cbbe2f6\\})|(\\{3cfffe30-e5f2-4e59-a9ec-e4c74e9f0774\\})|(\\{4f93dedc-ce3b-4c2e-9f74-f75dd14707ca\\})|(\\{73ec9218-d475-4149-a855-228dba2daad3\\})|(\\{7f0b3e60-10c8-4450-a3b6-88d0073238f4\\})|(\\{823da1a3-c9c6-4d52-a569-bf5a0bfd31f0\\})|(\\{86b96feb-ac7b-44a4-b4c6-e506a84b6653\\})|(\\{91f7f1e6-62f8-46c6-834b-75bcdeffa944\\})|(\\{9815d7d2-a195-4efa-afa9-f662dc113bc5\\})|(\\{998c5862-e728-4c11-87e8-24215c8eb605\\})|(\\{9cf8c6c9-098e-44e6-b7f0-d4ef29f52706\\})|(\\{9f683ae4-ed81-4a1f-9564-89aac4fe1c4e\\})|(\\{b27881fc-1af3-4c7b-b803-53b2589ba169\\})|(\\{c90706d8-783c-427a-9c9e-cade21ef2e10\\})|(\\{cfd7c21b-31d7-4265-8981-210ba4f639ea\\})|(\\{d4a427f8-d0e5-42eb-bbe1-589f583d8d23\\})|(\\{e254b1aa-eea6-4323-b425-643b69c80f5b\\}))$/", + "prefs": [], + "schema": 1603395700564, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1672973", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "494eab50-b308-4fb1-8fa4-acd779328567", + "last_modified": 1603467453036 + }, + { + "guid": "/^((blog@link)|(ft@ext\\.xpi)|(hometab@ext)|(open@tab)|(tabpro@ext\\.xpi)|(zen@tab))$/", + "prefs": [], + "schema": 1603456450633, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1672981", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Addons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0d63cebc-6b95-46f0-a998-2d8ada2dea47", + "last_modified": 1603467453031 + }, + { + "guid": "/^((\\{3c22a946-d2ae-45cd-8c47-e3c5bd960fcf\\})|(\\{4495e9ca-17d2-4256-8051-b0a3a376d273\\})|(\\{e7b217ec-d7b0-4e68-a24e-498763c10a46\\}))$/", + "prefs": [], + "schema": 1603458435498, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1673005", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search-hijacking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b216ddd0-009f-4ae1-ab41-d4fec96f2fcc", + "last_modified": 1603467453025 + }, + { + "guid": "/^((\\{27320f04-1c21-4ff1-97da-4f70ba73c37c\\})|(\\{49cd942b-8184-43fd-ab21-b707e16fd954\\})|(\\{5052bd1c-67ec-42d2-bf25-1c0a82fcf144\\})|(\\{879b285f-8675-4039-bce1-bd9500d67983\\})|(\\{9f1e7e3d-f866-45f2-80e8-8e416b10fce2\\})|(\\{a7eb39ab-244d-4073-bd91-64967d433e35\\})|(\\{d23a2e71-cce2-4c1a-8333-25dd5603725a\\})|(\\{d6afd1d6-c0e6-46a6-a976-02aa376872f5\\})|(\\{e69e46e9-fed2-4398-83e6-506631c0eed5\\})|(\\{f99f5e77-53ff-4242-9cc4-cd17b5c75f38\\}))$/", + "prefs": [], + "schema": 1603115672935, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1672114", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Ledger Clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7e2ac67b-1287-4786-a742-26b364887026", + "last_modified": 1603186371797 + }, + { + "guid": "/^((\\{931a969d-88fa-4daf-a309-281d3f9e2604\\})|(\\{dcac30a2-6b81-48af-b02c-50cf66d80acd\\}))$/", + "prefs": [], + "schema": 1602704491067, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1671417", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Coin Add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2cdc427f-3627-45fe-aaf2-adb2d8bf941d", + "last_modified": 1602783995620 + }, + { + "guid": "{4c6939d7-8a28-419b-b300-fc3aaa713e00}", + "prefs": [], + "schema": 1602531694521, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1671127", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Metamask Fake" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b3f49864-78e1-4165-943d-903decebf0ec", + "last_modified": 1602697713590 + }, + { + "guid": "/^((\\{02258e92-9873-4b42-9c94-4321194d4e1d\\})|(\\{0cb5f899-2778-4cc6-b35e-d0cdd6f66d21\\})|(\\{520cb697-d3d0-4ee4-ad77-8830ec5743c0\\})|(\\{53cdb998-5b38-4fb0-96ac-f27e315e4555\\})|(\\{c28e5913-ad85-4bee-8d42-55fc61d36157\\})|(\\{d5e0d76c-8c2f-4e61-b20c-3a20a4278d51\\}))$/", + "prefs": [], + "schema": 1602189331410, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1670237", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "facebookBot" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "472284d1-e68c-4379-be93-f8ba2419742a", + "last_modified": 1602255261265 + }, + { + "guid": "{ff2867d1-4757-4bd2-820c-8332fa19e8b1}", + "prefs": [], + "schema": 1602249386322, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1670239", + "why": "This add-on violates Mozilla's Add-ons No Surprises policy by opening unrelated tabs on startup.", + "name": "allPhoneParnker" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2d23761c-c258-41fd-9a12-4b3b19cb49af", + "last_modified": 1602255261259 + }, + { + "guid": "{5ed32cb9-2517-4657-95bc-4a2b8bd5b66c}", + "prefs": [], + "schema": 1602249714157, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1670249", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "MetaMask Wallet (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "34839545-1520-4dda-b518-78f6ae2efad2", + "last_modified": 1602255261254 + }, + { + "guid": "/^((\\{072bfa23-cfd6-495a-a496-aa80a7348680\\})|(\\{08bd8f24-c413-456a-a3e3-bb66c9003211\\})|(\\{08da5c03-3368-4bef-b5a6-cfe406c1f986\\})|(\\{0ab42cee-b0a0-472a-bc97-00f433e6f26b\\})|(\\{0dbd16ac-07d9-497b-af3e-c3be9acffa6e\\})|(\\{0ddbd50b-e700-4f32-94d4-9623ebf84bde\\})|(\\{0f31d364-724b-4f06-863c-7173bae99f1d\\})|(\\{12a4ae26-9afd-4ed2-aa82-e3b113c5af2b\\})|(\\{12c9a302-9089-49eb-84c9-04b4f518b9a3\\})|(\\{1414b2ab-288f-4b8d-9883-bae6690c5119\\})|(\\{162223de-faf1-4db9-b134-3f1030f470a3\\})|(\\{170b50e1-8905-49c5-8f4a-0fa383a0fad6\\})|(\\{17972267-d32b-4120-b5a5-5b5d5e0c0acc\\})|(\\{1af1881a-0898-45cc-84fb-87e446c369bb\\})|(\\{1b46ff0a-b620-487f-b302-f4a76c30cee7\\})|(\\{1ca1dc51-e2d0-4922-900d-21170716a502\\})|(\\{1d1b6b1c-a893-4f13-a1dd-16644bded07e\\})|(\\{2018eab4-7eaa-4c52-9bdd-bc6ec30af027\\})|(\\{24c0190e-dcd6-4074-836e-7dcc635a7a1a\\})|(\\{252306c3-ab62-494c-a3a5-4adc99983e44\\})|(\\{25b663b0-17b6-4c44-b72c-bbd545bd7f53\\})|(\\{268fd854-0d41-4f21-89ef-60705f936de2\\})|(\\{29a19e2d-cf27-465b-b47c-3020a930dc4a\\})|(\\{2a3af548-93ff-4537-abf9-5dae508cd38a\\})|(\\{2c541d57-f4b3-4dbd-ad96-6a3a305f88fc\\})|(\\{33e09c15-2c23-40ea-9a0f-80d4646ced60\\})|(\\{34bf0a97-bee0-42d4-bd97-d229505f44ee\\})|(\\{426d7bd1-9332-4d05-b44a-c3547fddb7d3\\})|(\\{476ba741-1dd1-498d-9a28-4d99f8348bfb\\})|(\\{4a7e74da-21a1-4306-a97e-5b37016f7c32\\})|(\\{4e86cc61-036f-45a8-991f-928d4c54999b\\})|(\\{52b9d108-872e-499a-8af1-e96a6d944bf0\\})|(\\{53eeba51-50b6-4348-b931-51460f7425cc\\})|(\\{545bf194-8006-4166-9732-375f517e35fb\\})|(\\{582ecf42-ef62-425d-a572-b8c21a08b22e\\})|(\\{5985e7a1-6449-4a8d-87e5-fab210574ad6\\})|(\\{5c015f49-99a1-4896-aaec-40c7f6094a91\\})|(\\{5c1529b9-ebc6-4b9b-aad4-299a676b5c6b\\})|(\\{6029d93b-183c-4874-b139-5fd392d7f60f\\})|(\\{64542449-566e-4fa6-a17f-4d6d288dba2b\\})|(\\{64c6a93c-20dc-4de7-86d5-aa02349f959c\\})|(\\{6da98135-d3c2-4c69-a828-c7172af6b59d\\})|(\\{6f41d41e-47db-472e-8dc9-6c1a2f660bfd\\})|(\\{70862264-783e-4ee7-910e-de2d0884e450\\})|(\\{72dae3b7-ebdc-4156-8ef3-e3b11ca6cfcd\\})|(\\{766718d2-1d1b-48ea-b932-dbb9ccf48498\\})|(\\{78595767-9185-4ae6-858c-f2e8539d733f\\})|(\\{785ce386-1eca-4822-9fc0-640cc4a508ba\\})|(\\{79f46e5c-9c5c-45a2-8d36-8c1cd17f5f79\\})|(\\{7a3113f6-b0cf-4a95-a1d6-58918413fc22\\})|(\\{7d13eed3-2c81-4b11-a70a-82287ce7a39f\\})|(\\{7dbd2526-6727-4b50-aa5d-d73ea86282cc\\})|(\\{8796a946-9814-4517-89c9-fc4c61874449\\})|(\\{88138f10-2e6c-4675-84c7-0f68f9725c42\\})|(\\{88d167c3-0a08-4386-ba52-4f3917da2c8f\\})|(\\{899c1fe8-ae17-4fb3-9c46-62006c0d5d62\\})|(\\{8b0f590f-f229-4625-be1b-81c0106eb273\\})|(\\{8b2f1e97-0df9-4954-860b-22e4f8bb17d9\\})|(\\{8e1bccf6-ac2b-4aaa-b8dc-4146efd97f13\\})|(\\{93fed05d-ea35-46d4-9cf7-72e95dbe28e7\\})|(\\{95c16470-d73a-487e-a1af-2a6aa5dd6e0c\\})|(\\{97ac99c5-a95f-4525-a69e-95c5dcdd2790\\})|(\\{9aea9e5b-a595-421b-a894-41ac52351ae1\\})|(\\{a0376dfe-bc1b-48fb-a875-edf30f7c4fcf\\})|(\\{a0802a5e-733b-4b4c-b8d4-d53d9fad856b\\})|(\\{a0ff6d88-e4ec-4ff4-b362-cbd62defe16e\\})|(\\{a664c3f7-0ae4-481d-8fa3-0a2d1e27d1a0\\})|(\\{a98ec8fa-3a5c-44c7-80e1-b8de2f4b873f\\})|(\\{aace152a-3f14-4535-b521-5a4360895531\\})|(\\{b27d23bb-1116-4797-a56c-99f2c5308850\\})|(\\{b3729dc0-5b58-408c-9d22-1c0eaadbaea3\\})|(\\{b4114612-0f65-470c-8574-0d28ad13df22\\})|(\\{b5113e76-b8ca-41e1-b347-2dc779e3aa80\\})|(\\{b566b404-c1a5-4ff1-84a2-85beb77d9449\\})|(\\{b82fcfb4-b315-41a3-a4b6-d3fb7512a31a\\})|(\\{b93d15a8-03b7-4cbb-9a26-d053246bb219\\})|(\\{b95ae003-d6b9-4d69-926d-268b379492e5\\})|(\\{be90a8c8-22b2-4e71-bd79-61d9c8ab1bec\\})|(\\{bf075676-a801-4014-98e2-a407024b4c9a\\})|(\\{c14a942d-c79d-4cce-bfc6-8d0631e93012\\})|(\\{c1dd47cb-1dfb-441c-91ed-a8dbb12f4339\\})|(\\{c2c0c68c-4543-41cb-be8a-615a809aed31\\})|(\\{c4003f83-334f-47b5-8cf1-46ac290fe6d5\\})|(\\{c4026f1a-92ce-4d71-96a3-6fc55992228a\\})|(\\{c45453cc-21c5-42db-957d-4dc9d404838a\\})|(\\{d255390c-8f21-4831-8dd9-7b1a3a9cf732\\})|(\\{d4773452-076e-4221-be65-e4316e82cc62\\})|(\\{d4ef967f-c59a-465e-950a-97c3dd49b19c\\})|(\\{d5330c50-d27f-46bf-a4be-b0b8635493d4\\})|(\\{d8aeafa1-1ae7-4167-9857-88bf92f912ca\\})|(\\{dc197378-0fdf-4b7c-825d-4fefc59e32a5\\})|(\\{e02e7533-d8ee-45b9-bbdd-b152e4198f9a\\})|(\\{e30f227a-7483-4dfe-a699-4bfaaaaf4cfc\\})|(\\{e4b15bb3-bafa-4008-9d67-81333ee1b25a\\})|(\\{e5840f1b-be29-4414-a9db-aef4880d20c5\\})|(\\{e65b6df1-d21f-410f-a817-6bf2da6fc759\\})|(\\{e9301ed6-99dc-49b3-a3b8-6e059fed0065\\})|(\\{ea4d1685-1ecd-45e0-857e-c1f5416bbd75\\}))$/", + "prefs": [], + "schema": 1602253151136, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1670250", + "why": "These add-ons determined likely to be malicious becuase they are related to confirmed malicious add-ons.", + "name": "Add-ons related to malicious add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "be966a82-a780-4ec5-9877-a5fbd9869068", + "last_modified": 1602255261248 + }, + { + "guid": "/^((\\{efa1c1ab-d827-4041-a5d2-aa7bf99d4915\\})|(\\{f33c604f-9ef6-4d65-9c7b-55bbdc394c88\\})|(\\{f386f96c-8bf5-4b56-b6e0-b4986ef42628\\})|(\\{f598902c-da33-439f-be3d-bd6a2c8d7066\\})|(\\{fa0075c0-c068-4461-bbf2-7d4410afa074\\})|(\\{fac05277-33a8-4843-94f6-5df6979a2f20\\})|(\\{fba54691-cb5b-4902-97a3-088290ef4b7f\\})|(\\{fc79e85a-a9aa-473c-a384-6297a70efcc4\\})|(\\{ffad0d38-b7a2-4d84-942c-73972159df29\\}))$/", + "prefs": [], + "schema": 1602253400574, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1670250", + "why": "These add-ons determined likely to be malicious becuase they are related to confirmed malicious add-ons.", + "name": "Add-ons related to malicious add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e7b803b9-5c91-4fe3-b151-331a30f00e87", + "last_modified": 1602255261242 + }, + { + "guid": "{e0e12577-ed05-484c-8e18-0f6f07ad5c32}", + "prefs": [], + "schema": 1602187122542, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1670101", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Cookie Manager Spoc" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b85f4cb4-5f09-4d98-89d4-8c90136ab83a", + "last_modified": 1602189330975 + }, + { + "guid": "/^((sp@FindingFormsPro)|(sp@WatchTVOnline)|(sp@QuickEmailAccess)|(sp@QuickNEasyRecipes)|(sp@MyFlightFinder)|(sp@BreakingNewsPlus)|(sp@WeatherDiscover)|(sp@MyRecipeFinder)|(sp@AppDiscoveryTools)|(sp@FileConversionNow)|(sp@TemplatesHere)|(sp@SpeedCheckerPlus)|(sp@MapsNDirectionHub)|(sp@FormsHere)|(sp@ConvertPDFHub)|(sp@ProPDFConverters)|(sp@DLOfficeTools)|(sp@MapNDirectionHub)|(sp@MyLoginHub)|(sp@LocalForecast)|(sp@GoGamesHub)|(sp@LiveRadioPro)|(sp@MyOnlineCoupons)|(sp@TrackYourPackages)|(sp@MapsProHub)|(sp@ShipmentTrackers)|(sp@ConvertPDFTo)|(sp@EmailProHub)|(sp@MySweeps)|(sp@MyHoroscope)|(sp@MyVideoConverter)|(sp@EasyClassifieds)|(sp@WatchSportsHere))$/", + "prefs": [], + "schema": 1602081787999, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618984", + "why": "The add-ons violate our no surprises policy", + "name": "Various add-ons violating no surprises" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "2.*", + "minVersion": "0" + } + ], + "id": "3fc5e417-c0bd-4bdd-9226-5047bf0db7d6", + "last_modified": 1602152338028 + }, + { + "guid": "/^((_l4Membersttab03_@www\\.quicktemplatefinder\\.com)|(_l6Membersttab03_@www\\.propdfconverter\\.com)|(_lbMembersttab03_@free\\.worldofnotes\\.com)|(_ozMembersttab03_@free\\.newnotecenter\\.com)|(_ptMembersttab03_@www\\.simplepackagefinder\\.com)|(_pvMembersttab03_@www\\.mapmywayfree\\.com)|(_q1Membersttab03_@free\\.everydaymemo\\.com)|(_q8Membersttab03_@www\\.getformsfree\\.com)|(_qfMembersttab03_@www\\.formfinderfree\\.com)|(_qwMembersttab03_@free\\.shoppingdealslive\\.com)|(_r7Membersttab03_@free\\.onlineformsdirect\\.com)|(_rsMembersttab03_@www\\.freeauctionfinder\\.com)|(_rwMembersttab03_@free\\.getlyricsonline\\.com)|(_s0Membersttab03_@free\\.funnyjokesnow\\.com)|(_s2Membersttab03_@free\\.mybabyboomerhub\\.com)|(_s6Membersttab03_@free\\.celebgossiponline\\.com))$/", + "prefs": [], + "schema": 1602083005272, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1669737", + "why": "The add-ons violate our no surprises and data consent policies. Please upgrade to their latest version (9.x or above) to continue.", + "name": "Misleading search add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "8.*", + "minVersion": "0" + } + ], + "id": "a8f0fe57-e340-4171-a4e7-f79e32034878", + "last_modified": 1602152338022 + }, + { + "guid": "/^((_0dMembersttab03_@www\\.myaudiotab\\.com)|(_12Membersttab03_@free\\.myscrapnook\\.com)|(_29Membersttab03_@www\\.headlinealley\\.com)|(_2jMembersttab03_@www\\.recipekart\\.com)|(_2vMembersttab03_@www\\.dailybibleguide\\.com)|(_39Membersttab03_@www\\.mapsgalaxy\\.com)|(_4jMembersttab03_@www\\.radiorage\\.com)|(_57Membersttab03_@free\\.marineaquariumfree\\.com)|(_5eMembersttab03_@www\\.translationbuddy\\.com)|(_5pMembersttab03_@www\\.metrowhiz\\.com)|(_64Membersttab03_@www\\.televisionfanatic\\.com)|(_65Membersttab03_@download\\.fromdoctopdf\\.com)|(_69Membersttab03_@www\\.packagetracer\\.com)|(_8eMembersttab03_@download\\.howtosimplified\\.com)|(_8iMembersttab03_@download\\.audiotoaudio\\.com)|(_8jMembersttab03_@download\\.myimageconverter\\.com)|(_94Membersttab03_@www\\.motitags\\.com)|(_9pMembersttab03_@free\\.onlinemapfinder\\.com)|(_b7Membersttab03_@free\\.mytransitguide\\.com)|(_beMembersttab03_@free\\.dailylocalguide\\.com)|(_brMembersttab03_@free\\.yourtemplatefinder\\.com)|(_ceMembersttab03_@free\\.easypdfcombine\\.com)|(_d1Membersttab03_@free\\.mysocialshortcut\\.com)|(_dbMembersttab03_@free\\.getformsonline\\.com)|(_dgMembersttab03_@free\\.trackapackage\\.net)|(_diMembersttab03_@www\\.easymaillogin\\.com)|(_dnMembersttab03_@www\\.webmailworld\\.com)|(_dpMembersttab03_@free\\.findyourmaps\\.com)|(_dqMembersttab03_@www\\.downspeedtest\\.com)|(_drMembersttab03_@free\\.downloadinboxnow\\.com)|(_dsMembersttab03_@free\\.internetspeedutility\\.net)|(_dxMembersttab03_@www\\.download-freemaps\\.com)|(_dzMembersttab03_@www\\.pconverter\\.com)|(_e1Membersttab03_@free\\.actionclassicgames\\.com)|(_e5Membersttab03_@www\\.productivityboss\\.com)|(_eaMembersttab03_@www\\.mynewsguide\\.com)|(_ebMembersttab03_@download\\.metrohotspot\\.com)|(_ecMembersttab03_@www\\.instantradioplay\\.com)|(_edMembersttab03_@free\\.myradioaccess\\.com)|(_eeMembersttab03_@download\\.freeradiocast\\.com)|(_efMembersttab03_@free\\.funcustomcreations\\.com)|(_ehMembersttab03_@free\\.dailyrecipeguide\\.com)|(_eiMembersttab03_@www\\.100sofrecipes\\.com)|(_esMembersttab03_@free\\.downloadmanagernow\\.com)|(_euMembersttab03_@free\\.filesendsuite\\.com)|(_ewMembersttab03_@free\\.mergedocsonline\\.com)|(_exMembersttab03_@free\\.easydocmerge\\.com)|(_eyMembersttab03_@free\\.convertdocsnow\\.com)|(_f7Membersttab03_@download\\.smsfrombrowser\\.com)|(_fkMembersttab03_@free\\.getflightinfo\\.com)|(_foMembersttab03_@free\\.flightsearchapp\\.com)|(_frMembersttab03_@free\\.testforspeed\\.com)|(_fsMembersttab03_@free\\.pdfconverterhq\\.com)|(_fwMembersttab03_@free\\.howtosuite\\.com)|(_fxMembersttab03_@free\\.mytransitplanner\\.com)|(_gcMembersttab03_@www\\.weatherblink\\.com)|(_gtMembersttab03_@free\\.gamingwonderland\\.com)|(_hgMembersttab03_@free\\.atozmanuals\\.com)|(_hmMembersttab03_@free\\.easyweatheralert\\.com)|(_hnMembersttab03_@free\\.quickweatheralert\\.com)|(_hoMembersttab03_@free\\.directionsbuilder\\.com)|(_hpMembersttab03_@free\\.easyfileconvert\\.com)|(_hxMembersttab03_@free\\.mergedocsnow\\.com)|(_hyMembersttab03_@free\\.formfetcherpro\\.com)|(_hzMembersttab03_@free\\.televisionace\\.com)|(_i2Membersttab03_@free\\.streamlineddiy\\.com)|(_i4Membersttab03_@free\\.fileconvertonline\\.com)|(_i6Membersttab03_@free\\.dailyproductivitytools\\.com)|(_ijMembersttab03_@www\\.freedirectionsonline\\.com)|(_iuMembersttab03_@free\\.productmanualsfinder\\.com)|(_iwMembersttab03_@free\\.allinonedocs\\.com)|(_j5Membersttab03_@ext\\.ask\\.com)|(_j6Membersttab03_@www\\.freemanualsindex\\.com)|(_j7Membersttab03_@www\\.convertdocsonline\\.com)|(_jaMembersttab03_@www\\.testonlinespeed\\.com)|(_jbMembersttab03_@www\\.onlinemapsearch\\.com)|(_jiMembersttab03_@www\\.searchformsonline\\.com)|(_jjMembersttab03_@www\\.onlineroutefinder\\.com)|(_jnMembersttab03_@www\\.pdfconverttools\\.com)|(_joMembersttab03_@www\\.onlineformfinder\\.com)|(_jpMembersttab03_@www\\.directionswhiz\\.com)|(_jqMembersttab03_@www\\.convertpdfsnow\\.com)|(_k8Membersttab03_@www\\.mymapsexpress\\.com)|(_koMembersttab03_@www\\.quickpdfmerger\\.com)|(_krMembersttab03_@www\\.easyemailsuite\\.com)|(_kwMembersttab03_@www\\.productmanualspro\\.com)|(_kxMembersttab03_@www\\.smarteasymaps\\.com)|(_kyMembersttab03_@www\\.quickflighttracker\\.com)|(_kzMembersttab03_@www\\.productmanualsguide\\.com)|(_l1Membersttab03_@www\\.videoconverterhd\\.com)|(_l3Membersttab03_@www\\.watchmytvshows\\.com))$/", + "prefs": [], + "schema": 1602083056710, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1669737", + "why": "The add-ons violate our no surprises and data consent policies. Please upgrade to their latest version (9.x or above) to continue.", + "name": "Misleading search add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "8.*", + "minVersion": "0" + } + ], + "id": "4262a74b-e514-45b3-a6f3-df869d369d6d", + "last_modified": 1602152338016 + }, + { + "guid": "/^((test@WhatsMySpeed)|(ext@630e3b49-decc-4d5e-8a9e-242df04e8ba1)|(ext@6bbf8f72-c54d-4731-b455-e164685c5914)|(sp@EmailAccessHere)|(sp@WatchTelevision)|(sp@GamerHubPro)|(sp@FreeConverterHub)|(sp@AccessFreeTemplates)|(sp@ViewOnlineRecipes)|(sp@SatelliteandEarthMaps)|(sp@SatelliteandEarthMap)|(sp@PackageTrackingOnline)|(sp@DirectionsandMapsNow)|(sp@ClassifiedsListApp)|(sp@meinemailzentrum)|(web@meinemailzentrum)|(web@meinemailzentrum.com))$/", + "prefs": [], + "schema": 1602081916747, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618984", + "why": "The add-ons violate our no surprises policy", + "name": "Various add-ons violating no surprises" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "076d3556-68cb-4a5d-83cc-e4cca29d3f48", + "last_modified": 1602152338007 + }, + { + "guid": "{9f94439a-292e-4b2f-9144-3a84c35021ec}", + "prefs": [], + "schema": 1601840499677, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1669431", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "old-layout (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "43df375b-832e-49db-934c-ada0e0ea8b64", + "last_modified": 1601975517286 + }, + { + "guid": "/^((\\{05548773-8d3f-4dbc-908c-91c9801723ab\\})|(\\{09fa5118-58e7-4a2c-85aa-3d299aadca2d\\})|(\\{0d83297b-ee0f-4470-b047-ea1eb5161e0a\\})|(\\{19638976-64c1-4bb5-815b-64a3a6a0a767\\})|(\\{196ca185-6582-9e63-8a6f-5d2141598bdc\\})|(\\{2371fa74-b56a-5198-a162-ad1d7db6f8ae\\})|(\\{2a2dae8a-a18c-4e0f-ac31-f5609dab6b99\\})|(\\{3596f810-bf50-47e2-b54a-2128ebdc5179\\})|(\\{4cac7d9e-fa55-4ac8-9a6c-f5797e598584\\})|(\\{598ca189-9e63-43d2-8a2f-5d5141598bdc\\})|(\\{69db3d9a-5273-4147-aabe-299f40869fb4\\})|(\\{83a7ca08-c5a3-43bb-8110-903f7ba60e52\\})|(\\{8da42f72-5f9e-49e0-8821-9e152b472cbc\\})|(\\{8e6a181e-40ce-43ff-9436-c68e42e5aef7\\})|(\\{913af60c-3d60-4400-9cac-e4090de6787c\\})|(\\{9e6f886c-70ea-4d6d-9e10-0961f3dc5093\\})|(\\{a1eac861-2bf1-4cf2-82dd-63ae7cc674eb\\})|(\\{a596e1fb-12ac-bb96-67ea-f11aab7dea9a\\})|(\\{ba316506-0b16-4e72-9edc-145646ac9e2c\\})|(\\{bc684719-7ec8-40b5-ba7f-a971fe3ef269\\}))$/", + "prefs": [], + "schema": 1601645194883, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1668856", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e0349a95-2bfa-4682-872f-e9a4e947e9ca", + "last_modified": 1601656026437 + }, + { + "guid": "/^((\\{536f3cba-58ea-4855-a3a0-b636da88fa3e\\})|(\\{d16f4aff-dabf-42cb-a491-7fe87e957c43\\}))$/", + "prefs": [], + "schema": 1601494897591, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1668468", + "why": "These add-ons violate Mozilla’s add-on policies by collecting ancillary user data.", + "name": "OldLayout (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3e39aaa3-1567-4498-a736-64fb68d956cc", + "last_modified": 1601559385012 + }, + { + "guid": "/^((v-down@addons-firefox)|(\\{667ef836-c20c-4a01-bfa4-af9b3e17299b\\}))$/", + "prefs": [], + "schema": 1600704040590, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1666440", + "why": "These add-ons violate Mozilla's add-on policies by making use of obfuscated code.", + "name": "Video Download" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "efc66283-f419-43fa-b656-361bde591874", + "last_modified": 1600772875317 + }, + { + "guid": "/^((mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified--117756830)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified--1216575438)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified--2088645733)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified--573512243)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-1260029560)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-1519886836)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-1557628910)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-1704698220)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-1939627508)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-1969522249)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-2169863222)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-2340954546)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-2944855766)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-2994830326)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-307914412)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-313645365)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-3184548427)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-3206263432)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-3817143579)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-4214504777)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-4238340145)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-464522215)|(mekpfngodchodemgmkhinohkfjefjeea@chrome-store-foxified-655613955)|(mekpfngodchodemgmkhinohkfjefjeea@chromeStoreFoxified-3562932078)|(\\{4a635075-14e6-4bdb-84ce-ff3954342266\\})|(\\{5f795d1b-9337-4ee6-b98e-04cbcb632223\\})|(\\{6d5d77b0-33e5-4b98-89ef-45a6f35eca8e\\})|(\\{80d07d4a-99a5-49f4-9a0e-8f9bc0f265de\\})|(\\{b154832e-410c-43d7-9d82-d606fa8e225c\\}))$/", + "prefs": [], + "schema": 1600630898542, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1666220", + "why": "These add-ons violate Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons colecting ancillary user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "40322df1-6c76-43f5-add4-d5a083ac53b0", + "last_modified": 1600704040099 + }, + { + "guid": "/^((\\{57c9b8b0-e41b-7c9b-4890-f6ce48908edb\\})|(\\{840ee0e6-ac43-4ace-a0b6-ba8e89ad6e6b\\})|(\\{945a90fa-404b-435e-ad48-537590c31f8c\\})|(\\{a8f161ef-4fcc-4a0c-b919-9eaa3441d45d\\})|(\\{ba97a455-d45e-4585-a59b-dad16ad81280\\})|(\\{bd7264fb-248a-476c-8746-2b51793c04ec\\}))$/", + "prefs": [], + "schema": 1600685522118, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1666225", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7a40eef1-ed69-4053-b148-c87a24ceec69", + "last_modified": 1600704040091 + }, + { + "guid": "{b98a2aba-4044-443f-99c7-4d4137d0454e}", + "prefs": [], + "schema": 1600199325165, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1665345", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Akap Internet security for all" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7b4ca57b-c057-4aa0-bd7e-bd386e08dff3", + "last_modified": 1600284413867 + }, + { + "guid": "pro@page-pref", + "prefs": [], + "schema": 1599766900172, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1664295", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Page Preferences" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "14ff0365-5b4e-4f8b-9c99-d9b6aea9f044", + "last_modified": 1599817087652 + }, + { + "guid": "/^((add-on@bookmark-this)|(extension@tab-modify))$/", + "prefs": [], + "schema": 1599421296553, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1663608", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "60737247-2edc-4c90-9938-eff22f1b06e4", + "last_modified": 1599557323077 + }, + { + "guid": "{aea0443d-e1eb-40c3-937f-7b0986b0f6f2}", + "prefs": [], + "schema": 1598902900782, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1662384", + "why": "This add-on violates Mozilla’s add-on policies by collecting user data without disclosure or consent.", + "name": "Tano Wallet" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6307ddc1-3cb0-4797-882b-71a16615d4ae", + "last_modified": 1598995479473 + }, + { + "guid": "ext@new-custom-tab", + "prefs": [], + "schema": 1598968498912, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1662476", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "New Custom Tab" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ebf000c5-59ae-4aad-a0df-131d79fc94c8", + "last_modified": 1598995479465 + }, + { + "guid": "{859fa04b-4d37-476e-a3b7-2e0306723e0c}", + "prefs": [], + "schema": 1597769413922, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1659777", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "FaWave" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "db4a6dbe-3669-4f6d-9642-2daaa03e2883", + "last_modified": 1597781922506 + }, + { + "guid": "{145d4851-34fb-4fcb-8b0d-9260fa911f54}", + "prefs": [], + "schema": 1597220003337, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1658731", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Addons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f456a9c8-338d-4ca1-9c41-f5c43ab1c6af", + "last_modified": 1597241887211 + }, + { + "guid": "{706b4435-fb0c-471e-be66-0c132e47640d}", + "prefs": [], + "schema": 1596656494784, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1657728", + "why": "This add-on violates Mozilla's add-on policies by tricking users into executing remote code.", + "name": "Netflix (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e21a5638-a89a-4103-b76c-5f0ea797b79e", + "last_modified": 1596746780409 + }, + { + "guid": "/^((\\{1f4742c1-0b89-4fa1-915c-1d732ab813c9\\})|(\\{612697f6-846a-4d55-a707-9f6c8da3f29e\\}))$/", + "prefs": [], + "schema": 1596138097245, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1656512", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fa514474-9450-48fb-a6f4-25e0faa565ff", + "last_modified": 1596206660495 + }, + { + "guid": "/^((\\{5e717dad-4d46-450a-9001-6d87fa2764a6\\})|(\\{a585b03f-b8cf-40ea-aef9-05049dfc1946\\}))$/", + "prefs": [], + "schema": 1595101293613, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1653947", + "why": "These add-ons violate Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9abcd6ed-4678-495d-8c9f-94a6626e3011", + "last_modified": 1595258518841 + }, + { + "guid": "/^((\\{33d203ab-419c-4ef6-a512-4b9e59767000\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767001\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767002\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767960\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767961\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767962\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767963\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767970\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767971\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767972\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767973\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767974\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767975\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767981\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767982\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767983\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767984\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767985\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767990\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767996\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767997\\})|(\\{33d203ab-419c-4ef6-a512-4b9e59767999\\})|(\\{39000c59-8aaa-4d18-b97f-11945e519e11\\})|(\\{39000c59-8aaa-4d18-b97f-11945e519ebd\\}))$/", + "prefs": [], + "schema": 1594909351966, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1653269", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7c502209-c720-4db5-83fe-b444f1e4368b", + "last_modified": 1594981206721 + }, + { + "guid": "/^((\\{5a3eefeb-3e92-474e-81f2-d27e973e93b8\\})|(\\{60a10b62-b9da-46b8-ad61-ae64852afa7f\\})|(\\{807e06e3-2e66-4cb0-b745-28093644c18a\\})|(\\{c0bcd3e2-28c1-4359-bf50-e1cb51691a23\\})|(\\{f9dd990d-5aa9-4b28-9da4-814ea4531776\\})|(\\{fc1aa9a4-1029-4978-854d-9adfcfdeae43\\}))$/", + "prefs": [], + "schema": 1594909465358, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1653286", + "why": "These add-ons violate Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3d5548b1-a192-4ac0-9946-39950b8df310", + "last_modified": 1594981206716 + }, + { + "guid": "/^((adultwebsiteblocker@lipocodes)|(DownloadYoutubeNow@lipocodes\\.com)|(GermanSpellingDictionary@lipocodes)|(websiteblocker@lipocodes)|(website_pdf@lipoapps))$/", + "prefs": [], + "schema": 1594582895881, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1652742", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6e2e1c90-b816-42a7-ae77-203de8ff3fc2", + "last_modified": 1594751132789 + }, + { + "guid": "/^((AmericanEnglishSpellingChecker@lipocodes)|(SpanishSpellingChecker@lipocodes))$/", + "prefs": [], + "schema": 1594733542218, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1652745", + "why": "These add-ons violate Mozilla’s add-on policies by collecting user data without disclosure or consent.", + "name": "Add-ons collecting user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ffeae2b1-cf81-417d-ab86-c857a2402f10", + "last_modified": 1594751132784 + }, + { + "guid": "{d52ea726-df3e-48c5-b931-da90c3ae6dd9}", + "prefs": [], + "schema": 1593783157352, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650585", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Yoroi" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d89bdc3b-bb85-4613-828c-2bab532b1578", + "last_modified": 1594066991967 + }, + { + "guid": "/^((\\{bb627a45-8f46-4555-98da-097cdab69615\\})|(\\{bce4a211-11a1-41a1-9827-50f18d258e11\\}))$/", + "prefs": [], + "schema": 1593899325072, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650836", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e2785820-173c-4057-860a-a6444637cc98", + "last_modified": 1594066991961 + }, + { + "guid": "{e8984717-d520-4aa5-b4ef-1cdd14c109ce}", + "prefs": [], + "schema": 1594053530845, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650840", + "why": "This add-on violates Mozilla’s add-on policies by collecting user data without disclosure or consent.", + "name": "YouTube Downloader Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "82de35ac-b7dd-4e30-bae8-5ea3d5485843", + "last_modified": 1594066991955 + }, + { + "guid": "/^((advanced_srch@ext\\.xpi)|(blooom@ext\\.xpi)|(bookamarks@ext\\.xpi)|(borrower@ext\\.xpi)|(cbutton@ext\\.xpi)|(charger@ext\\.xpi)|(commit@ext\\.xpi)|(dbutton@ext\\.xpi)|(dendi@ext\\.xpi)|(emoji@finder\\.xpi)|(emoji_box@ext\\.xpi)|(emoji_finder@ext\\.xpi)|(git@ext\\.xpi)|(giver@ext\\.xpi)|(header@ext\\.xpi)|(home-ext@main\\.xpi)|(homegy@ext\\.xpi)|(homepage@ext\\.xpi)|(import@page\\.xpi)|(incognitowindow@ww\\.xpi)|(local@ext\\.xpi)|(main@ext\\.xpi)|(my_emoji@ext\\.xpi)|(newtab-for-ff@ext\\.xpi)|(oscoboteam@gmail\\.com)|(pagesetup@v1\\.xpi)|(pdf-tab-converter@ext\\.xpi)|(pdf_converter@ext\\.xpi)|(pdf_editor@ext\\.xpi)|(pdf_online@ext\\.xpi)|(pdf_saver@ext\\.xpi)|(pdf_tab@ext\\.xpi)|(pdf_tab_converter@ext\\.xpi)|(pdf_wizzard@ext\\.xpi)|(preferences@ext\\.xpi)|(private_browsing@ext\\.xpi)|(private_search@ext\\.xpi)|(pull@ext\\.xpi)|(ring@ext\\.xpi)|(sviftjoe@ext\\.xpi)|(windows-tab@ext\\.xpi)|(\\{4de43d5a-f57c-4425-a354-93a9ce2fe861\\})|(\\{a1bb4007-473d-4203-ab7d-7de9ae878279\\})|(\\{bfaabd10-b172-4140-8946-429d7bbb6fb8\\}))$/", + "prefs": [], + "schema": 1594053935800, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650841", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search-hijacking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "21acca65-e7ed-4c0a-b3cd-f05f4df3584e", + "last_modified": 1594066991949 + }, + { + "guid": "/^((fedemmchkhdelojjjgdkopmplmjiengg@chrome-store-foxified--823560021)|(fedemmchkhdelojjjgdkopmplmjiengg@chrome-store-foxified-1217776487)|(fedemmchkhdelojjjgdkopmplmjiengg@chrome-store-foxified-77529707)|(fggpapnokdmcagooedemcgfhpcidnnbc@chrome-store-foxified-56486150)|(jmeodlkabhcmdebfikcgnhdneigffoag@chrome-store-foxified--940653973)|(jmeodlkabhcmdebfikcgnhdneigffoag@chrome-store-foxified--970176466)|(\\{029d0500-68ad-4b02-aa34-c3be28d3c952\\})|(\\{0fe88455-850f-4f9e-b34e-c64747ac3274\\})|(\\{12d2c374-69c2-4747-b606-482586e56fb9\\})|(\\{18c16fe6-918b-4303-900c-ff86339574df\\})|(\\{1d5a4c53-81b3-40d9-a5e5-f915b26f879b\\})|(\\{213994e8-a858-4f3f-8c1c-c3906d10370f\\})|(\\{23e1aa4d-5061-406e-aba3-907974a9baa6\\})|(\\{33b0a817-bb94-4ff8-90d5-54d7519ef143\\})|(\\{3c16da40-3c81-4e78-b62b-831ad3b9d7bc\\})|(\\{441432c1-4336-4c00-8ad0-9cb3374432dc\\})|(\\{49f51783-7154-48d9-98d6-d48fab6f3f49\\})|(\\{4a1556b3-498e-4d59-97ca-09069ab3e2e0\\})|(\\{5b9264e8-609d-445f-993d-9d35c6cbe372\\})|(\\{62bfcada-a107-41ab-9aa3-91ccc804e984\\})|(\\{6b0891fb-3664-456f-92ab-ad6956d26986\\})|(\\{708b40d9-43ed-4ce4-95e2-093b2245ec71\\})|(\\{8175e95e-fe95-48ae-aa4f-d73807237ae6\\})|(\\{8eb80b1a-7abb-4a88-bd1f-51403bdc1a8c\\})|(\\{95b56317-db14-4c77-b6b3-d300710224ef\\})|(\\{9973a0b4-356a-470a-9b8f-9f89e74c7840\\})|(\\{b357faad-ec9a-4c75-ad87-9ecc876c9ff8\\})|(\\{d0d90a87-03de-44da-8dc4-d432bd4a8ae0\\})|(\\{d479fc03-87a9-4ffe-98fb-d20738d4aece\\})|(\\{d93f2300-3fb0-4d12-9d4a-39dc379e35ae\\})|(\\{e6796495-a93f-4307-8bff-f937287b0fc2\\})|(\\{f14840e4-196b-4ef6-9c85-84d9203c2924\\})|(\\{f3126a97-3a2c-487a-86bb-18bd426edcc1\\}))$/", + "prefs": [], + "schema": 1593599784537, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650138", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Data collection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6e7a021d-f995-40f1-8f1b-aab451089613", + "last_modified": 1593783156743 + }, + { + "guid": "/^((bahkljhhdeciiaodlkppoonappfnheoi@chromeStoreFoxified-3761781728)|(dbfmnekepjoapopniengjbcpnbljalfg@chrome-store-foxified-unsigned)|(fgigfliokgjiicoladjbodllohgkolbe@chrome-store-foxified--1744075311)|(fgigfliokgjiicoladjbodllohgkolbe@chrome-store-foxified-1686967414)|(generated-6qpuwmerrkb0nutc18kzls@chrome-store-foxified-181411724)|(llgiblikeclfoebojkplbcmnicgcabhg@chrome-store-foxified--759157022)|(llgiblikeclfoebojkplbcmnicgcabhg@chrome-store-foxified-3251930310)|(llgiblikeclfoebojkplbcmnicgcabhg@chrome-store-foxified-4051630149)|(safebrowsing\\.firefox@trustnav\\.com)|(\\{05fdaf75-d3e4-4434-aa8b-183d491d54d6\\})|(\\{a37e731d-7644-4fed-8612-1ee0474950f9\\})|(\\{a54b0c1d-6c98-4ac4-9e9e-d623aba08921\\}))$/", + "prefs": [], + "schema": 1593705201653, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650141", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Remote script injection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4947073b-381f-4267-89f4-9591e38b29c3", + "last_modified": 1593783156738 + }, + { + "guid": "/^((bahkljhhdeciiaodlkppoonappfnheoi@chrome-store-foxified-1375878516)|(vidmate@india\\.com)|(\\{02315b1d-6e91-46bc-92a9-4611eac43692\\})|(\\{023cc38a-e7cb-4098-9564-0bc2fc434ed0\\})|(\\{05b3ec50-df36-4667-ad87-8e7eb4b25091\\})|(\\{05bba9cd-6547-48b2-86f2-454fd09309f9\\})|(\\{05dae426-4c6a-47da-affd-adff1f19025b\\})|(\\{061e6cd3-40c9-427b-b3f4-1e3246bb898a\\})|(\\{063DA41A-2561-401B-91FA-AC75E460F4EC\\})|(\\{06a4929b-48d6-4c29-9fbf-8283c7af6817\\})|(\\{0ae6a4d7-7a2f-4c91-9937-a0b209dd9929\\})|(\\{0f26adb8-8084-4925-b111-ffa1240a3f36\\})|(\\{13f3d29f-b148-4fdb-822f-6bad39befc92\\})|(\\{16b0e9d0-5292-4161-86fc-e8d4f938bede\\})|(\\{16ca0ebf-0870-418d-a4ff-aa65b93cd962\\})|(\\{19758c54-d52b-4b96-9a29-52d67cfdcd3a\\})|(\\{19d569ff-4d0c-4cff-bf7f-3fc7a44b47af\\})|(\\{1ac34f1e-c584-4d2e-80ec-bc7f6c97e214\\})|(\\{1e089af9-9474-40e4-8767-a0f142af429e\\})|(\\{1f4ff519-5941-4e0c-b4b0-bd7205b5dce1\\})|(\\{20175382-1566-4a4a-8073-a8ad39b71a3d\\})|(\\{20dd4a66-ad07-4b45-b5bb-8c7d325e4235\\})|(\\{2136a796-5f22-4dd9-be01-07eafab1204a\\})|(\\{23b6469e-28a5-4396-8094-df88860847a5\\})|(\\{24436206-088d-4a1a-8d0e-cf93ca7a2d23\\})|(\\{2743421e-e752-48de-92a4-dbc0bc17218a\\})|(\\{27f35034-8adb-4070-8a2d-19874d869627\\})|(\\{29a70ebf-55de-4d0d-b32b-f593c40beafe\\})|(\\{2b5dee4d-6fa2-4544-988e-4b6796fcc2dd\\})|(\\{2b8d4e01-8cd7-41d1-a780-88f6487be76e\\})|(\\{2c8d7a96-a6ee-46be-8a1b-1132eb217107\\})|(\\{2d56442e-48ed-43fc-8a3d-f1287948b85e\\})|(\\{2eb5f6dc-b13f-4e61-8d22-8dd67c6eaa41\\})|(\\{3054365f-84fb-46a4-894c-4e27c0befe35\\})|(\\{30b4646c-e1db-4dee-a397-6a2531d0225f\\})|(\\{3120fef3-3693-420b-8aba-b62a40be2e99\\})|(\\{31c38bb1-3da7-4145-afa8-3f930a8d23ea\\})|(\\{323ee1c1-04de-4fcd-8e23-fb80ea246433\\})|(\\{33904608-2fa2-42ff-9521-a8f8b96b3df6\\})|(\\{34859f18-afa0-4fbb-aa6e-ef6b2c2dd7b8\\})|(\\{34d06de6-3074-4ab1-a9da-6448fc1e2b29\\})|(\\{35cb8472-646b-4b2a-8cdd-3206681be6fa\\})|(\\{3625f370-b607-45eb-b997-8e1808047bb6\\})|(\\{372b22d8-55a3-4f9a-98e5-a22fe1af7d4d\\})|(\\{3777ceb4-b4d8-4155-b86c-86b08e509898\\})|(\\{381d3696-0422-41aa-b606-12cc9ebe8b59\\})|(\\{3c9f8dc4-eb25-405c-b0be-db3c087d1e5c\\})|(\\{3cecb889-7f55-4b51-abc1-2ef68f03e755\\})|(\\{3db9b4e7-a4ae-4f85-80e8-8804a3e65a43\\})|(\\{3eba841e-3ed1-49b0-a676-17a8044f396f\\})|(\\{3ec01cb3-b730-4536-b1f2-b1737f7f76a5\\})|(\\{3f13b2ea-8523-4296-81e9-d41b34a028a6\\})|(\\{3f4ce7be-f18c-4809-a048-8915b6fab570\\})|(\\{3f8b1172-bae4-49b6-97dd-56610d9be9f0\\})|(\\{403ec2e1-66a1-4fa6-9dc7-fb7ec85df601\\})|(\\{4322d31f-91ca-4138-ab6a-976338d1cd3b\\})|(\\{4a3d4001-73a0-4038-aeb6-1229dded0523\\})|(\\{4a703694-8daa-43de-979a-e5194ae98444\\})|(\\{4d584a35-da8c-459a-9adb-dab88d27ebe7\\})|(\\{4e2b1fc7-6298-4982-a61a-25d678636cb1\\})|(\\{4f39a102-574e-4f74-93b3-51ca7958ad22\\})|(\\{4f3debf1-0e74-4dc5-a7ab-448280ce6461\\})|(\\{5117db36-6a2e-485d-a1a8-97f75af00bc1\\})|(\\{51311486-15c0-4f65-a40b-9561ba07e215\\})|(\\{56495d47-f44e-4d98-9434-7bc2f5525823\\})|(\\{56786bf6-645f-419d-b913-d0ca3859fa33\\})|(\\{5947c59b-69df-414c-9c10-017f4196a309\\})|(\\{5afee4b9-98f8-443f-935b-9a207620db3f\\})|(\\{5ca2a8a6-ace0-41da-b842-e2a42f2e1cd4\\})|(\\{5cd491dd-bd65-49c9-8152-08d474ad34be\\})|(\\{5eee10be-a741-455f-95d3-4d03215c9690\\})|(\\{5f68bcac-eebc-4671-b75a-9dd6cea18a15\\})|(\\{5f9713ff-6dc2-4adf-a9f3-47f80cd85dfb\\})|(\\{62e37e5b-4701-4a1c-8a82-029432691f40\\})|(\\{634462e7-3f71-4150-b108-69b51780e68c\\})|(\\{65e7d7b7-7e48-4568-988c-2d189d9ff5ff\\})|(\\{675812c6-1b4f-4c56-af85-3bda760719f6\\})|(\\{67c45529-cf0b-4c5c-b24f-280d9349c909\\})|(\\{68da33b7-e3d9-4afe-b8ab-765093be602f\\})|(\\{69387715-933a-45d9-9ddf-8a727c9dbcf3\\})|(\\{6ae0f1b6-be29-4790-8721-ca36bd738876\\})|(\\{6afe3849-bf08-476e-a451-2aee47b5aea4\\})|(\\{6ca71d05-3cc8-4e33-8ae4-fbbee89de965\\})|(\\{6d0404a3-40a0-4b6b-a530-e8d045ae0ee4\\})|(\\{6d743625-7a7f-4eec-9b44-c6465325d0a3\\})|(\\{702cc617-a851-44fc-8b58-46ad6e640ecc\\})|(\\{708cc305-3067-4094-bb39-b068fb990c61\\})|(\\{7107fef7-513e-4921-a60e-17d0fb90e25c\\})|(\\{724e248e-97dd-4b87-9a22-22b3a4e90d7c\\})|(\\{732f68a6-d9b5-4ff8-aada-707fa43b19d5\\})|(\\{737dc7a8-df2d-4dfa-92ee-0c8959cae647\\})|(\\{7469196e-2de4-485e-aaf4-ebdf2e792348\\})|(\\{759701de-a587-4b50-aaaa-258c5e1bddb8\\})|(\\{75ff68d2-c459-4433-86ce-a9c241cb3e73\\})|(\\{765eb1ff-8106-428d-8d4c-c57f1e33049f\\})|(\\{777d46f8-890d-47b6-8079-d55104b2856c\\})|(\\{7bdc2314-02d1-4b33-ad6f-79569ab3674c\\})|(\\{7c510db5-9da4-4529-985b-98cb042bc6ff\\}))$/", + "prefs": [], + "schema": 1593705366484, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650144", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search Overrides" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "38040bfd-8701-43da-ad69-3b210f7a658c", + "last_modified": 1593783156732 + }, + { + "guid": "/^((\\{7d6ae6df-1c04-43a5-869e-8a23d3934737\\})|(\\{7de74298-3d21-41a2-a178-370132faa85d\\})|(\\{7ed82570-bbf3-402f-a5c4-31ac0530293f\\})|(\\{81622d67-2ba6-425c-8175-4014ec821c84\\})|(\\{817b743d-685f-436f-9104-2718370f0f14\\})|(\\{852eb947-3f78-427e-96f6-bae8c7d45480\\})|(\\{8551b8c2-d137-4d78-b6de-8f99e612ad6b\\})|(\\{85d62722-1d90-4e52-bd9a-c75cc8c53c62\\})|(\\{8a384a87-1b7b-485d-b032-ccd3156c8ef7\\})|(\\{8da0dac4-b157-4741-aa49-c99430a9aa7e\\})|(\\{8e0f770c-3087-4055-abd1-839e138e145a\\})|(\\{8e74f640-5158-476b-a039-d9b16adc23e3\\})|(\\{8e95aea7-ba10-4665-8f99-39e673ef3aba\\})|(\\{8f8c089f-1adc-46de-ba27-f03d8aa77507\\})|(\\{90d88023-4dc1-46bc-bf8c-09f0e563864c\\})|(\\{9248343d-6415-410f-bdc2-3e73b2dd4c25\\})|(\\{93de7c3e-1021-4ae0-8825-19a55c9a00a9\\})|(\\{95eaf46e-7a60-496e-a52c-ca119fe404ef\\})|(\\{964e4ab3-7043-4cda-8ec9-1041f4b683ec\\})|(\\{99e34243-0ab2-488d-8cf4-476a39342875\\})|(\\{9c5be5d9-0f11-4f1f-92d6-e000576b8bf7\\})|(\\{9d6b0c0c-c502-4ee2-9ee3-907dab4dc1d9\\})|(\\{9e1eeb8e-6627-4c59-b686-7df31d9ff62a\\})|(\\{9eb48825-7932-442b-84a5-de2fd2800306\\})|(\\{9f8cca9a-2bcf-4b04-b796-562b3302e433\\})|(\\{a1ce0a8c-1177-4049-b5ba-3a798d9f0279\\})|(\\{a1eb42fd-6c3b-4c63-81e0-4904806243d9\\})|(\\{a451ebb4-8c80-4684-a32f-6b331c3e9390\\})|(\\{a4e8e10c-898e-462b-8db1-220044c3f4b3\\})|(\\{a5949142-2af4-4957-8ed9-64d4e06d586d\\})|(\\{a792db23-156d-437f-9ed5-d3319cfb82a5\\})|(\\{a85a38fc-011e-4f2c-9e8a-ff2963f76955\\})|(\\{a8a5035c-0cef-4504-8c0d-a26997e84d93\\})|(\\{ac12dcbf-f835-48db-b4fc-56c145b3bb59\\})|(\\{ae49d937-7e75-40b0-a784-635363f68bcc\\})|(\\{b38ebbc1-fd37-4644-8d2e-67f11a2018fe\\})|(\\{b6bae9e6-6940-4698-8673-8b5cdc4ac5b9\\})|(\\{b6e3d5ab-e8bc-40d8-8eab-f1836caf1052\\})|(\\{b753389d-d85e-4125-98f8-bf19c0511089\\})|(\\{b7897131-2bf5-4024-8c41-701d1c6890ef\\})|(\\{b84de506-80f0-4b73-afe0-0b930da27ce7\\})|(\\{bcaa879f-60b5-4dcc-8da9-8b97a8db4c2c\\})|(\\{bd9a5038-8fec-454d-8f72-d14315618852\\})|(\\{bd9bc86e-5f14-4463-8163-255f694599f2\\})|(\\{c074cd4b-bcf7-4cb7-965d-e03a5ba6c978\\})|(\\{c1c04f48-b24a-4966-9876-b75095372491\\})|(\\{c39c50d0-c7ea-474e-9d2a-10d9eb663299\\})|(\\{c7fd903d-e8bd-4991-ac5c-51606f3df5ad\\})|(\\{cc4dc32e-d435-42fd-a6ec-5971218b86a7\\})|(\\{cc54a433-beda-449e-9125-821479dd2bbf\\})|(\\{cd1f7c79-46b5-457a-a2ae-e82131eb473c\\})|(\\{ce9fbeb1-ee7f-4e87-a8e2-8229317833c4\\})|(\\{cee22b0d-3f3d-4e89-ae79-0e21dc1bb4b1\\})|(\\{cf35a04b-3b15-44fd-8cb7-8eed586bb23c\\})|(\\{d0e39061-7f95-4ee2-a258-206aae9b4301\\})|(\\{d0f31cdc-518b-4277-be2a-0f85c9b66fd7\\})|(\\{d496d961-2fbe-445d-a56d-058db7826492\\})|(\\{d49a9d4d-b997-4292-a0ca-4d2f7a0c61c8\\})|(\\{d4a8ee97-9864-4676-ac91-2683a604d633\\})|(\\{d4a96684-0e76-4269-b08b-00f9f2f7cf29\\})|(\\{d5d00f6f-a94c-4f61-8881-e9e8aabd6732\\})|(\\{d748943d-da9b-4cc9-aa0b-4f9ba5e4108c\\})|(\\{d7b4cdcc-4779-4ab9-b2ee-f0be10219a20\\})|(\\{d7ba7e93-14e0-49b0-a743-6480bcd44671\\})|(\\{d8d8cb23-1963-4544-a496-881d9a1d821c\\})|(\\{d9aafd87-d1df-4459-b09a-48f9c66ee9d6\\})|(\\{da77b261-64cc-466a-ab08-b810ac8b16f1\\})|(\\{dbcf27a9-4bb2-4ad7-82d2-f4781f4fa54d\\})|(\\{dcd3cb2e-235d-4451-a3e5-f1531f5b7b69\\})|(\\{de592dfa-2529-411c-a4b3-80c6e9fcfe84\\})|(\\{df63c4ff-9080-4bad-bb40-a362d02fe047\\})|(\\{e0888bc5-861b-4062-9601-f621b4ededd3\\})|(\\{e1aaa683-ecb9-4673-af6a-0522b25def1a\\})|(\\{e2b719ca-bb2a-4f8f-85fc-ed8ee5a711eb\\})|(\\{e30bad75-2798-4dbf-bf0a-c0238d4bc72e\\})|(\\{e40c5cd7-dfc2-4eb6-ab5c-8f931728dc07\\})|(\\{e4cb2302-9fc9-415c-af9d-4f30114b7e76\\})|(\\{e52d8f33-3f82-4225-8bca-dd2d8d507b02\\})|(\\{e6a9830c-cf6b-41c7-9a7d-394cbfb0f561\\})|(\\{e764acf2-dec7-4a8e-9654-1cff3d47f81b\\})|(\\{e803c340-021f-4df9-b35a-b57081d12126\\})|(\\{e9679072-420c-479f-a269-1a2b82d07668\\})|(\\{e9e71e6c-976f-4a65-b6ac-86aec1f76800\\})|(\\{ea044650-db5d-4665-aee8-5fd5c0019baa\\})|(\\{eac2a1ea-a38e-430d-9e7c-2f05b0145dff\\})|(\\{ebf3d8e2-e805-4d03-bdb0-e3cd5e6818d3\\})|(\\{ec57dcba-d2bb-4c96-8df0-2c98811afdbb\\})|(\\{eca1ec4c-4131-4ac7-b6dc-5506a3aae09a\\})|(\\{f0408c68-1d51-4d18-befe-bfd8de73b1fe\\})|(\\{f31775a2-1753-442b-bd38-34015c893dd2\\})|(\\{f51ebc22-0e0b-4ab8-a998-2eda93ebb11c\\})|(\\{f6ef3800-5101-42a2-9e55-7e2c5e3562fd\\})|(\\{f8c9209d-7c46-400b-80dd-554522a3045d\\})|(\\{f90fa605-2954-43b0-989c-9c7cdb844eb5\\})|(\\{f96ee6a2-21a1-4f00-98f4-0ec791e0f8d4\\})|(\\{f97baacd-156b-47e6-b518-c6beb06905b2\\})|(\\{fad66d2f-514e-4c13-98c0-338713d37838\\})|(\\{faf9c0dc-ef49-44d6-a116-f4fc3de27969\\}))$/", + "prefs": [], + "schema": 1593705486294, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650144", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search Overrides" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "66152682-a9fd-4ce6-ae62-17ec4b9d5da7", + "last_modified": 1593783156727 + }, + { + "guid": "/^((\\{fc872e3f-443a-4b3e-87b1-1c413e1e2dc4\\})|(\\{fd51546c-b08b-4682-b52e-fbde47aede45\\})|(\\{fda53749-d26a-4007-876e-dc10cee6fcf9\\}))$/", + "prefs": [], + "schema": 1593705487230, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1650144", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search Overrides" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "07b84d5b-33c0-4e4b-ae86-b10bf0045bd1", + "last_modified": 1593783156722 + }, + { + "guid": "/^((addon@kravi\\.online)|(\\{0f3b3fb6-a971-40b8-aa64-fa79b9d2018d\\})|(\\{24d770e0-959a-4838-a436-c0f6269936a7\\})|(\\{2f5817b9-1b88-4632-a008-958b3039557d\\})|(\\{520e3e3b-92e7-4ca5-845d-6a3b106bca6e\\})|(\\{5aff28a3-74b6-4317-84ee-6936392fc6ab\\})|(\\{9bc27edf-9f4f-48ed-ba96-7df380747065\\})|(\\{aa50ee26-de9d-4dbd-a837-64f6923f025e\\}))$/", + "prefs": [], + "schema": 1593373295253, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1649271", + "why": "These add-ons violate our no surprises and monetization policies.", + "name": "FireX affiliate promotion add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6c06f7be-952e-4dae-8d48-3ed7eb94e3a5", + "last_modified": 1593599783923 + }, + { + "guid": "/^((_ejMembersttab03_@free\\.downloadrecipesearch\\.com)|(_flMembersttab03_@free\\.myformsfinder\\.com)|(_psMembersttab03_@www\\.freetemplatefinder\\.com))$/", + "prefs": [], + "schema": 1592682094497, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1627249", + "why": "The add-ons violate our no surprises and data consent policies. Please upgrade to their latest version (9.x or above) to continue.", + "name": "Misleading search add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "8.*", + "minVersion": "0" + } + ], + "id": "e016df6f-c253-441c-a867-386d213b228b", + "last_modified": 1592846474156 + }, + { + "guid": "/^((suisserien90@outlook\\.com)|(\\{04f34321-900d-49e8-b2ba-85642e1d2167\\})|(\\{13305730-6d06-4d29-8a50-103b55b41f94\\})|(\\{23730c01-2bd6-47fd-869d-bed842f0819a\\})|(\\{31f54302-17b7-4d2e-875f-921193ea34bb\\})|(\\{33d8aeab-57b3-4942-92da-e2cb9f7d7d7e\\})|(\\{4953bde8-216d-4524-aba6-9d84b7cf4ea8\\})|(\\{4a7c8ec5-2899-4730-9742-d111a55c2194\\})|(\\{670319d6-3d45-4f44-99e2-0b23a2d8bccc\\})|(\\{7cbfee61-804f-4c49-a79b-556502ce9fc9\\})|(\\{88fa0036-482d-48f4-aefd-bebd1bfc6945\\})|(\\{8a25959e-6268-4e71-8959-3a3b09d3c9f7\\})|(\\{8e07001b-157e-4c18-8aac-178e8e42a944\\})|(\\{92bdb180-7b26-4ec4-b0bd-de6f8b8a0d59\\})|(\\{9de2201d-2a5a-4cf3-bcdc-e3679dce250f\\})|(\\{db252e2a-cf4c-457e-b134-c0af8b5970fc\\})|(\\{ef2940f7-bdd4-49be-948f-469777641893\\}))$/", + "prefs": [], + "schema": 1592309193166, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1646070", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e161917c-387c-4ebb-905c-02e923525780", + "last_modified": 1592386739136 + }, + { + "guid": "/^((\\{6244689f-c58f-45ea-a86f-b270862d52d1\\})|(\\{91c831bf-f2a4-4189-b745-da48f1c0d7bd\\}))$/", + "prefs": [], + "schema": 1591126893821, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1642976", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2380a178-4230-46a1-a64b-d084daa7a03d", + "last_modified": 1591217936777 + }, + { + "guid": "{00353fe1-9fe1-402d-91cd-0603672b24aa}", + "prefs": [], + "schema": 1589797244276, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1632212", + "why": "This add-on violates Mozilla's add-on policies by using a misleading name.", + "name": "MediaPlayer10 Search Powered by Yahoo" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.20.213", + "minVersion": "0" + } + ], + "id": "380f49e3-8fc0-4698-be45-ebe5e2d43620", + "last_modified": 1589882159554 + }, + { + "guid": "/^((\\{8da217c7-3ad0-4bca-8dbc-20c9f190a49f\\})|(\\{3dc7ec06-f769-4213-9f78-d7f212b97306\\})|(\\{8b4a6f7a-35fa-4e19-a967-f2bf58680c9c\\})|(\\{7dcbc355-56f7-47cd-8f39-02dfab691d5c\\})|(\\{3cd1ee27-b930-41d3-940f-5ce6e5f351bb\\})|(\\{4e8cb340-8b4c-4930-a474-475401d4acef\\})|(\\{70dd2db8-a59f-4107-8158-d03535ff3e5b\\})|(\\{3143c250-b65a-47e2-a412-76b06d514898\\})|(\\{d7f4f007-98bc-402b-b03e-abac1fa889e7\\})|(\\{bc1fd5d5-1984-4dfe-801c-c52d4621b38d\\})|(\\{56ab7b94-c5bd-4353-9ab0-88a9ee203ae3\\})|(\\{e9762aad-4543-458d-a374-f8034b09c4fa\\})|(\\{8f451db8-02fa-46ed-a51d-89101a8f7c05\\})|(\\{5b8dc6d6-0b5f-4d69-9a96-cc7c4ec2c910\\})|(\\{d964e6e6-ee57-4f85-939f-a68385f91172\\})|(\\{aee53115-a386-4c0a-b55c-501fb0be4a95\\})|(\\{6e3fcb82-0bff-40c7-af05-13557ea52f7c\\})|(\\{ea408188-701e-4ebe-bbaa-78f31841ae0b\\})|(\\{1fd9b109-4b8a-45b7-a6cf-e8c548f4ed7d\\})|(\\{d34bc0ad-1c64-48ab-b99a-40528b323335\\})|(\\{db16d458-324c-4c76-af08-1097cd66400a\\})|(\\{bfeb5542-e12d-44d3-bda6-67c6c587a46a\\})|(\\{744878d2-fc78-44dd-bc23-9f2a60666f4d\\})|(\\{c8433eeb-7e77-4f6c-85a3-97d94a80b5f9\\})|(\\{1a30df6c-d08a-4528-9c61-7de5617ef920\\})|(\\{387b8bf2-6d33-4036-a383-57eedd44eed2\\})|(\\{77e0efb7-6d23-465f-8e44-144b64444e61\\})|(\\{383f5d11-e6ec-4c2d-a10d-9dc4e0b3f33e\\})|(\\{daf7b378-d0bd-4555-a36d-2a72c530fe2f\\})|(\\{d55ca4be-4bcc-4e50-bb8a-2e7b9ea7cdd6\\})|(\\{6770b2e1-ff5e-44ce-ad31-582d2846b379\\})|(\\{cd426d98-f41c-41c7-844a-68b2914e6c45\\})|(\\{2cd538ae-ddcd-4997-9a61-e1f7b5cbd671\\})|(\\{7c22cd77-84dc-4535-93b1-82d8cb7c17c2\\})|(\\{8ca4136e-49af-4466-869f-d7f3375d4eff\\})|(\\{21ce3c32-578e-4ad0-91c4-b024987c1675\\})|(\\{1b533e15-0e04-4fa8-92ed-211d7689000f\\})|(\\{19ec95c4-5b74-475d-a6df-dd8b0ed59268\\})|(\\{31908468-e35d-4716-992f-6b8e781709f6\\})|(\\{883a0770-0148-414c-b0e8-ee54f62325fe\\})|(\\{51440879-e72b-4de9-b171-3566f0d050be\\})|(\\{f6369c61-a8a9-4f23-a443-247f5f708c9f\\})|(\\{3de42493-eae0-47f3-8201-42379770e58a\\})|(\\{d5b0375d-df44-4740-8627-e47c4590742f\\}))$/", + "prefs": [], + "schema": 1589793046978, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1632212", + "why": "This add-on violates Mozilla's add-on policies by executing remote scripts, collecting search terms going to a third-party provider or expsoing add-on settings to web pages.", + "name": "Add-ons violating Mozilla's add-on policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "33b46813-8910-46b3-92d1-47d022a66294", + "last_modified": 1589882159550 + }, + { + "guid": "firefox-night@mode.ubk", + "prefs": [], + "schema": 1589285002744, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1637571", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Night Shift mode" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7790cbae-2ba6-43d9-8496-0ce311cecd00", + "last_modified": 1589450770496 + }, + { + "guid": "/^((2020-adblock-for@youtube\\.com)|(@save-from-youtube)|(@youtube-converter-addon-for-firefox)|(@youtube-converter-video-music-downloader)|(@youtube-download-helper-addon)|(@youtube-mp3-download-addon)|(@youtube-mp3-downloader-addon)|(@youtube-video-download)|(@youtube\\.download\\.helper)|(@YouTube1)|(@youtubehdVideoDownloader)|(adblock-for@youtube\\.com)|(addon@youtube-adblock)|(addon@youtube-mp3-downloader)|(addons-mozilla@flappy-bird)|(addons-mozilla@youtube-hd-download)|(addons@youtube-mp3-download)|(addons@youtube-video-download)|(autoyoutubehdquality@developer\\.org)|(convert-to-mp3@youtube)|(download\\.helper@youtube\\.com)|(downloader@youtube\\.com)|(google\\.translator\\.addon@addons\\.mozilla\\.org)|(google\\.translator@my\\.addons)|(gtranslate@addons\\.mozilla\\.org)|(info@video\\.download-lagu-mp3\\.com)|(info@youtubemp3music\\.com)|(info@youtubemp3music\\.info)|(info@yt-download\\.org)|(info@yt2mp3s\\.me)|(instagram-download-addon@addons\\.mozilla\\.org)|(instagram-downloader-pro@firefox)|(mp3\\.download@youtube\\.com)|(mp3\\.downloader@youtube\\.com)|(open-my-page-button1@mozilla\\.org)|(video-download@downloader-addon)|(ydh-addon@youtube\\.com)|(youtube-to-mp3@downloader)|(youtube\\.downlaoder\\.update\\.2019@addons\\.mozilla\\.org)|(youtube\\.download\\.helper@youtube\\.com)|(youtube\\.downloader@professional\\.com)|(youtube@tomp3)|(youtubedownloaderhd\\.addon@mozilla)|(youtubehd@developer\\.org)|(youtubetomp3@addon)|(youtubetomp3@youtube\\.com)|(youtudbe\\.download@addons\\.mozilla\\.org)|(yputube-video-download@addons-mozilla)|(yt-download-addon@addons\\.mozilla\\.org))$/", + "prefs": [], + "schema": 1589369260661, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1637588", + "why": "This add-on violates Mozilla's add-on policies by loading surprising content without disclosure or consent.", + "name": "Add-ons violating No Surprises policy" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "864a2a91-e341-4cca-bc22-00300255604b", + "last_modified": 1589450770493 + }, + { + "guid": "{e8f8555b-c4c4-4be6-b0ee-00b452e06e8d}", + "prefs": [], + "schema": 1589375112158, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1637590", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Screen Capture Lite" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ea706d4a-34ae-4fc6-b67c-2e4dc941437c", + "last_modified": 1589450770486 + }, + { + "guid": "new_tab@ext.xpi", + "prefs": [], + "schema": 1589375251912, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1637710", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "New Tab" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "79b35553-78d1-4de4-aff3-8b744b2cbe82", + "last_modified": 1589450770482 + }, + { + "guid": "/^((@pdf-software)|(@pdf-tools)|(\\{3f5de3f0-2a4a-4310-adc9-aa320caceac1\\})|(\\{cbeba114-49a8-45aa-9b20-58d744301a7b\\})|(\\{ef94edee-053f-4d62-9101-cbc8550608df\\}))$/", + "prefs": [], + "schema": 1589053292831, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1636933", + "why": "This add-on violates Mozilla's add-on policies by including remote content in the newtab page.", + "name": "Add-ons using remote content on the newtab page" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f9afaa85-b3fc-48d0-a696-0a916844281e", + "last_modified": 1589285002325 + }, + { + "guid": "/^((@gotransitx)|(@new-tab)|(@new-tab-ff-sideload)|(@new-tab-may)|(@new-tab-poc)|(@new-tab-side)|(@new-tab-sideload)|(@new-tab-test)|(@newtab-ext)|(@newtab-omni)|(@s_search2)|(\\{283f0ce4-ae17-4f72-8400-a6f455b5b278\\})|(\\{4318af2c-eccf-40aa-bee3-9f53a840baa3\\})|(\\{c24b53b4-1925-4b55-a955-0f24e807c33c\\})|(\\{c39923fb-30d8-49cb-8036-c692df03b282\\}))$/", + "prefs": [], + "schema": 1589201469333, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1636935", + "why": "This add-on violates Mozilla's add-on policies by collecting user search terms.", + "name": "Add-ons collecting search terms" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3b5fe370-f122-4b12-b4f0-81b5e32c347d", + "last_modified": 1589285002321 + }, + { + "guid": "/^((@adaware_webprotection)|(@adaware_webprotection_test)|(@browser-safety)|(@new-tab-ff-addon)|(@torrent-scanner)|(@waterfox-monitor\\.xpi)|(\\{65c3d1c8-6a6b-4f25-ae84-3770846ded13\\})|(\\{baf80d95-ba88-4506-9b17-d03d2fd188e7\\})|(\\{e789c2ad-91e5-4621-85f0-5e7e43036233\\}))$/", + "prefs": [], + "schema": 1589201612918, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1636936", + "why": "This add-on violates Mozilla's add-on policies by collecting user data without disclosure or consent.", + "name": "Add-ons collecting user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "943dc1de-7edb-4a52-91d3-c1043e69f658", + "last_modified": 1589285002317 + }, + { + "guid": "/^((@browser-safety-inline)|(newtab@lavasoft\\.com)|(\\{a34f4b0d-2813-4e64-a72b-59998488e866\\}))$/", + "prefs": [], + "schema": 1589201762208, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1636937", + "why": "This add-on violates Mozilla's 'No Surprises' add-on policy.", + "name": "Add-ons including surprising functionality" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b4ca1419-20df-4c27-969f-883f3e9fc05f", + "last_modified": 1589285002313 + }, + { + "guid": "firefox@browser-addon.xyz", + "prefs": [], + "schema": 1589226093582, + "details": { + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Adblock Premium" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "eafece33-b993-443e-b944-8bcefb4389ec", + "last_modified": 1589285002309 + }, + { + "guid": "/^((\\{4172b9c5-86b5-4a94-9c6d-fc9c586e00e1\\})|(\\{6e4137fb-4840-47e4-953f-9a5a59acd2f0\\})|(\\{7ed913b5-a7b6-48db-903c-a9014341b672\\})|(\\{74300757-a623-4b34-ac23-a24d5f4b932e\\}))$/", + "prefs": [], + "schema": 1588757238897, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1635950", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "NEX Extension Trade" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "067bea22-d677-4f45-9e6b-c05bf4137e29", + "last_modified": 1588871670403 + }, + { + "guid": "/^((\\{6eb37565-44ae-4512-b847-dd731b4e89b7\\})|(\\{70cbaf25-0acd-43eb-88bd-d1d867e8a5fc\\}))$/", + "prefs": [], + "schema": 1588801398036, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1636082", + "why": "This add-on violates Mozilla's add-on policies by executing remote code or violating the No Surprises policy.", + "name": "Various youtube download add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8d9347d8-d30f-4251-bfbb-ad94f57bfa04", + "last_modified": 1588871670397 + }, + { + "guid": "push@ext.xpi", + "prefs": [], + "schema": 1588016491847, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1633857", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Push (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "22ed13b0-bb78-4b59-9eac-b938f1d6db24", + "last_modified": 1588178650319 + }, + { + "guid": "/^((dind@ext\\.xpi)|(dolen@ext\\.xpi)|(dom@ext\\.xpi)|(fynal@ext\\.xpi)|(fyster@ext\\.xpi)|(grabber@ext\\.xpi)|(homly@ext\\.xpi)|(info@ext\\.xpi)|(kibbi@ext\\.xpi)|(konned@ext\\.xpi)|(oryole@ext\\.xpi)|(plac@ext\\.xpi)|(primary@ext\\.xpi)|(privor@ext\\.xpi)|(remote@ext\\.xpi)|(reture@ext\\.xpi)|(rurel@ext\\.xpi)|(sarc@ext\\.xpi)|(shakk@ext\\.xpi)|(sicq@ext\\.xpi)|(sidebar@ext\\.xpi)|(stolan@ext\\.xpi)|(tofik@ext\\.xpi)|(unwyre@ext\\.xpi)|(ynto@ext\\.xpi)|(zoom@ext\\.xpi))$/", + "prefs": [], + "schema": 1588156830156, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1634015", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1c42622f-7093-4bae-95e7-333122964ad0", + "last_modified": 1588178650310 + }, + { + "guid": "/^((\\{d7f46ca0-899d-11da-a72b-0800200c9a65\\})|(\\{d041a782-a59e-44a9-9496-02bfc5eb24b5\\}))$/", + "prefs": [], + "schema": 1587843690891, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1633380", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e211c71e-144c-4b23-a33c-d698e398416a", + "last_modified": 1588004938454 + }, + { + "guid": "/^((\\{66b636a9-2c5e-462e-83f6-89650caa7049\\})|(\\{70f04df5-d98b-473a-b394-664db1ccf6f3\\})|(chi_tab@ext\\.xpi)|(emoji_tab@ext\\.xpi)|(my_pdf_tab@ext\\.xpi)|(calm_tab@ext\\.xpi)|(nice_tab@ext\\.xpi)|(relaxing_tab@ext\\.xpi))$/", + "prefs": [], + "schema": 1587989793275, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1633381", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0d6b3ced-45b7-47b0-9b1a-901eb87f3c28", + "last_modified": 1588004938450 + }, + { + "guid": "{84c1d4fc-641f-4910-800b-b538d6f7273c}", + "prefs": [], + "schema": 1587989848219, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1633382", + "why": "This add-on violates Mozilla's add-on policies by collecting search data going to third-party search engines.", + "name": "Add-ons collecting search datta" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b46a37dd-528d-4d79-8abe-51d4fb190de6", + "last_modified": 1588004938446 + }, + { + "guid": "/^((abnmaapdjgmmnlodmpmjdebnklepecok@chrome-store-foxified-2396027552)|(abnmaapdjgmmnlodmpmjdebnklepecok@chrome-store-foxified-2768870236)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified-3862814240)|(abnmaapdjgmmnlodmpmjdebnklepecok@chrome-store-foxified-1995059603)|(ddbcnfgbjcicbjdblfafohkpodcnnehi@chrome-store-foxified-749309492)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified-1874441907)|(mekjacpfdgboianikjjonilmgfiekmic@chrome-store-foxified-1955510596)|(mpnamocnciebhgnpcnmoodclmocfcdig@chrome-store-foxified-1063516441)|(mpnamocnciebhgnpcnmoodclmocfcdig@chrome-store-foxified--20246131)|(mpnamocnciebhgnpcnmoodclmocfcdig@chrome-store-foxified-1884689962)|(kgcglkchocglabfcpdiepkifnfgffipe@chrome-store-foxified-1741550361)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified-1214727723)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified--1260504401)|(mpnamocnciebhgnpcnmoodclmocfcdig@chrome-store-foxified--1159015990)|(jellydsgliker@gmail\\.com)|(eoejpaafchckabdajohgeejbijdcgekh@chrome-store-foxified-140400165)|(coonecdghnepgiblpccbbihiahajndda@chrome-store-foxified--1373559051)|(mdaabhnjlpeemhcdbpopjfpjhbahgljl@chrome-store-foxified--411767831)|(pojgfhefohapcfnfbpmhkheejmfdkoap@chrome-store-foxified--1625696403)|(ghbdhifaekeljelljigndababkcmnkbc@chrome-store-foxified--381631510)|(eeocglpgjdpaefaedpblffpeebgmgddk@chrome-store-foxified-329350023)|(eeocglpgjdpaefaedpblffpeebgmgddk@chrome-store-foxified-1137326342)|(pojgfhefohapcfnfbpmhkheejmfdkoap@chrome-store-foxified-499858012)|(eeocglpgjdpaefaedpblffpeebgmgddk@chrome-store-foxified-541423900)|(inglpdjejkleleiikjkankoliodjihfd@chrome-store-foxified-1717509943)|(inglpdjejkleleiikjkankoliodjihfd@chrome-store-foxified--463564538)|(mdaabhnjlpeemhcdbpopjfpjhbahgljl@chrome-store-foxified-1665879186)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified--1720842629)|(lplaiehenloheihooakfjkigmkbmmhon@chrome-store-foxified-1634056625)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified--690580557)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified--1733601123)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified-626655243)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified--1696752497)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified--2146203701)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-1831123651)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified-1767565081)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-2083020687)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified-1426582348)|(nknfhhmhoflkcijaodalbncnmidocced@chrome-store-foxified--1290320498)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified--1121442903)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-951765545)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified--1520388245)|(mdaabhnjlpeemhcdbpopjfpjhbahgljl@chrome-store-foxified-2096851818)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-1268339820)|(mkbgdfopfbhcdnoccicgpcpgghhkgocf@chrome-store-foxified--120390217)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-1144806275)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified--1474463899)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified--2139994037)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified-610190214)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified--745418008)|(djijfbpaknhcpkmmdpjehlohnfjignne@chrome-store-foxified-1752420038)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-2147224839)|(\\{d15c2e66-7d4a-42d2-ad26-7145307a085a\\})|(lplaiehenloheihooakfjkigmkbmmhon@chrome-store-foxified-208664670)|(lplaiehenloheihooakfjkigmkbmmhon@chrome-store-foxified--1439727444)|(\\{12ee1eeb-7788-444f-8430-4edd085ab7e6\\})|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified--1731227161)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified--1624543284)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-1394660953)|(mhhlegoabmmlmmafmepadpdnncknjdid@chrome-store-foxified-1394660953)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-1935240009)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified-400250101)|(lmnialfbncmdjnnlkieehpbbgaoiihdc@chrome-store-foxified--80073038))$/", + "prefs": [], + "schema": 1587989982847, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1633390", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote script" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "952d5b57-1105-4e57-aa3a-a0c63d554c6f", + "last_modified": 1588004938442 + }, + { + "guid": "/^((coonecdghnepgiblpccbbihiahajndda@chrome-store-foxified--168270995)|(\\{aecccccf-16fa-4fad-b8e3-add05aaa9111\\})|(\\{bc5a705b-7b2d-4bb0-9cf9-360387b682eb\\})|(\\{7ab16f1b-627c-4ac3-ba35-e3545a9090cc\\})|(\\{fcca50f6-5318-4eb3-b737-73dd3b9e6045\\})|(\\{f0990433-f30e-4943-8fb7-e1de2b913d48\\})|(fjekneelhekaolbldhmokjfjlfdlbfcp@chrome-store-foxified--681203570)|(mhhlegoabmmlmmafmepadpdnncknjdid@chrome-store-foxified-1094969866)|(kbfppphbbfnnfbpbgilecdildnckbcam@chrome-store-foxified--1871365729)|(kbfppphbbfnnfbpbgilecdildnckbcam@chrome-store-foxified--1356642283)|(kbfppphbbfnnfbpbgilecdildnckbcam@chrome-store-foxified-1428700807)|(kbfppphbbfnnfbpbgilecdildnckbcam@chrome-store-foxified--2007683861)|(kbfppphbbfnnfbpbgilecdildnckbcam@chrome-store-foxified-2015138387)|(kbfppphbbfnnfbpbgilecdildnckbcam@chrome-store-foxified--1508372373)|(phbfhmldomeohhegllnephiclooafjdj@chrome-store-foxified--1178514589)|(bikpdjjalmcdjoglbgfhmdjclblhfjei@chrome-store-foxified--1178514589)|(debaadifgajofjmemiiphodjgamjaebh@chrome-store-foxified-219347684)|(fpocmbppcinhpeholdacbakebdkijfdp@chrome-store-foxified--493366227)|(pllaimjanehlenjlohniomaplpchdpea@chrome-store-foxified--606969408)|(fpocmbppcinhpeholdacbakebdkijfdp@chrome-store-foxified-2133428645)|(debaadifgajofjmemiiphodjgamjaebh@chrome-store-foxified--2071337123)|(@vkmediadownloader)|(\\{80d083fc-8d0c-43b9-bb21-9c1545797019\\})|(@fud)|(@facebookcolor)|(\\{6f72fdeb-a77c-4626-94bd-80d2966d67f9\\})|(\\{5b9ba2c6-e069-431c-b1a0-6013cb2668ae\\})|(image-search-reverse@4\\.90)|(nokpebgkfckhkmiejkpokjgeaigopbmo@chrome-store-foxified-unsigned)|(generated-hgzdu0x7vzdjuxwf4p8twj@chrome-store-foxified-864201011)|(\\{ea43dc99-0607-481e-b82c-f32769aa691e\\})|(\\{2981b4f4-25a6-429a-a3a8-18fd228f20bc\\})|(lite-vpn4\\.1@gmail\\.com)|(\\{61a36b8b-6c7c-4dbc-ba7a-2b58d74eedd2\\})|(\\{580d5662-cfca-4314-95ea-d0eee823a540\\})|(\\{9643ee9d-e783-40ee-9dd5-fb7219e82ea9\\})|(\\{a7376abe-343c-4205-99c8-8871ab87fa6f\\})|(\\{c5283dde-419a-405a-a837-4ed56761c1c9\\})|(\\{91b25e6e-b160-44f0-8342-ba36049fc336\\}))$/", + "prefs": [], + "schema": 1587991952607, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1633390", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote script" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7b12233a-69c3-4e98-9337-7df0f010b939", + "last_modified": 1588004938434 + }, + { + "guid": "/^((jellydsg@gmail\\.com)|(FreeZvoni@kust24181002)|(FreeZvoni@kust24181002vat)|(\\{c982c69c-aa50-48b4-b09f-1d2da9019668\\}))$/", + "prefs": [], + "schema": 1587992188104, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1633393", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1d6aadb9-6bff-4050-a040-34dcf255de8d", + "last_modified": 1588004938424 + }, + { + "guid": "adblock-6.6@addons.mozilla.org", + "prefs": [], + "schema": 1587736485695, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1629925", + "why": "This add-on violates Mozilla's No Surprises add-on policy", + "name": "Adblock by Adblock" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9e5d0c15-a204-4e08-8342-069927ca1cb9", + "last_modified": 1587746716524 + }, + { + "guid": "/^((\\{101e4d89-674e-4284-8c4c-8aac12048410\\})|(youtubedownloaderhd\\.addon@mozi0lla)|(\\{65960fa3-6ea9-4c57-b7d4-2e7af34772ff\\})|(\\{0fa50bbb-b7d2-4616-9603-1194a1198a4d\\}))$/", + "prefs": [], + "schema": 1587670892686, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1632826", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d7cac810-a22a-45e0-94d4-acdcbd5f4076", + "last_modified": 1587736485219 + }, + { + "guid": "fvd@download", + "prefs": [], + "schema": 1587733828378, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1632827", + "why": "This add-on violates Mozilla's No Surprises add-on policy.", + "name": "fvd@download" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8701caa3-4210-478f-8e60-d829e1b63cac", + "last_modified": 1587736485214 + }, + { + "guid": "/^((lsoares1995@gmail\\.com)|(amqp-dwn-all-vd@michael\\.slidan)|(\\{cfd42eed-18e4-42e0-a0ff-3ec6ecc8f806\\})|(\\{ed73da0b-36bd-432b-9215-f778760c7053\\})|(\\{84d3da4e-7e3b-4712-8e3e-5cd30a4a6546\\}))$/", + "prefs": [], + "schema": 1587733948804, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1632828", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "faca9e29-818a-4897-bd98-bae27046dc56", + "last_modified": 1587736485210 + }, + { + "guid": "/^((\\{fb07cc6e-491c-4826-951a-65f60e85c81b\\})|(\\{11dfb55e-0ded-4a7d-be63-e57a5dd67d5e\\})|(\\{349526f1-bac0-459d-b2d0-ee5a27b54ba2\\})|(\\{3a8fc1bc-866d-4531-b0d9-6b36bc8a8884\\})|(\\{773df6a2-1d9c-4b98-aef8-4c44c92b7ea4\\})|(\\{a0774577-adeb-4d0a-b191-70762ac80fb4\\})|(hbehjppehnealjnhnoopckhmhahgacac@chrome-store-foxified-13858238)|(\\{d93ef4bf-3fe8-4400-86a5-886f2a058786\\})|(\\{e985e17f-7073-45dd-addc-4b1ec9ba1f01\\})|(\\{9a6f884b-3b8a-492a-8f7e-a975e2f6ec20\\})|(\\{bf441351-19c6-4245-8f83-b4a66ccb43fb\\})|(\\{dedf3f79-4629-4628-a4c6-695a4f03b424\\})|(\\{20b844c3-411b-4632-897f-c47070f72403\\})|(\\{81dcf023-18c0-457c-a883-6951269402d6\\})|(\\{81dcf023-18c0-457c-aa83-6951269402d6\\})|(\\{75ca571c-556c-4901-8c17-0fd7a2d845b0\\})|(\\{f2155d36-a230-4ae3-a743-3d146648c45d\\})|(\\{9fe202c1-1cb5-4645-a9aa-c3034f9f3ec2\\})|(\\{0e064fb4-7cdf-429b-9739-8d8aca9055af\\})|(\\{40cb7315-3bd5-4b9e-99e4-3bfabbb7a554\\})|(\\{f9a897f7-2ce7-4eb0-948a-218855b16bdf\\})|(\\{b038d2dc-46f7-4f96-b159-0fee836bbe76\\})|(\\{b954032e-3a94-4b85-b212-d0cd29f23750\\})|(\\{6072ab6b-4987-44b1-97c8-a26ede805966\\})|(\\{e6ccbdbd-3644-4ccf-b442-c5e733e68a41\\})|(\\{89e9630a-36f9-454d-8c88-58681e679d84\\})|(\\{3e95cd89-4058-4059-97fb-80fe2c6bd608\\})|(\\{e3b78ccc-10eb-4022-9fc2-9bfacdf2d042\\})|(\\{8bca4db2-2139-4410-b7a2-af6c21dd5c2e\\})|(\\{63a6a8e8-918e-4b51-a616-1e0125302eb1\\})|(\\{8c65c4b7-6680-4d7d-a2c3-3b6ea8d7cd44\\})|(\\{2a968879-9525-4a34-bf8b-bc173f9c4c35\\})|(\\{cb4d0255-eba7-4ee6-8a4e-ba586acb4d2a\\})|(\\{84a859ae-c6fe-4adf-a779-8a08e9028778\\})|(\\{bb1ac3b5-96b9-437e-8d34-39ebd37aea42\\})|(\\{f3fdd60b-f051-48b5-8c6f-9294ac7ddf8b\\})|(\\{159fad04-dfe7-49f1-bae7-8b4b15f28ca8\\})|(\\{2a2f7c8d-faf8-4faf-aa39-c8149db9a18d\\})|(\\{cd0ebad1-7a39-40a4-a88e-343abb6fe6f2\\})|(\\{be0d4d36-f71e-41a7-8a4d-56c0b885dead\\})|(\\{17cb6906-27f8-4b17-bbaa-983939949c67\\})|(\\{3ab14e91-cbe8-4a83-8c82-41210e237673\\})|(\\{5bd93da0-9900-4e78-a6d3-1b9de3ce471e\\})|(\\{e22aeabf-fe0f-4ea6-a5da-124e733a6856\\})|(\\{ee85b096-d78a-4123-b077-8d74d2c86331\\})|(\\{8f908c81-ac8a-4d0a-ad0c-e8f26df841a0\\})|(\\{23b304bb-3887-4854-8867-a0cafcb021b9\\})|(\\{af79e4cc-af3b-4712-a803-169c4556c578\\})|(\\{d7d93c78-ecf8-4727-8ad0-de0706e81e6b\\})|(\\{02ae80ae-8ad2-4882-87cf-3cab6d037aac\\})|(\\{6bb6d370-8dec-4e2a-a8fa-065871304ff2\\})|(\\{898a9073-415c-4300-b569-5ce013da6bd7\\})|(\\{a3d7c2e5-7803-4348-bea4-5dedccd64a50\\})|(\\{6f16edea-ef55-48f1-99fc-75eca7439412\\})|(\\{7d24c39b-9291-48d8-b5bd-81628c397228\\})|(\\{a44b19a5-8e1a-432b-bab3-17e760c7b747\\})|(\\{d7dee400-d09f-46f5-b702-0d81ef94976e\\})|(\\{f618b6b5-faef-4a6d-917f-07b1f95f1489\\})|(\\{2ccedff5-407f-4194-9eca-c2563a4b51ee\\})|(\\{43d9105a-020e-4137-b137-ada41fc6d05d\\})|(\\{af15cd0d-feb4-441a-8692-6832a5cece5f\\})|(\\{3629dde6-0659-449d-8bfc-3d09bcfd1bf3\\})|(\\{a586c658-b570-4d43-97c9-f78f99d0e99d\\})|(\\{05f3ec76-9a51-4687-8b74-ad7c4143ca01\\})|(\\{7b6939db-dc52-4a55-b8ae-5aa53ed408d0\\})|(\\{261a9b0b-c37a-47e5-b779-06595b35679d\\})|(\\{d8ba0ffc-e4a1-4ef8-8f5a-fa6e03305faa\\})|(\\{eb2bb212-9b5b-4b3c-a864-41a15d88098d\\})|(\\{95a84bda-ea71-4c0e-aacf-6123e2f710f5\\})|(\\{2c17b106-4da9-4e45-ac9e-c6afe258cd79\\})|(\\{be877377-f2c7-40ed-904e-5a0573fb5bf0\\})|(\\{f7f6572d-8107-486f-9dba-ea8aa5577d68\\})|(\\{69592649-9f3f-4013-86cc-add248067d1f\\})|(\\{d5d73232-b950-4a37-8300-b66c084061c7\\})|(\\{a1b0aa8c-546b-4a41-a9d5-de8c97638235\\})|(\\{e422f1b1-c290-413b-85cd-17ba4a994e40\\})|(\\{aff1e2a5-450e-4406-a90a-07e1e2a6af99\\})|(\\{2da2bbe4-b8a4-4b86-a340-8b4973e7acbb\\})|(\\{67671ea3-34ca-4cf3-8a92-f1ea15fe05d5\\})|(\\{7eae263a-29a4-4cf4-8ae6-651dae80f3f0\\})|(\\{1e5a12b3-6ab3-4de4-af96-19204f611aaa\\})|(\\{32d829ea-7c44-4510-b199-a212400315c5\\})|(\\{a949a26f-941c-4925-bde6-6530315038eb\\})|(\\{94216c39-e7b9-4cba-85de-ece3cbc99392\\})|(\\{6f95b9c5-7271-48f9-bf5d-e1f2475b246d\\})|(\\{9f3560af-0dde-4462-9c94-fd3eb666cab9\\})|(\\{e7efc2fd-15b3-4883-8ab8-d5f6b032719f\\})|(\\{bc29a9c7-87ef-4fe9-b4a3-716450825df6\\})|(\\{acdec1eb-b577-46ba-afca-7083a4f435a5\\})|(\\{523d358d-5c8c-4ab9-b450-72c9612ee451\\})|(\\{ce5e1f02-eaff-47cd-9244-e62972cb6527\\})|(\\{821bed9b-7d77-4096-91e3-e85908748969\\})|(\\{51a062cc-a286-4a29-b4f4-be266be03b28\\})|(\\{665dc8f5-b56b-40f1-8afe-7c6ef7b563c0\\})|(\\{93387229-dc8f-42bc-841c-6d95b1b05ca9\\})|(\\{dc46f742-4dad-435f-8827-205c1455fde8\\})|(\\{0b5d0ca4-df53-412a-a575-9cda5174fb7e\\})|(\\{9b3fb49b-90a0-4558-80ce-5357b1ea0886\\}))$/", + "prefs": [], + "schema": 1587498092559, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1632212", + "why": "This add-on violates Mozilla's add-on policies by executing remote scripts, collecting search terms going to a third-party provider or expsoing add-on settings to web pages.", + "name": "Add-ons violating Mozilla's add-on policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "00d26716-a2d9-41cc-b588-9d27b572ad19", + "last_modified": 1587573362859 + }, + { + "guid": "/^((\\{d8cf9bb9-3569-4639-8862-c7ddb82d5f1a\\})|(\\{bc3a1465-8945-454e-bc5d-a0619782e141\\})|(\\{8eaab1d0-29f9-4a36-af6d-5e2ed425c900\\})|(\\{947567da-3457-4eab-ff56-987452456322\\})|(\\{d35cbb3c-3c67-4e07-8386-966c86318f8b\\})|(\\{d065767f-8bcf-4990-bd5a-68bcf0a07fb5\\})|(\\{7b34b6f9-b93e-4dda-975c-4ae2b8496eb2\\})|(\\{bf5c4a48-5096-4e13-a14f-b21f707e7c09\\})|(\\{0cd0d26a-303a-4a60-b164-05973b7c2181\\})|(\\{ab3736f5-0a14-4747-ab9e-7f73ce289e11\\})|(\\{40e868db-380b-4564-aa42-d8efb0448f9a\\})|(\\{56637a41-c7c4-4046-b2bc-2afebdc45f82\\})|(\\{7bf9bb0e-449b-4864-8bf8-1e9584e0df75\\})|(\\{96f0a636-16ed-4150-afa7-2dea4004d42e\\})|(\\{8d52e6a6-733e-467c-a31c-1626e62d3dd9\\})|(\\{a8ccbb2e-699c-4e0d-a6c5-a4b2538dc59e\\})|(\\{c9dfecd3-8dfa-44e8-b57b-62cf6b9eaf8d\\})|(\\{b90bd179-d092-47d6-9247-7b80081490e0\\})|(\\{a52db3fe-a21b-41be-8db0-c522c918e24a\\})|(\\{0bbb2670-a281-46b1-9b94-7b0f1a5094c1\\})|(\\{18bd2488-e103-4f15-b59c-eb1a5e037166\\})|(\\{58d1ae16-88c1-4b53-848f-6148e7b9b24c\\})|(\\{d76b83aa-bbd1-4370-818d-e8d332015451\\})|(\\{d7dddf2f-e1bf-4475-bde1-fa7fdac7d8e8\\})|(\\{5cba1e13-f8d7-442a-b705-663c764b0dd1\\})|(\\{12ea2aa2-261a-4fe2-b137-abdbbf6f90a3\\})|(\\{69e2be54-a38e-434d-a26e-052105153f97\\})|(\\{4582aaa7-9688-4038-a9ef-06345fa0f400\\})|(\\{d4a978ec-2aa5-40cf-9970-0c4b425041de\\})|(\\{29eb0733-95e1-4540-b88b-f7d95f5c89fc\\})|(\\{230dc693-8671-4c93-95e9-273a1cd0d637\\})|(\\{2e214782-9160-4c24-a234-0bf9f7a7d7d9\\})|(\\{3e5e63bc-323f-4d46-b757-2a47dd6079fd\\})|(\\{5baeae2e-d152-49ea-ad10-ec124abaa5a9\\})|(\\{cd75a0fe-4d14-4ac9-9633-868c45145ad5\\})|(\\{3bc54878-fb86-4696-9e95-bad0da699159\\})|(\\{1a55f734-685d-47cd-8077-7223f5719042\\})|(\\{7e92bba1-1f5b-476b-af4c-66a3fb87c883\\})|(\\{6b34fc9e-74a8-4172-8e03-a18b3df33507\\})|(\\{261918a4-11ce-4ade-9c00-a38340e76c8a\\})|(\\{3f245e99-96b8-4902-a9a6-905f8f3b7bd6\\})|(\\{a927e0b6-c765-4117-9204-44559abbb760\\})|(\\{3b725149-d305-44e5-81e2-5a65193d98bc\\})|(\\{c9ec7390-e443-42c7-81d3-0c04a21ccb6d\\})|(\\{e67c019c-88ce-410f-a193-f315eef5b307\\})|(\\{2560ecd5-d3b5-497c-b2bd-3655ce95c7c8\\})|(\\{c9182ac6-2d90-45ae-b9d2-8b1630fc2ab9\\})|(\\{7d6e8e6e-e8a3-4109-aed3-807404275242\\})|(\\{ffee5e17-64c7-46fe-9e0a-3b5163913a09\\})|(\\{cbeeff4e-d66f-4e69-8faf-e76e388cea55\\})|(\\{40000ae1-c1dd-4edd-9939-4361c51ac36d\\})|(\\{ddd9f4a1-bb11-4f9a-aa25-bbf294ee77d3\\})|(\\{70a5d868-74a5-4579-bf6a-9306c7d878bf\\})|(\\{1c88d738-5203-4b61-8a47-47e0c2f870c4\\})|(\\{b397e8f1-5133-40d9-ab94-c50acf74602a\\})|(\\{bec4f263-f7ea-4fa7-b1d6-b86ac5c599fb\\})|(\\{a9d6068b-5264-4e42-9d19-902e84a63225\\})|(\\{40b7988b-531f-4653-bc24-46c5fc1e828e\\})|(\\{f2ed03be-0923-4a76-b9ac-2b9bed2a0898\\})|(\\{8bcc5597-0617-4032-ab9f-2c588fbde1e0\\})|(\\{c5f476fb-9767-4a47-a277-41752c588022\\})|(\\{2ddb229d-27c0-4a03-8672-5bca0ef65e60\\})|(\\{1738bd80-2569-4d7b-8c15-d73af8a7c974\\})|(\\{93aca1b6-fbf4-4a2a-b7c4-05a908af4d55\\})|(\\{d68eb2a0-a533-467e-be53-0551c7b2b14e\\})|(\\{d47f20ed-b6ba-4ede-8568-747f0ab6b110\\})|(\\{03570c25-3fc2-40ad-acb7-35d5544faddd\\})|(\\{67d0bded-ebd6-4173-b10e-c288319478ec\\})|(\\{c331baaf-b89e-4ec6-9636-fb18d259cb90\\})|(\\{c1ef737c-c8e6-4577-9c34-66c1c31d2ca8\\})|(\\{389218e4-e2b3-4a14-af5c-80917883dff9\\})|(\\{65d8b6a9-7109-4f60-bb56-c22a360435b7\\})|(\\{f469578c-3712-469f-b377-51ccf97d268d\\})|(123search6010@coinup\\.org)|(123search6011@coinup\\.org)|(123search6012@coinup\\.org)|(6013@coinup\\.org)|(\\{9413e9e0-24c3-4e7b-8f33-5e4270ae0921\\})|(\\{a68392cb-63ba-4f93-adef-bb170ffedddc\\})|(\\{f10c2e43-884f-48f7-ad36-7627880b53d0\\})|(lookmoviedssxml@coinup\\.org)|(coinupsearch6013@coinup\\.org)|(newlook03@7thsense\\.media)|(animeio6015@coinup\\.org)|(picto7100@coinup\\.org))$/", + "prefs": [], + "schema": 1587325291483, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1631422", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "53340de1-6a6d-4528-a587-f17216c6a32a", + "last_modified": 1587485550892 + }, + { + "guid": "/^((\\{e79975e9-e40d-452c-a995-5895c8d110d5\\})|(\\{a36d5211-070b-4021-bca1-1b73b2ce4d73\\}))$/", + "prefs": [], + "schema": 1587388543147, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1631425", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "52e5afe6-e024-40b4-948d-d190aac71b5f", + "last_modified": 1587485550889 + }, + { + "guid": "/^((@realonlineradio)|(@searchanonymo)|(\\{947567da-3457-4eab-ff56-987452456444\\})|(\\{be6be898-315f-48df-b515-6004129ce31d\\})|(\\{9c1b818d-45f7-4c4c-8ad4-5eff683c4607\\})|(\\{55220873-2dfc-4037-b09f-bd50101ba7d8\\})|(\\{0fe62f39-10f3-4a15-8ce3-2e789db17ce3\\})|(\\{124a5ba8-a611-4e9c-889b-ce94e3903ff7\\})|(n18TMqBF@nature-wallpapers\\.com)|(\\{4b1b9905-a554-468f-a780-7b1f498428da\\})|(\\{c4c0caf7-856c-4b33-bd5a-c2e4b8ca20bb\\})|(\\{993da426-2f79-4337-8da3-f06f1001e6ef\\})|(streamplus0305@coinup\\.org)|(streamplus1100@coinup\\.org)|(access1100@coinup\\.org)|(\\{c6ee999f-a247-4077-8229-20c8f13de509\\})|(access1100-33vidia@coinup\\.org)|(prime@valid-install\\.com)|(\\{0a4082a4-82ad-4ba3-94a2-5b0dba7e0953\\}))$/", + "prefs": [], + "schema": 1587389169680, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1631746", + "why": "This add-on violates Mozilla's Add-on Policies by executing remote scripts, collecting user data without disclosure or consent or collecting ancillary data.", + "name": "Add-ons violating Mozilla'S add-on policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2b0c70da-21e1-491e-a186-4fd5af268d7d", + "last_modified": 1587485550885 + }, + { + "guid": "{612dfdb7-81eb-459e-8d81-dc4ecb62dc9b}", + "prefs": [], + "schema": 1587475321531, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1631789", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Best Youtube Downloader Free (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e241d95d-cb5d-4220-a4d7-f2bf89844f96", + "last_modified": 1587485550880 + }, + { + "guid": "/^((\\{529b261b-df0b-4e3b-bf42-07b462da0ee8\\})|(\\{8f4fa810-9c36-4b81-8e72-de97c8f29d67\\}))$/", + "prefs": [], + "schema": 1586987031329, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1630427", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.\n", + "name": "Universal Bypass" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "13.8.4", + "minVersion": "12.0" + }, + { + "severity": 3, + "maxVersion": "13.10.2", + "minVersion": "13.9", + "targetApplication": [] + } + ], + "id": "59b96bbc-20bc-4433-8596-79990aa0f670", + "last_modified": 1587125503251 + }, + { + "guid": "/^((\\{329fc2d4-30d7-48b7-8247-4a4bb7682da8\\})|(\\{5738de0c-38f0-4e38-a01e-8553ec09235c\\})|(\\{bc775a74-c002-4e99-bf4d-ad9a806263dd\\})|(\\{1272ff2b-0339-4d10-9297-cb0e90a2be0e\\})|(\\{e5771c28-c725-43b0-b8d4-5d4acd072b7d\\}))$/", + "prefs": [], + "schema": 1586806901368, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1629790", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6a02c6a6-ef2e-4b46-ab67-49157ed9e49c", + "last_modified": 1586977183602 + }, + { + "guid": "/^((\\{39262546-d73e-4259-ba57-b772473106a3\\})|(\\{f6e51fb0-9724-4c86-aa95-12d20c77a7fa\\})|(\\{805466a4-90a7-48a3-85bb-bad244244a85\\})|(\\{60cfdc21-74c4-42bb-9b9f-9688f1a22e17\\})|(\\{c96f35f7-9ca8-4119-8d08-aaf1f5231fa7\\})|(\\{057b25cd-d923-4faf-828b-47d36f069a40\\})|(\\{1c5a9f06-a63b-49c4-9d66-563c7a9ed059\\})|(\\{61e204ba-f49f-41e4-8d38-0cbf003e3ea8\\})|(\\{139b5a73-e8ae-496a-b0eb-2bc4de518fc6\\})|(\\{59b82710-8c63-4d82-bc16-64f6bda5cd20\\})|(\\{bfe32796-ee0b-4271-a5d2-81938374395f\\})|(\\{ef5353b7-cf92-40e9-8f5c-93210015b15f\\}))$/", + "prefs": [], + "schema": 1586956158067, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1630241", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Cryptowallet Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "17d89613-a8c6-4e49-a5c7-e9eac64133f1", + "last_modified": 1586977183599 + }, + { + "guid": "/^((spylog@cfsoft\\.com)|(\\{7cb9bddc-8c0e-4b4e-92df-3735e3db909a\\})|(\\{8c64047e-20da-45f2-998d-f852e0886c70\\})|(\\{684a1175-31b9-478a-845f-ab332d0c0ec3\\})|(vni@vietbacsecurity\\.com)|(\\{95e9e74d-4318-4f14-939d-24ec61ed6657\\})|(blockpopup@vietbacsecurity\\.com)|(blocallpopup@vietbacsecurity\\.com)|(Vanga@vietbacsecurity\\.com)|(sontung@vietbacsecurity\\.com)|(TiengViet@vietbacsecurity\\.com)|(Pomodorotimer@vietbacsecurity\\.com)|(Clock@vietbacsecurity\\.com)|(alarmclock@vietbacsecurity\\.com)|(gotiengviet@vietbacsecurity\\.com)|(location@vietbacsecurity\\.com)|(Amlich@vietbacsecurity\\.com)|(lich@vietbacsecurity\\.com)|(lichx@vietbacsecurity\\.com)|(Bitcoin@vietbacsecurity\\.com)|(thoitiet@vietbacsecurity\\.com)|(Bitcointovnd@vietbacsecurity\\.com)|(font@vietbacsecurity\\.com)|(porn@vietbacsecurity\\.com)|(TiengVietkhongdau@vietbacsecurity\\.com)|(Calendar@vietbacsecurity\\.com)|(007@vietbacsecurity\\.com)|(spymyself@vietbacsecurity\\.com)|(youtubedl@vietbacsecurity\\.com)|(adminaaa@vietbacsecurity\\.com)|(adminaaaq@vietbacsecurity\\.com)|(adminaaan@vietbacsecurity\\.com)|(adminaaank@vietbacsecurity\\.com)|(admindsa@vietbacsecurity\\.com)|(admindsak@vietbacsecurity\\.com)|(admdin@vietbacsecurity\\.com)|(admins@vietbacsecurity\\.com)|(cotuong@vietbacsecurity\\.com)|(\\{a127afcb-7284-40aa-aac2-cbe0fdd4491d\\})|(\\{9c07f8ad-57dc-459f-b07c-96900e8a3806\\})|(\\{c2c59ff7-83e6-4988-8c1d-473d8ac76bba\\})|(\\{8f0e655a-22ee-43bb-97fc-80af74a4dc34\\})|(\\{49253a6c-eeb9-465c-bcf9-258f731dad9e\\})|(\\{c5542677-064b-4fce-876b-3e1ba30371a4\\})|(\\{7b786f87-2016-4752-a781-5f8b4c63241e\\})|(\\{4b768602-8729-4852-8587-d55169ce67e9\\})|(\\{04c37c01-32bd-4e4a-94bf-36d8175b0555\\})|(\\{743aeddb-e150-40e0-8091-e8b52e7225a4\\})|(\\{1b7f9ca4-0a14-4ef9-8ec3-3cd05efb683c\\})|(\\{e843a1e9-21a2-43ad-a1ff-34e430eae000\\})|(\\{92a420a0-5075-4126-8318-2ce8b4ef8746\\})|(\\{f204e4e2-c9d0-4a12-bbdf-20ae0346171e\\})|(\\{045504f3-13d3-420d-a8a2-74e6e548f5f8\\})|(\\{44c67012-6467-4925-9284-1ad3785e02fd\\})|(\\{709baa43-e321-4d09-80ad-bf41c93d49d5\\})|(\\{7679dbca-1b6e-4e9a-ae70-8fab1e541051\\})|(\\{46d6f718-bbf8-42de-85bf-07c7e2ee3db1\\})|(\\{9b4d83fa-395f-4c86-a897-7927ce9383e3\\})|(chudaibi@vietbacsecurity\\.com)|(\\{f55e480d-37ff-403c-9601-1380c8119b7a\\})|(\\{1c52bac9-b511-47e6-9492-51832b6d73ae\\})|(\\{f09be92f-8d2a-4294-b787-dee323ca52ef\\})|(\\{14e65683-b5ee-4e41-8083-c8138b0a30ed\\})|(\\{1c65312f-bbf4-4447-9fe5-433234eb4562\\})|(\\{f7d105c4-d341-4d17-b0ee-257cb1473a07\\})|(\\{12b5579f-eb13-4399-a902-2f36c3ad3447\\})|(\\{e3d1a35f-50eb-4125-9b0f-d26a59f88823\\})|(minh@41batrieu\\.com)|(\\{abb3fe45-b511-4d21-aa0c-0cd1fed6dffb\\})|(tocao@vietbacsecurity\\.com)|(\\{87cb22c4-7395-4658-bf65-a2c27c8f1e8c\\})|(\\{9324c255-2e6e-4b27-b4fa-5af7f6e167c6\\})|(\\{02d3a0c5-1f60-4824-b37f-9dc2c67ef9f2\\})|(\\{711e1a95-bc52-44de-bb17-ea24ed493e65\\})|(\\{c2fff0c1-601f-42e4-878f-c1cdd624c279\\}))$/", + "prefs": [], + "schema": 1586962120860, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1630297", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1c016cb6-d9b3-4a24-a887-1649b3ac2134", + "last_modified": 1586977183595 + }, + { + "guid": "{ab2186b0-8c0b-4921-a2d4-95e6e05c0e3c}", + "prefs": [], + "schema": 1586115691726, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1628040", + "why": "This ad-on violates Mozilla's Add-on policies by collecting user data without disclosure or consent.", + "name": "Add-ons collecting user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "baf700c9-b165-4569-a247-8ced9b94fab1", + "last_modified": 1586290176418 + }, + { + "guid": "/^((\\{ddf1ef2e-af32-dd2f-0640-85ef6d10fb02\\})|(\\{b511c8c7-d036-7302-a1cf-4aefb16e8658\\})|(\\{1e939f19-1391-b8ba-3790-5d4404c43bb1\\})|(\\{7da26e3b-a032-bad1-857f-d19328d0ee12\\})|(\\{ff4cdb7a-d968-140e-6709-eea6c7e0d1fc\\})|(\\{63a8cb2c-5a8f-d848-272d-310e846a7bfb\\})|(\\{07b11f4d-d53b-f8df-1bc8-10773768a2d6\\})|(\\{e74124a5-c58e-a28d-9e96-d0fcb73e6905\\})|(\\{4c9f5729-16af-e8ff-5e07-7ee7ccc06665\\})|(\\{76f93d6c-ace1-28af-5e1b-d09442925b1a\\})|(\\{694226c5-3426-466d-d826-4dd70cc4260c\\})|(\\{ea403745-59f6-af83-4190-ab3b9fafd369\\})|(\\{cbe1950f-9a99-9d66-4060-12b9e85b675b\\})|(\\{874a192b-50de-af0f-53c0-76b54b4c5b76\\})|(\\{13bf414f-8444-9f88-e05a-5fb8be3bef6a\\})|(\\{7aaab407-a72e-c07a-5971-77a238b093e1\\})|(\\{d4d1feb4-71b9-a8e4-089f-699483413d32\\})|(\\{c7eef1d4-370d-8005-26e7-67ee1b76ef4d\\})|(\\{941e84f4-e93f-2bd3-be34-efc9be9f8b66\\})|(\\{256ac5f6-5fcb-44ac-eeb8-c65f4470c7e1\\})|(\\{84a0bb93-9422-8d02-b607-fd98f854837a\\})|(\\{c987166c-d73d-b9c1-0e21-51e9ea4d2df6\\})|(\\{8cc0f1c4-990a-5fb4-eb3b-bb455da8c268\\})|(\\{5b6512ff-d5fe-bebb-3862-43a9d7a7bcb4\\})|(\\{6885fd10-acd9-2987-0a27-583828c051df\\})|(\\{945b997e-4cf8-1476-76e8-4c0e489d57b8\\})|(\\{284e1fd3-5f68-c175-1cd5-9f8340543d61\\})|(\\{3b5f5ac5-9b7e-0d10-678c-100f39949103\\})|(\\{e2eda9e6-76b1-c5e0-6ab5-4a119db1f094\\})|(\\{e7c89b14-f86f-8f9e-4aa7-bf93f058c403\\})|(\\{57711d16-ab2e-7841-ddcc-3d245c9e53f4\\})|(\\{30017eef-489f-1557-cd23-bd588933e30d\\})|(\\{a376fa1e-0214-3b11-3260-f464aa202ff0\\})|(\\{ab64c65f-a4d5-ffee-61ec-635208ee910c\\})|(\\{c31b8bd4-6ba5-414f-5929-3b108372cfb5\\})|(\\{ef96f34d-6b17-687f-fdc2-b2286ef7c68e\\})|(\\{8238128c-7659-fb9c-a29e-ce5844273852\\})|(\\{27bbe28b-181e-6e1c-be31-bdd6404004b6\\})|(\\{d70e5451-61bb-9c6d-0913-4767c47d156a\\})|(\\{9c705bfa-89f2-a7c0-329d-d7dc4ad0b32b\\})|(\\{4af055a2-e1f3-6c2c-484a-dd12d651085d\\})|(\\{46a61b09-8b11-2334-9c1d-d057ac644702\\})|(\\{cca52e1f-857c-3654-3f8f-ba32136d0160\\})|(\\{04a32ec2-76c5-a8e0-49ac-aa51b36cf91c\\})|(\\{fc4bfc3d-666e-2687-5f77-2514854794ac\\})|(\\{8f11842c-4a65-d7b5-c12d-6fab43a5f51f\\})|(\\{8d08e6d2-e82f-ede4-089b-23184112b0b5\\})|(\\{7dd7b266-a9da-451a-47e7-b56fb5c77a40\\})|(\\{55636e59-2fac-a595-6a3d-42f4359b7f3a\\})|(\\{a6b75d85-0607-a71a-087e-6ecd5cd525ac\\})|(\\{b39ab5a0-3902-15c3-6fc1-ce45924dcf22\\})|(\\{90f781a4-3d9f-a1c1-c4a6-b446223bee6d\\})|(\\{047bdfe9-b256-d910-a2ff-aaef394e37bc\\})|(\\{3496aee8-1e4a-4a2d-d935-e9c60a1007da\\})|(\\{a197c072-4b06-440e-391e-d60a64bdcd6a\\})|(\\{13fd2e82-d19d-6ad0-b77a-3caaab77ee00\\})|(\\{0ac1543f-5b68-c8b8-9268-af4b51a0233d\\})|(\\{c3f2a253-e235-d612-9d44-e1d5a28fd883\\})|(\\{5ed4b053-1445-dd12-2118-9d697d40736b\\})|(\\{7c2c341e-c8a7-7806-cf00-0ed3a16496c9\\})|(\\{c1d30a2e-4b27-3730-da00-93c350122f57\\})|(\\{63b32739-1c43-d874-19db-5dd4d1ef2abc\\})|(\\{92bae4b7-ea7d-3fdb-b34c-20293af43734\\})|(\\{5fd29856-2296-49cc-37ee-4d0e4dc738ef\\})|(\\{c4487a30-f901-f358-8a05-23ac3cfa9469\\})|(\\{4a2169e8-1a23-378a-bc4b-5d3b817193a5\\})|(\\{5917dee3-e834-7514-6768-2324f39ebe90\\})|(\\{2986179a-af19-6571-79f8-1bb75de24f88\\})|(\\{8cfbe90c-eeb7-30b2-31ce-da719c1a63cf\\})|(\\{4617e32e-8a17-1fe9-d89c-7e8fffeca945\\})|(\\{de3d433a-af12-a27c-8886-113668435e8d\\})|(\\{52326034-27a5-2035-473b-46cf569866b9\\})|(\\{58172e8e-3cfb-9a0d-90a7-d9ae64537326\\})|(\\{530c001f-94f1-f453-dbc6-b89c30c36cf9\\})|(\\{38ea3f7d-6f09-2f03-e75f-de2bad37e84a\\})|(\\{82e77989-fe96-1895-c958-6a9a7c8955c5\\})|(\\{1baf2f92-1872-b460-7d6c-8fc1ccacedc9\\})|(\\{6f8c3be7-5264-bf34-b488-8991ebccb7b5\\})|(\\{82f650b6-57b2-3160-843b-c6b9c0aafd28\\})|(\\{ce0fda96-6059-e2bd-b61c-7e0ea9426c48\\})|(\\{77fe55df-763b-c6e1-6d5d-540d86a3f4d5\\})|(\\{3028912a-4002-ee0e-4a5a-ea060ddc1ed0\\})|(\\{8049c65e-6536-1556-f5a0-82745ba3a711\\})|(\\{43962ebd-ae57-d2d2-7660-8b501c66090c\\})|(\\{3be51fdd-4325-9022-ce8a-94fc9406d0ff\\})|(\\{4bbf76c6-0cda-6b55-a82d-6a02869ba851\\})|(\\{627f651e-58c1-4f10-81d9-8fc4a2b8a6d4\\})|(\\{b3d9f42b-0cf6-5f80-0645-6870904c6d99\\})|(\\{f24bbc96-20bc-4510-a275-442a604aa12c\\})|(\\{a97e6c70-d890-4a69-b7ae-ff97bb25808e\\})|(\\{95674029-cc9f-4e46-9a64-b7a4b3e39277\\})|(\\{f8f40912-e963-48d3-b176-fe7223e54d94\\})|(\\{8a1eed18-914e-4a58-aac5-791272bc5c26\\})|(\\{3267ca67-cf6c-451b-8ba9-311d3b954e8e\\})|(\\{00969488-23d9-4291-a6d7-abfbb9d75fd7\\}))$/", + "prefs": [], + "schema": 1585730548980, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1626592", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "df94545c-e570-4efe-afd4-11ab0e4f9ddf", + "last_modified": 1585930042641 + }, + { + "guid": "{7fa7ce90-e40e-44dc-8b79-84337f3de987}", + "prefs": [], + "schema": 1585743918356, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1626597", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Decodex" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ab0258b9-63bc-425a-8dc4-53250d2f27e2", + "last_modified": 1585930042636 + }, + { + "guid": "{6f62927a-e380-401a-8c9e-c485b7d87f0d}", + "prefs": [], + "schema": 1585744575197, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1626602", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Lookbox" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3486ee79-b128-4496-985d-6f5d9dc2c340", + "last_modified": 1585930042632 + }, + { + "guid": "{68d4ca8c-c012-43a7-8f22-d569a5fe3eb6}", + "prefs": [], + "schema": 1585745131075, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1626603", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Maximize" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "39e5eda4-c9a8-4a8e-a6d5-fd3d0d3b0215", + "last_modified": 1585930042628 + }, + { + "guid": "{171cdcfa-d6d3-4bc2-8e5d-dfcbb67c7695}", + "prefs": [], + "schema": 1585597291445, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1625509", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Sushkom AV" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a51c888f-234f-4f0b-a76c-c0a036bfa6ca", + "last_modified": 1585730548574 + }, + { + "guid": "{b7410e57-0452-47d5-a7cf-a1a91e7fd0b7}", + "prefs": [], + "schema": 1585424490920, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1625922", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "eaa5e3c2-1be6-4eb9-9357-f2d0686448be", + "last_modified": 1585575900465 + }, + { + "guid": "/^((\\{e2e34cff-c7d2-4c87-8195-d206ac928969\\})|(\\{aea3f9d0-909d-4c39-9282-25b11aa29d61\\})|(\\{ec5c4eed-0275-4148-a7f0-b21c05121703\\}))$/", + "prefs": [], + "schema": 1585158977245, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1625025", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name or collecting ancillary user data.", + "name": "Deceptive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "034951cb-7614-4202-af35-18123476eb03", + "last_modified": 1585238797372 + }, + { + "guid": "/^((\\{927f6d6a-104f-4e69-b99a-e62e856899d9\\})|(\\{edf2d18c-ba07-4f30-bd59-85520b7259d8\\})|(\\{2128e9bc-aa97-4f9e-9327-bdce611538fb\\})|(\\{41a71134-b81e-49c5-be44-154c1b981153\\})|(\\{e00696fa-7731-40c6-81f1-5507d0435347\\})|(\\{f6dee727-c6cf-4d4a-b108-d59c3d9723ce\\})|(\\{48935666-26c5-484a-ade3-1660eca6a219\\})|(\\{06b9fcaa-45df-4fb3-a8fa-775d68690f5b\\})|(jid1-4P0kohSJxU1qGa@jetpack)|(jid1-93WyvptyvzGATw@jetpack)|(addon@ytdownloader1\\.info)|(\\{f4817c52-46ac-4b98-b682-f900701c5778\\}))$/", + "prefs": [], + "schema": 1585177209604, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1625186", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "abcf90de-7322-4232-a59c-06e4b3a33c68", + "last_modified": 1585238797365 + }, + { + "guid": "/^ext@bettersurfplus$/", + "prefs": [], + "schema": 1585140126652, + "blockID": "i506", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=939254", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on appears to be malware and is installed silently in violation of the Add-on Guidelines.", + "name": "BetterSurf (malware)", + "created": "2013-12-10T15:10:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b4da06d2-a0fd-09b6-aadb-7e3b29c3be3a", + "last_modified": 1585158976536 + }, + { + "guid": "/^((\\{7aeae561-714b-45f6-ace3-4a8aed6e227b\\})|(\\{01e86e69-a2f8-48a0-b068-83869bdba3d0\\})|(\\{77f5fe49-12e3-4cf5-abb4-d993a0164d9e\\}))$/", + "prefs": [], + "schema": 1585140060946, + "blockID": "i436", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=891606", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow the Add-on Guidelines, changing Firefox default settings and not reverting them on uninstall. If you want to continue using this add-on, it can be enabled in the Add-ons Manager.", + "name": "Visual Bee", + "created": "2013-08-09T15:04:44Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ad6dc811-ab95-46fa-4bff-42186c149980", + "last_modified": 1585158976531 + }, + { + "guid": "/^((\\{0b24cf69-02b8-407d-83db-e7af04fc1f3e\\})|(\\{6feed48d-41d4-49b8-b7d6-ef78cc7a7cd7\\})|(\\{8a0699a0-09c3-4cf1-b38d-fec25441650c\\}))$/", + "prefs": [], + "schema": 1585138327934, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1434759", + "why": "These add-ons use remote scripts to alter popular sites like Google or Amazon.", + "name": "Malicious remote script add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "32ffc62d-40c4-43ac-aa3f-7240978d0ad0", + "last_modified": 1585158976522 + }, + { + "guid": "/^(({41c14ab8-9958-44bf-b74e-af54c1f169a6})|({78054cb2-e3e8-4070-a8ad-3fd69c8e4707})|({0089b179-8f3d-44d9-bb18-582843b0757a})|({f44ddcb4-4cc0-4866-92fa-eefda60c6720})|({1893d673-7953-4870-8069-baac49ce3335})|({fb28cac0-c2aa-4e0c-a614-cf3641196237})|({d7dee150-da14-45ba-afca-02c7a79ad805})|(RandomNameTest@RandomNameTest\\.com)|(corpsearchengine@mail\\.ru)|(support@work\\.org))$/", + "prefs": [], + "schema": 1585145454357, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1458330", + "why": "These are malicious add-ons that inject remote scripts and use deceptive names.", + "name": "\"Table\" add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3a123214-b4b6-410c-a061-bbaf0d168d31", + "last_modified": 1585158976514 + }, + { + "guid": "/^((@mixclouddownloader)|(all-down@james\\.burrow)|(d\\.lehr@chello\\.at)|(easy-video-downloader@addonsmash)|(easy-youtube-mp3@james\\.burrow)|(gid@addonsmash)|(gmail_panel@addon_clone)|(idm@addonsmash)|(image-picka@addonsmash)|(instant-idm@addon\\.host)|(jdm@awesome\\.addons)|(open-in-idm@addonsmash)|(open-in-idm@james\\.burrow)|(open-in-vlc@awesome\\.addons)|(saveimage@addonsmash)|(thundercross@addonsmash)|(vk-download@addon\\.host)|(vk-music-downloader@addonsmash)|(whatsapp_popup@addons\\.clone)|(ytb-down@james\\.burrow)|(ytb-mp3-downloader@james\\.burrow)|(\\{0df8d631-7d88-401e-ba7e-af1425dded8a\\})|(\\{3c74e141-1993-4c04-b755-a66dd491bb47\\})|(\\{5cdd95c7-5d92-40c5-8e2a-8c52c90191d9\\})|(\\{40efedc0-8e48-404a-a779-f4016b25c0e6\\})|(\\{53d605ce-599b-4352-8a06-5e594b3d1822\\})|(\\{3697c1e8-27d7-4c63-a27e-ac16191a1545\\})|(\\{170503FA-3349-4F17-BC86-001888A5C8E2\\})|(\\{649558df-9461-4824-ad18-f2d4d4845ac8\\})|(\\{27875553-afd5-4365-86dc-019bcd60594c\\})|(\\{27875553-afd5-4365-86dc-019bcd60594c\\})|(\\{6e7624fa-7f70-4417-93db-1ec29c023275\\})|(\\{b1aea1f1-6bed-41ef-9679-1dfbd7b2554f\\})|(\\{b9acc029-d62b-4d23-b921-8e7aea34266a\\})|(\\{b9b59e13-4ac5-4eff-8dbe-c345b7619b3c\\})|(\\{b0186d2d-3126-4537-9186-a6f198547901\\})|(\\{b3e8fde8-6d97-4ac3-95e0-57b797f4c56b\\})|(\\{e6a9a96e-4a08-4719-b9bd-0e91c35aaabc\\})|(\\{e69a36e6-ee12-4fe6-87ca-66b77fc0ffbf\\})|(\\{ee3601f1-78ab-48bf-89ae-0cfe4aed1f2e\\})|(\\{f4ce48b3-ad14-4900-86cb-4604474c5b08\\})|(\\{f5c1262d-b1e8-44a4-b820-a834f0f6d605\\}))$/", + "prefs": [], + "schema": 1585149409880, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1476020", + "why": "Add-ons repeatedly violated several of review policies.", + "name": "Several youtube downloading add-ons and others" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ae8ae617-590d-430b-86d4-16364372b67f", + "last_modified": 1585158976511 + }, + { + "guid": "/^(({a4d84dae-7906-4064-911b-3ad2b1ec178b})|({d7e388c5-1cd0-4aa6-8888-9172f90951fb})|({a67f4004-855f-4e6f-8ef0-2ac735614967})|({25230eb3-db35-4613-8c03-e9a3912b7004})|({37384122-9046-4ff9-a31f-963767d9fe33})|({f1479b0b-0762-4ba2-97fc-010ea9dd4e73})|({53804e15-69e5-4b24-8883-c8f68bd98cf6})|({0f2aec80-aade-46b8-838c-54eeb595aa96})|({b65d6378-6840-4da6-b30e-dee113f680aa})|({e8fc3f33-14b7-41aa-88a1-d0d7b5641a50})|({c49ee246-d3d2-4e88-bfdb-4a3b4de9f974}))$/", + "prefs": [], + "schema": 1585138540335, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1484536", + "why": "Add-ons that don't respect user choice by overriding search.", + "name": "Search hijacking add-ons (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "01c22882-868b-43e1-bb23-29d5dc7bc11b", + "last_modified": 1585158976508 + }, + { + "guid": "/^((\\{0b47ef96-f7c9-4017-97b6-51c1280eaf6e\\})|(\\{2c65ed54-5215-4f10-a7bd-39445a6450fd\\})|(\\{80dbc30a-168b-4a18-bd5b-51f9b1807d7f\\})|(\\{93a106e3-e70b-48af-9282-30e3d6c87af9\\})|(\\{2120dc72-6040-45ed-9655-aaabed57fc93\\})|(\\{c3c9f1ee-4192-4d5a-b753-a62c19b16c98\\})|(\\{d0fc8cf9-66aa-4f08-8c96-3f882c2e9c9b\\})|(\\{da69b9e2-c2d1-4b90-93be-4cc3976e452d\\})|(\\{eb70585e-76bb-4eae-9f06-7fc5efbc877e\\})|(fairshare-unlock@burstworks-test\\.com)|(fairshare-unlock@burstworks\\.com)|(freevideodownloader-hosted@funnerapps\\.com)|(freevideodownloader-test-hosted@funnerapps\\.com)|(gmo-panel@ddmr\\.com)|(helper-sig@savefrom\\.net)|(ihmgiclibbndffejedjimfjmfoabpcke@chrome-store-foxified--?\\d+)|(ihmgiclibbndffejedjimfjmfoabpcke@chromeStoreFoxified--?\\d+)|(panel-branded@ddmr\\.com)|(panel-canadaTalkNow@ddmr\\.com)|(panel-community@ddmr\\.com)|(panel-digaYgane@ddmr\\.com)|(panel-ecglobal@ddmr\\.com)|(panel-fusionCash@ddmr\\.com)|(panel-grindaBuck@ddmr\\.com)|(panel-measurement@ddmr\\.com)|(panel-mysoapbox@ddmr\\.com)|(panel-ofernation@ddmr\\.com)|(panel-rewarding@ddmr\\.com)|(panel-superpay@ddmr\\.com)|(panel-zippy@ddmr\\.com)|(test_fairshare-unlock@burstworks\\.com)|(test-fairshare-unlock@burstworks\\.com)|(vindale@ddmr\\.com))$/", + "prefs": [], + "schema": 1585151995695, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1562965", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "04c27082-2b78-4005-a6c5-8e0a13f83288", + "last_modified": 1585158976505 + }, + { + "guid": "/^((\\{3c8970fa-1340-45ad-82fe-81f3beccfbdc\\})|(\\{4ab99b95-4d05-438c-8a3e-adb1b3fe8d81\\})|(\\{7f87a05d-dba7-448e-9af2-ee0f4a294c01\\})|(\\{59a219a8-45cd-458d-9b3e-8d86c19dfc31\\})|(\\{79f4bfc7-b1da-4dc4-85cc-ecbcc5dd152e\\})|(\\{484dc5ad-4d6a-4ee4-91b7-b5b8166e6b3d\\})|(\\{2643d75f-9d64-47ef-9c23-78f0f055c7b8\\})|(\\{76399bf2-8354-4b11-bf43-6c863b195b1d\\})|(\\{110791c0-2883-4301-8214-90be7549df43\\})|(\\{a33e004d-2ac0-4d77-8e14-50780bc231a3\\})|(\\{aaaa5840-6b3b-49d8-92c2-9696798c4e2a\\})|(\\{bfc55377-7210-4e7a-828f-6fdb9df02847\\})|(\\{c6c78b9a-370d-49c5-b9c6-96d7e38861c5\\})|(\\{c115eb3a-4746-472b-8f1f-d8596c49b3b6\\})|(\\{deaa22e5-33ed-440f-a734-c3175e6228a7\\})|(\\{e34d5840-6b3b-49d8-92c2-9696798c4e2a\\})|(aapbdbdomjkkjkaonfhkkikfgjllcleb@[cC]hrome-?[sS]tore-?[fF]oxified--?((\\d+)|(unsigned)))|(babelfox_client@rami)|(blndkmebkmenignoajhoemebccmmfjib@chrome-store-foxified--?\\d+)|(bridge-translate-app@chrome-store-foxified--?\\d+)|(dephbpajmknbniclommefdlnflkfnpgh@chrome-store-foxified--?\\d+)|(extension@newtab\\.biz)|(generated-74o6bact7xu7y32fvfju4s@chrome-store-foxified--?\\d+)|(generated-axbwzwbksnnig1ug9v5dly@chrome-store-foxified--?\\d+)|(googletranslateelement@developer\\.org)|(icdahkkjdchifpnbebileaelbcgipepe@chrome-store-foxified--?\\d+)|(ifgljfjnflaadalpmkkgdailepedeehd@chrome-store-foxified--?\\d+)|(knpgbkpddpcepnloiijojmgbdhihkjkl@chrome-store-foxified--?\\d+)|(translate-4@chrome-store-foxified--?\\d+))$/", + "prefs": [], + "schema": 1585157952678, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1593243", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "11fd123a-e67d-44ab-909f-b776ea2e8d0a", + "last_modified": 1585158976501 + }, + { + "guid": "/^((graph-helper@thegraph\\.co)|(lab@uneg\\.edu\\.ve)|(ponyhoofbeta@ponyhoof\\.little\\.my)|(\\{75080f0e-3dbe-455d-a1d1-33850f36e2ff\\})|(\\{27883109-b35d-4b6b-9e5b-89b26496fb5e\\})|(\\{cf407827-6739-4514-ad2e-8af25c0e1d97\\})|(\\{c9efb61c-f0bb-42a2-a3e5-ca75da67007c\\})|(\\{c671463f-7bd2-4a35-839c-2097aa52b1d2\\})|(\\{83d16411-a3d6-4268-a95f-1e9abcbdd521\\})|(\\{3835f36e-a8a8-4247-a731-6e1ed7d52c5f\\})|(\\{8d13fb0d-e6b6-4301-9865-3c438a1a347f\\})|(\\{21262c5a-3c8e-46a1-b504-7d16ccddf656\\})|(\\{3e1f3207-8b6c-493b-91f3-0eb43326e273\\})|(\\{9fe1f0bb-c24d-4eab-90f5-9471905132f6\\})|(\\{8b189632-155f-4478-bf51-285c3a54de8c\\})|(\\{3854ac9e-7c1c-4d2a-84f7-edc8656ac7ae\\})|(\\{ec82c419-3cc7-4789-bad0-a069ef80211e\\})|(\\{C0D03FFD-391A-41CE-ACA2-64557D9440CC\\})|(\\{D4A86045-8722-4289-93C4-7C80970F0E7D\\})|(\\{5f23a0d5-ff0e-4e18-8afc-cf50c468ea1d\\})|(\\{6b67941f-2544-468d-b371-ad5c2d99687b\\})|(\\{d6a07a0c-cad7-4e08-987a-099c1add5fc4\\})|(web-signer@softplan\\.com\\.br)|(\\{251eec9f-108a-4864-960f-e07cee35a991\\})|(\\{9cd44027-9d26-4cbd-bfd9-da7eaa495261\\})|(\\{4244357f-568b-477b-802a-db9d6551a719\\})|(\\{1ed0819f-e210-4089-8e24-af2dba6451bb\\})|(\\{e02dba27-b975-4dcb-9adb-ea74ed6cd632\\})|(\\{1240d113-0410-418b-b99f-047aad6ddae3\\})|(\\{115d13bd-f95d-4a61-9dd7-fb3ab816a8dd\\})|(mydesires@mydesires\\.com)|(mesenvies@mesenvies\\.fr)|(\\{0f250607-4465-49bb-a56c-12c8514a3e7d\\})|(\\{12a290cc-2b60-4efb-96f0-dde41f27769d\\})|(\\{9ac03a7d-f80f-4868-8b95-35074f8cdf62\\})|(\\{ce1da0e5-133b-4828-8cbb-66c5ea97e78e\\})|(\\{0da8fe99-30f7-462c-85d0-db66f4f70a97\\})|(\\{9aca9f23-d9aa-4680-9dcb-837291ff6854\\})|(\\{d86d1438-56ef-4294-ab9b-ac1f2c6420e6\\})|(\\{6280d3e5-a444-4823-be7d-317090d3b175\\})|(\\{faccc7b3-b0c7-4fcc-9d4d-b12721ea7f0d\\})|(\\{b84f4632-26f1-4fb5-9082-1f2ca7685afa\\})|(\\{a543f841-3613-4a52-bce8-58bd6e7eb100\\})|(\\{5b1c9b32-9dd3-461c-85c6-bbac4ef2af19\\})|(\\{0207f077-7f95-47ba-b0f9-b356d456f914\\})|(teste@addon\\.com)|(\\{6da58723-1757-42a4-a2db-849f6159c880\\})|(\\{4a122670-da9d-4257-b2bc-27440af385af\\})|(\\{f39035e7-e7f6-4dc1-b13b-04d7a365ab41\\})|(\\{3b3201ca-d7a1-48ab-a89b-ad97cd688590\\})|(\\{bb0179bf-3435-4924-8ae5-19f35e7f1a7f\\})|(\\{d2f6e317-3957-4931-b9b1-6b3ebef25aab\\})|(luckypdf@luckypdfconverter\\.com)|(\\{755ae77a-2901-4177-99aa-ac3fe15974e0\\})|(\\{6e5cab8f-78da-45fe-90af-4cfaf3d95d0e\\})|(\\{cd0c26ad-5150-42cb-bec1-2da0db715a90\\})|(\\{357d3c5d-5c68-4943-b49e-df20c6ff931e\\})|(\\{8b5d6a2c-d18c-47b6-9b13-cebab7425838\\})|(\\{d26b38f2-1ec0-43d8-8d05-8e14a09955bc\\})|(\\{d3b4d997-cf97-4ffe-9401-3efcbc816fd9\\})|(\\{506848ee-e612-4d2f-8645-70c26e577efd\\})|(\\{c0272d15-248c-4f93-80cd-684c0f360da1\\})|(\\{7a478659-43ba-4054-8d67-9ad75f66f0f6\\})|(\\{351b37fb-6d84-4820-8dbe-a517d8f856ce\\})|(\\{f0031513-5b7e-4cc6-9a16-616c82e79d48\\})|(\\{adf2ee6b-ef1c-4350-b35f-369e6b0dc64a\\})|(\\{06a6425d-0e31-4703-a0e2-72d4a552477c\\})|(\\{6f8a85a2-d83b-4a2c-ac38-d06bf3fb1b92\\})|(\\{9d96147e-9418-4493-bedb-380bf065f8b0\\})|(\\{3055992b-3fda-44bf-9065-8514ea2c2acb\\})|(\\{602ade61-49fc-4a40-9287-53949c8f0462\\})|(\\{c28dd807-0b3f-4bc4-984c-ff9578202712\\})|(\\{495e077c-0015-4e70-80f3-4b1b71e9c866\\})|(\\{19ebc477-304c-4d75-990b-6d47c230c19d\\})|(\\{08df44a4-ef6a-4828-9ebe-a730dd47ee5e\\})|(allsigningoffline@nextsense\\.com)|(\\{cc304579-73e2-4fa7-9863-f1cda545e542\\})|(\\{bf0d9dc8-ca00-4bba-88a0-535e20b089ea\\})|(\\{00dd06ff-7392-449b-9e87-8a39c5f5cb05\\})|(\\{c8882c78-ee4f-4c9c-8380-ab8f1d333e0d\\})|(\\{1bf5b2e0-14aa-49a4-9642-d2f28bb44730\\})|(\\{8afde9a0-bd2d-48f4-939c-8e7ec4210085\\})|(\\{8bd4cf33-e9c2-4a9c-84f5-ef0f3a512c53\\})|(\\{390d4afb-83c3-4a8c-93a0-587f9c52eb10\\})|(\\{3624218a-2799-478f-84bf-8f6ddd4fe8f5\\})|(\\{5303ae90-10ef-4c9c-bc51-d7062883b9a1\\})|(vivoinforma@tutanota\\.com)|(\\{8e638a74-6131-40b9-b64e-4341cf67d491\\})|(\\{f9295774-e455-4986-832c-cc4863769e66\\})|(thisisummsstrikethroughhighlighting@example\\.com)|(frigate@loranrendel)|(\\{ff4dc908-b851-42b7-adee-d9a5bb1e6813\\})|(\\{3ec50eaf-e36b-4b26-b255-0d75abe112b9\\})|(@LmXEarthZoomToolOneShot)|(\\{91be62bd-8752-45bf-a20a-b6b2d4abf87e\\})|(cloud-support@wildfire\\.ai)|(YoutubeUploader@ArcAvalon\\.com)|(\\{2e3be1f1-fb5c-40fb-b578-d2c9184ce470\\}))$/", + "prefs": [], + "schema": 1585146642510, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602022", + "why": "This add-on violates Mozilla's add-on policy by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "92ac1ebd-7dcd-4446-a7b3-7e359ad1c4b5", + "last_modified": 1585158976498 + }, + { + "guid": "/^((Smart_Search_voIhxkbgGH@www\\.convertmyfile\\.co)|(Smart_Search_vRIICPGhwx@www\\.job-portal\\.co)|(Smart_Search_wsRtgwiOcH@www\\.search-smart\\.live)|(Smart_Search_xCropjfsuc@www\\.searchsmart\\.live)|(Smart_Search_yCAOHFcvfi@www\\.smart-search\\.live)|(Smart_Search_yDaEiIzkQK@www\\.smartsearch\\.live)|(Smart_Search_yhCKmmAXWz@www\\.searchsmart\\.online)|(Smart_Search_yrwXRZHXLh@www\\.search-smart\\.online)|(Smart_Search_YXySavVGVt@www\\.search-smart\\.work)|(SMART_SEARCH_YZaevPNtHc@www\\.localweathertoday\\.net)|(Smart_Search_ZsEjqVBHGd@www\\.convertthefile\\.net)|(Smart_Searches_FuCzQWfGWa@www\\.smartsearches\\.co)|(SMART-SEARCH_sHhQFGpFhV@www\\.coupondealer\\.co)|(Smart-Search_UasOSUTNhv@www\\.map-buddy\\.net)|(Smart\\.Search_631e8da2146d6187149c9206fac1a89e@www\\.convertthepdf\\.co)|(Smart\\.Search_IfzjxIabfy@www\\.convertthepdf\\.co)|(SmartSearch_clone_DyqkuEZPRF@www\\.search-smart\\.co)|(SmartSearch_DSuyKfIhTM@www\\.convertmypdf\\.online)|(SmartSearch_gkOEsQztVd@www\\.search-smart\\.co)|(SmartSearch_ohUbxjDpNV@www\\.search-smart\\.co)|(Speed_Check_01c5dfe0109bc6cb0e711840b239846c@www\\.checkmyspeed\\.co)|(Speed_Check_07449674bdae85845b4fc92e2695caf3@www\\.checkmyspeednow\\.com)|(Speed_Check_2098fea3a2c7a807bdeb61111c2de48f@www\\.checkmyspeed\\.co)|(Speed_Check_2305caaa9793a7577aa6283a89da7971@www\\.checkmyspeednow\\.com)|(Speed_Check_25ae2975d89f06e814b2e421109be7ef@www\\.checkmyspeednow\\.com)|(Speed_Check_55750cbafa724e361805677e2c1a0ea7@www\\.checkmyspeednow\\.com)|(Speed_Check_56ad7958c2981e0f1cad6d4de2abcb4b@www\\.checkmyspeed\\.co)|(Speed_Check_616286717d187249f5d29d2bc8771294@www\\.checkmyspeednow\\.com)|(Speed_Check_87ec83e10e18545948a0d739589fa6c0@www\\.checkmyspeed\\.co)|(Speed_Check_917a4c229b30f0ac1bca0aa5a43f8f2e@www\\.checkmyspeednow\\.disabled\\.com)|(Speed_Check_94cbe40b6d2b11c86b07bbb606fe11e2@www\\.checkmyspeednow\\.com)|(Speed_Check_a4d8b86ff6fa408ee87b70d65c41d1dd@www\\.checkmyspeed\\.co)|(Speed_Check_cdeb8d6f1f2426379a89eac745d5853a@www\\.checkmyspeed\\.co)|(Speed_Check_clone_aqksrBHRvi@www\\.checkmyspeednow\\.com)|(Speed_Check_clone_CrMqWRFYTl@www\\.checkmyspeednow\\.com)|(Speed_Check_clone_ec2c77c7b610b83c9f6bef2573e4d3a1@www\\.checkmyspeed\\.co)|(Speed_Check_clone_IchkKEIlVC@www\\.checkmyspeednow\\.com)|(Speed_Check_clone_nmRPlvUEJv@www\\.checkmyspeednow\\.com)|(Speed_Check_clone_OczJkeuXDq@www\\.checkmyspeednow\\.com)|(Speed_Check_clone_QlPnYtcPGo@www\\.checkmyspeednow\\.com)|(Speed_Check_clone_RjaZbJQbEk@www\\.checkmyspeednow\\.com)|(Speed_Check_clone_WPgSSdchao@www\\.checkmyspeed\\.co)|(Speed_Check_ed30669ac9901d20415ead7770db761a@www\\.speedchecktool\\.com)|(Speed_Check_FZSVtPegfX@www\\.checkmyspeed\\.co)|(Speed_Check_iQueDNOXmF@www\\.checkmyspeed\\.co)|(Speed_Check_LUlQBGtuOE@www\\.checkmyspeednow\\.com)|(Speed_Check_MNeDWiZeoo@www\\.checkmyspeed\\.co)|(Speed_Check_XckNwcWvNE@www\\.checkmyspeed\\.co)|(Speed_Check_xSObhPOTMw@www\\.checkmyspeed\\.co)|(Speed_Checker_Pro_260678188808c41791fdd9d9ed445802@www\\.speedcheckerpro\\.com)|(Speed_Checker_Pro_esgyoKewQa@www\\.speedcheckerpro\\.com)|(Speed_Test_Ace_0e5d46c255d8ceec71018d460c702e40@www\\.speedtestace\\.com)|(Speed_Test_Ace_b81323c0334e1c3f409c1dd14aa60f61@www\\.speedtestace\\.co)|(Speed_Test_Ace_clone_DKdXeTMeZx@www\\.speedtestace\\.co)|(Speed_Test_Ace_kcEikItaOC@www\\.speedtestace\\.co)|(Speed_Test_Ace_ppoJbdaucW@www\\.speedtestace\\.co)|(Speed_Test_Now_ZDIRLnXzms@speedtestnow\\.co)|(Speed_Test_OoJkZnYICK@www\\.speed-test\\.live)|(Speed_Test_uJpXMlaztD@www\\.testyourspeed\\.co)|(sportsbulletin@www\\.sportsupdates\\.co)|(sportseveryday@www\\.sportseveryday\\.co)|(sportseverydayco@www\\.sportseveryday\\.co)|(sportsupdates\\.website_SdCnLxLIxY@www\\.sportsupdates\\.website)|(Spot_Flight_Now_6b388ff946e79dc3fd82eec51958d550@www\\.spotflightnow\\.com)|(Spot_Flight_Now_clone_eb4c54758e45589d6039415632c1e319@www\\.spotflightnow\\.com)|(Spot_Flight_Now_clone_eTPyUPJmtK@www\\.spotflightnow\\.com)|(Spot_Flight_Now_clone_UZIlZeQYAb@www\\.spotflightnow\\.com)|(Spot_Flight_Now_EtIsXJyXXm@www\\.spotflightnow\\.com)|(Spot_Flight_Now_f268f21c4c60f3e78601d20b97453943@www\\.spotflightnow\\.com)|(staging\\.checknetspeed\\.co_nREaqypDrX@staging\\.checknetspeed\\.co)|(Start_A_Career_RXEBmchKFm@www\\.jobs\\.startacareer\\.co)|(Stream_TV_Live_KrYxWrtCtv@www\\.streamtvlive\\.co))$/", + "prefs": [], + "schema": 1585146841367, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "aae4b574-f195-4f18-8bc0-c3b712e4316b", + "last_modified": 1585158976495 + }, + { + "guid": "/^((Easy_Doc_Converter_b4ab66ebed6d4f9148bd9024b5b45266@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_egpEGOXPkz@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_f5e570a648be4d82f02fa2106a21a43f@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_IteaxXKJex@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_jJGqUHYgCN@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_KPOmYhpWMq@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_KyppGVLzlw@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_PuJVSyMdMn@www\\.easydocconverter\\.com)|(Easy_Email_Center_clone_LWcJzWsabx@www\\.easyemailcenter\\.com)|(Easy_Email_Center_clone_WroHXxYaWe@www\\.easyemailcenter\\.com)|(Easy_Email_Center_fhZhWrvhNZ@www\\.easyemailcenter\\.com)|(Easy_Email_Checker_54b7a48dd3773a01313275df62afd4cf@www\\.easyemailchecker\\.net)|(Easy_Email_Plus_GqrjVlzgJP@www\\.easyemailplus\\.com)|(Easy_Forms_Finder_gmYexjlshO@www\\.easyformsfinder\\.com)|(Easy_Inbox_Access_clone_BHQcoGwBXJ@www\\.easyinboxaccess\\.com)|(Easy_Inbox_Access_pXyLNbavvp@www\\.easyinboxaccess\\.com)|(Easy_Inbox_Access_yVECZMgzTG@www\\.easyinboxaccess\\.com)|(Easy_Mail_Login_FRPUoJrXUl@www\\.easymail-login\\.co)|(Easy_Mail_Logins_AGDDyUpIjb@www\\.easymaillogin\\.co)|(Easy_Map_Finder_a811a7fb227baf4b89b3ed4d017fcfc4@www\\.easymapsfinder\\.com)|(Easy_Map_Tab_44b8d948ebfb6010e7fe17ad02606c6e@www\\.easymaptab\\.com)|(Easy_Map_Tab_rfqbzWtygp@www\\.easymaptab\\.com)|(Easy_Maps_VsjiCqbVfo@www\\.mapdirectionspront\\.co)|(Easy_Online_Recipe_IzyMoUNAGs@www\\.easyonlinerecipe\\.co)|(Easy_Online_Recipe_oEqObvsniN@www\\.easyonlinerecipe\\.net)|(Easy_Recipes_277ea1aa5aaed20cbb145291ebf0cdc1@www\\.dailyrecipesearch\\.net)|(Easy_Recipes_35f61f2bee1a01e94edbbb868bbb66ca@www\\.dailyrecipesearch\\.net)|(Easy_Recipes_5d97b38952cbd818b53669ee377f4a4e@www\\.dailyrecipesearch\\.net)|(Easy_Recipes_JkInlZZXKJ@www\\.dailyrecipesearch\\.net)|(Easy_Recipes_qwPPMUMPbm@www\\.dailyrecipesearch\\.net)|(Easy_Recipes_VZjydpXYuD@www\\.dailyrecipesearch\\.net)|(Easy_Search_3ce2d360291c0435253c279a003f4bee@www\\.easy-search\\.co)|(Easy_Search_9259a3968f7a7a407cf8a9c5b5aac8b5@www\\.easy-search\\.co)|(Easy_Search_Access_41cd311e7ebb5abe48a68600a41b6a6e@www\\.easysearchaccess\\.com)|(Easy_Search_Access_zBaMiEnnEw@www\\.easysearchaccess\\.com)|(Easy_Search_bJrePoApTo@www\\.easy-search\\.co)|(Easy_Search_gFyxFnsAsY@www\\.easy-search\\.co)|(Easy_Search_Online_8d5692e3bf9af4e8d0fcb27b753b4bb0@www\\.easysearchonline\\.co)|(Easy_Search_Online_YJpastaXEE@www\\.easysearchonline\\.co)|(Easy_Search_Pro_edc3f4614998d63f6bddb8cddf1eca75@www\\.easysearchpro\\.co)|(Easy_Search_Pro_JDFkWEUATn@www\\.easysearchpro\\.co)|(Easy_Weather_Checker_b96bc45b78e94fcc92bbf66eabb4c7e0@www\\.easyweatherchecker\\.com)|(Easy_Weather_Checker_OZbmtCpDgx@www\\.easyweatherchecker\\.com)|(Easy_Weather_Forecast_6b2ee7704b4f8f93b4e4c42c707dadc1@www\\.easyweatherforecast\\.com)|(Easy_Weather_Forecast_pNIPnyHOJG@www\\.easyweatherforecast\\.com)|(EasyMailLogin\\.net_usETGyzdRX@www\\.easymaillogin\\.net)|(EasyOnlineRecipe\\.co_CVOjgBfTWs@www\\.easyonlinerecipe\\.co)|(EasyOnlineRecipe\\.net_icOMrzwKiN@www\\.easyonlinerecipe\\.net)|(EasySearchGo_JMJgGKKsKw@www\\.easysearchgo\\.com)|(Email_Access_7ad642481026e05b4ff4a7da92468bdb@www\\.accessmymails\\.co)|(Email_Access_JVuQrQxsXR@www\\.accessmymails\\.co)|(Email_Checker_MguwpDKJMd@www\\.easyemailchecker\\.net)|(Email_Inbox_Now_OVfDpBfbuk@www\\.emailinboxnow\\.com)|(Email_Login_CofCwMXmYY@www\\.easymaillogin\\.net)|(Email_Login_eVRzKHiywW@www\\.easymaillogin\\.net)|(Email_Login_KABgNaGQJZ@www\\.easymaillogin\\.net)|(Email_Login_Pro_80b99340e1bcedf5f0a1f4d1345538e1@www\\.emailloginpro\\.com)|(Email_Login_Pro_SHHHqfDSTO@www\\.emailloginpro\\.com)|(Email_Login_Pro_xvgHLfPbBB@www\\.loginemailpro\\.net)|(Email_Tab_13d31aa17a541c3018f2625a09486686@www\\.emailtab\\.co)|(Email_Tab_ZdbwsMpyuq@www\\.emailtab\\.co)|(Everyday_Astro_clone_bOSsysyaan@www\\.everydayastro\\.co)|(Everyday_Astro_CMqmHgpSTg@www\\.everydayastro\\.co)|(Everyday_Astro_dVNMgQmXws@www\\.everydayastro\\.online)|(Everyday_Astro_NHOKASspnT@www\\.everydayastro\\.online)|(Everyday_Astro_sDIIbCNITP@www\\.everydayastro\\.online)|(Everyday_Astro_xiJdwxnNom@www\\.everydayastro\\.online)|(Everyday_Astro_YGSUqIslKW@www\\.everyday-astro\\.com))$/", + "prefs": [], + "schema": 1585145745119, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "21ecfbe4-1255-4f86-b78c-c4fd06a6f197", + "last_modified": 1585158976491 + }, + { + "guid": "/^((\\{441a1a1f-ba4f-4f2e-ade8-88c2950323a6\\})|(\\{81faa316-ef9f-4fc9-9db8-af17eabc776b\\})|(\\{e2ee901b-44cb-430f-be5e-23ab0291d8f4\\})|(\\{d1e6ace8-9623-4a5d-a94f-81fd289fe834\\})|(\\{0de5c9e4-4488-489a-a130-0e192cc6bb08\\}))$/", + "prefs": [], + "schema": 1584906090910, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1624545", + "why": "This add-on violates Mozilla's add-on policies by redirecting requests without user disclosure or consent.", + "name": "Add-ons redirecting requests" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9b936d20-0a13-42de-b735-11409c3e4881", + "last_modified": 1585130632165 + }, + { + "guid": "/^((\\{9263519f-ca57-4178-b743-2553a40a4bf1\\})|(\\{d97223b8-44e5-46c7-8ab5-e1d8986daf44\\})|(\\{0111c475-01e6-42ea-a9b4-27bed9eb6092\\})|(\\{36a4269e-4eef-4538-baea-9dafbf6a8e2f\\})|(\\{2cac0be1-10a2-4a0d-b8c5-787837ea5955\\})|(\\{6072a2a8-f1bc-4c9c-b836-7ac53e3f51e4\\})|(\\{2134e327-8060-441c-ba68-b167b82ff5bc\\})|(\\{a3fbc8be-dac2-4971-b76a-908464cfa0e0\\})|(\\{de3b1909-d4da-45e9-8da5-7d36a30e2fc6\\})|(\\{02328ee7-a82b-4983-a5f7-d0fc353698f0\\})|(\\{28044ca8-8e90-435e-bc63-a757af2fb6be\\})|(\\{63df223d-51cf-4f76-aad8-bbc94c895ed2\\})|(\\{3f4191fa-8f16-47d2-9414-36bfc9e0c2bf\\})|(\\{a0c54bd8-7817-4a40-b657-6dc7d59bd961\\})|(\\{f0b809eb-be22-432f-b26f-b1cadd1755b9\\})|(\\{919fed43-3961-48d9-b0ef-893054f4f6f1\\})|(\\{cd28aa38-d2f1-45a3-96c3-6cfd4702ef51\\})|(\\{829827cd-03be-4fed-af96-dd5997806fb4\\})|(\\{7d932012-b4dd-42cc-8a78-b15ca82d0e61\\})|(\\{ed352072-ddf0-4cb4-9cb6-d8aa3741c2de\\})|(\\{a0ab16af-3384-4dbe-8722-476ce3947873\\})|(\\{23c65153-c21e-430a-a2dc-0793410a870d\\})|(\\{31680d42-c80d-4f8a-86d3-cd4930620369\\})|(\\{f4262989-6de0-4604-918f-663b85fad605\\})|(\\{e341ed12-a703-47fe-b8dd-5948c38070e4\\})|(\\{cd89045b-2e06-46bb-9e34-48e8799e5ef2\\})|(\\{ac296b47-7c03-486f-a1d6-c48b24419749\\})|(\\{5da81d3d-5db1-432a-affc-4a2fe9a70749\\})|(\\{72c1ca96-c05d-46a7-bce1-c507ec3db4ea\\})|(\\{869b5825-e344-4375-839b-085d3c09ab9f\\})|(\\{81ac42f3-3d17-4cff-85af-8b7f89c8826b\\})|(\\{df09f268-3c92-49db-8c31-6a25a6643896\\})|(\\{5f4e63e4-351f-4a21-a8e5-e50dc72b5566\\})|(\\{4479446e-40f3-48af-ab85-7e3bb4468227\\})|(\\{09c8fa16-4eec-4f78-b19d-9b24b1b57e1e\\})|(\\{be37931c-af60-4337-8708-63889f36445d\\})|(\\{71639610-9cc3-47e0-86ed-d5b99eaa41d5\\})|(\\{0432b92a-bfcf-41b9-b5f0-df9629feece1\\})|(\\{83d38ac3-121b-4f28-bf9c-1220bd3c643b\\})|(\\{01166e60-d740-440c-b640-6bf964504b3c\\})|(\\{76ce213c-8e57-4a14-b60a-67a5519bd7a7\\})|(\\{226b0fe6-f80f-48f1-9d8d-0b7a1a04e537\\})|(\\{be981b5e-1d9d-40dc-bd4f-47a7a027611c\\})|(\\{37f8e483-c782-40ed-82e9-36f101b9e41f\\})|(\\{01c9a4a4-06dd-426b-9500-2ea6fe841b88\\})|(\\{7d5e24a1-7bef-4d09-a952-b9519ec00d20\\})|(\\{408a506b-2336-4671-a490-83a1094b4097\\})|(\\{7f8bc48d-1c7c-41a0-8534-54adc079338f\\})|(\\{cf9d96ff-5997-439a-b32b-98214c621eee\\})|(\\{302ef84b-2feb-460e-85ca-f5397a77aa6a\\})|(\\{0c9970a2-6874-483b-a486-2296cfe251c2\\})|(\\{e5bc3951-c837-4c98-9643-3c113fc8cf5e\\})|(\\{93017064-dfd4-425e-a700-353f332ede37\\})|(\\{28092fa3-9c52-4a41-996d-c43e249c5f08\\})|(\\{a893296e-5f54-43f9-a849-f12dcdee2c98\\})|(\\{fc0d55bd-3c50-4139-9409-7df7c1114a9d\\})|(\\{c661c2dc-00f9-4dc1-a9f6-bb2b7e1a4f8d\\})|(\\{419be4e9-c981-478e-baa0-937cf1eea1e8\\})|(\\{449e185a-dd91-4f7b-a23a-bbf6c1ca9435\\})|(\\{1c981c7c-30e0-4ed2-955d-6b370e0a9d19\\})|(\\{9ce2a636-0e49-4b8e-ad17-d0c156c963b0\\})|(\\{a2de96bc-e77f-4805-92c0-95c9a2023c6a\\})|(\\{a42e5d48-6175-49e3-9e40-0188cde9c5c6\\})|(\\{86d98522-5d42-41d5-83c2-fc57f260a3d9\\})|(\\{05a21129-af2a-464c-809f-f2df4addf209\\})|(\\{446122cd-cd92-4d0c-9426-4ee0d28f6dca\\})|(\\{bfd92dfd-b293-4828-90c1-66af2ac688e6\\})|(\\{f9e1ad25-5961-4cc5-8d66-5496c438a125\\})|(\\{8a61507d-dc2f-4507-a9b7-7e33b8cbc31b\\})|(\\{84406197-6d37-437c-8d82-ae624b857355\\})|(\\{11df9391-dba5-4fe2-bd48-37a9182b796d\\})|(\\{e9ccb1f2-a8ba-4346-b43b-0d5582bce414\\})|(\\{79db6c96-d65a-4a64-a892-3d26bd02d2d9\\})|(\\{214cb48a-ce31-4e48-82cf-a55061f1b766\\})|(\\{4c140bc5-c2ad-41c3-a407-749473530904\\})|(\\{c5cf4d08-0a33-4aa3-a40d-d4911bcc1da7\\})|(\\{591d1b73-5eae-47f4-a41f-8081d58d49bf\\})|(\\{f1bce8e4-9936-495b-bf48-52850c7250ab\\})|(\\{7c1df23b-1fd8-42b9-8752-71fff2b979de\\})|(\\{3c27c34f-8775-491a-a1c9-fcb15beb26d3\\})|(\\{ddae89bd-6793-45d8-8ec9-7f4fb7212378\\})|(\\{c488a8f5-ea3d-408d-809e-44e82c06ad9d\\})|(\\{2f8aade6-8717-4277-b8b1-55172d364903\\})|(\\{2eb66f6c-94b3-44f5-9de2-22371236ec99\\})|(\\{216e0bcc-8a23-4069-8b63-d9528b437258\\})|(\\{d14acee6-f32b-4aa3-a802-6616003fc6a8\\})|(\\{b26bf964-7aa6-44f4-a2a9-d55af4b4eec0\\})|(\\{e2139287-2b0d-4f54-b3b1-c9a06c597223\\})|(\\{3f4dea3e-dbfc-428f-a88b-36908c459e20\\})|(\\{92111c8d-0850-4606-904a-783d273a2059\\})|(\\{2aa275f8-fabc-4766-95b2-ecfc73db310b\\})|(\\{f01c3add-dc6d-4f35-a498-6b4279aa2ffa\\}))$/", + "prefs": [], + "schema": 1585040894048, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1439702", + "why": "This malicious add-on claims to be a Firefox \"helper\" or \"updater\" or similar.", + "name": "FF updater (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "48b14881-5f6b-4e48-afc5-3d9a7fae26a3", + "last_modified": 1585130632161 + }, + { + "guid": "/^((__TEMPLATE__APPLICATION__@ruta-mapa\\.com)|(application-3@findizer\\.fr)|(application2@allo-pages\\.fr)|(application2@bilan-imc\\.fr)|(application2@lettres\\.net)|(application2@search-maps-finder\\.com)|(application-imcpeso@imc-peso\\.com)|(application-meuimc@meu-imc\\.com)|(application-us2@factorlove)|(application-us@misterdirections)|(application-us@yummmi\\.es)|(application@amiouze\\.fr)|(application@astrolignes\\.com)|(application@blotyn\\.com)|(application@bmi-result\\.com)|(application@bmi-tw\\.com)|(application@calcolo-bmi\\.com)|(application@cartes-itineraires\\.com)|(application@convertisseur\\.pro)|(application@de-findizer\\.fr)|(application@de-super-rezepte\\.com)|(application@dermabeauty\\.fr)|(application@dev\\.squel\\.v2)|(application@eu-my-drivingdirections\\.com)|(application@fr-allo-pages\\.fr)|(application@fr-catizz\\.com)|(application@fr-mr-traduction\\.com)|(application@good-recettes\\.com)|(application@horaires\\.voyage)|(application@imc-calcular\\.com)|(application@imc-peso\\.com)|(application@it-mio-percorso\\.com)|(application@iti-maps\\.fr)|(application@itineraire\\.info)|(application@lbc-search\\.com)|(application@les-pages\\.com)|(application@lovincalculator\\.com)|(application@lovintest\\.com)|(application@masowe\\.com)|(application@matchs\\.direct)|(application@mein-bmi\\.com)|(application@mes-resultats\\.com)|(application@mestaf\\.com)|(application@meu-imc\\.com)|(application@mon-calcul-imc\\.fr)|(application@mon-juste-poids\\.com)|(application@mon-trajet\\.com)|(application@my-drivingdirections\\.com)|(application@people-show\\.com)|(application@plans-reduc\\.fr)|(application@point-meteo\\.fr)|(application@poulixo\\.com)|(application@quipage\\.fr)|(application@quizdeamor\\.com)|(application@quizdoamor\\.com)|(application@quotient-retraite\\.fr)|(application@recettes\\.net)|(application@routenplaner-karten\\.com)|(application@ruta-mapa\\.com)|(application@satellite\\.dev\\.squel\\.v2)|(application@search-bilan-imc\\.fr)|(application@search-maps-finder\\.com)|(application@slimness\\.fr)|(application@start-bmi\\.com)|(application@tests-moi\\.com)|(application@tousmesjeux\\.fr)|(application@toutlannuaire\\.fr)|(application@tuto-diy\\.com)|(application@ubersetzung-app\\.com)|(application@uk-cookyummy\\.com)|(application@uk-howlogin\\.me)|(application@uk-myloap\\.com)|(application@voyagevoyage\\.co)|(application@wikimot\\.fr)|(application@www\\.plans-reduc\\.fr)|(application@yummmi\\.es)|(application@yummmies\\.be)|(application@yummmies\\.ch)|(application@yummmies\\.fr)|(application@yummmies\\.lu)|(application@zikplay\\.fr)|(applicationY@search-maps-finder\\.com)|(cmesapps@findizer\\.fr)|(findizer-shopping@jetpack)|(\\{8aaebb36-1488-4022-b7ec-29b790d12c17\\}))$/", + "prefs": [], + "schema": 1585060581210, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1452648", + "why": "Those add-ons do not provide a real functionality for users, other than silently tracking browsing behavior.", + "name": "Tracking Add-ons (harmful)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "36f97298-8bef-4372-a548-eb829413bee9", + "last_modified": 1585130632157 + }, + { + "guid": "/^((\\{bb5254c6-92a2-4361-b000-816bc4d34c55\\})|(\\{2fb3aa6a-4ab8-4071-a0f9-379100ac95a2\\})|(\\{0cd3acae-0589-41e3-9525-61786e493188\\})|(\\{3f259f0b-ad11-48c5-a7c4-a5a0897a6d1c\\})|(\\{02b6b8e0-f3b6-4f0a-8055-2baf79c606c1\\})|(\\{9419fd2e-894c-4aa8-aa14-18b5982c5889\\})|(\\{7d0d82e8-308b-4039-b3df-577b41483946\\}))$/", + "prefs": [], + "schema": 1584447915622, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1623342", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-os collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5b107267-ee8d-4ecf-b7d5-64966911681d", + "last_modified": 1584609109587 + }, + { + "guid": "/^((\\{e3d1040d-ea1e-4337-b51b-9f353817184f\\})|(\\{205c14a6-8b6f-49ef-b0d6-41bad99ad5e8\\})|(\\{08ac7dd2-8595-4966-a00b-1ef6a5bdb6f3\\})|(\\{8e937b56-4242-4cb5-b92a-f64452da816e\\})|(\\{9a1ded7e-5fe2-43ad-90a6-3732ceae3d08\\})|(\\{b384b75c-c978-4c4d-b3cf-62a82d8f8fff\\})|(\\{6133a4ea-818f-4e22-bbde-0437d80142bf\\})|(\\{aaf2b501-9d4f-4966-be95-0334bd16b291\\}))$/", + "prefs": [], + "schema": 1584542165088, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1623343", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1dd9652a-c6cf-4cb0-b6f7-7bea8990b4fb", + "last_modified": 1584609109583 + }, + { + "guid": "/^((\\{cde82615-f0be-9aff-872d-31da1bc93923\\})|(\\{85a8a08c-52a8-7d1d-5b80-f1b5cd4751bf\\})|(\\{3d1468f8-abbd-4a7a-95f1-fa5a1201adb4\\})|(\\{386056fc-2697-4a54-bd35-02de8d62fe1b\\})|(\\{1ae2bde0-f2ea-4bf1-a075-08853f9fcf31\\})|(\\{382056fc-2627-2a54-bd25-02de1d62fe1b\\})|(\\{926abf23-d2b0-4142-9aaa-5338b33a0000\\})|(\\{ffedd9c9-45b9-4e11-bbf0-a1408fa0975d\\})|(\\{b35ef07a-7c1b-4b6a-b7aa-de1e918396cb\\}))$/", + "prefs": [], + "schema": 1584542307136, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1623345", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5c8fccf9-b1bb-41f0-bdc3-3a5790fdd4be", + "last_modified": 1584609109580 + }, + { + "guid": "{4e820bcf-688e-4b4e-8773-3861dad6a7e0}", + "prefs": [], + "schema": 1584387693606, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1623047", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d65d4593-d2bb-41ab-b485-9818bc6f285f", + "last_modified": 1584447915216 + }, + { + "guid": "/^((\\{c89232c3-39bb-41e5-a197-7f4412b45fe6\\})|(oniria@valid-install\\.com))$/", + "prefs": [], + "schema": 1583964274652, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1621940", + "why": "This add-on violates Mozilla's add-on policies by collecting search data going to a third party provider or collecting search data without user disclosure or consent.", + "name": "Add-ons violating policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6d431a97-5d54-4de7-a012-5b030c5324eb", + "last_modified": 1584054334501 + }, + { + "guid": "/^((\\{93475144-dae7-452c-a9e4-ef9da092517d\\})|(\\{cfc5bfc9-3dc0-483d-8dc8-27d05f6cff30\\})|(\\{498bc33d-979f-4d5d-93e4-ba608752ad10\\}))$/", + "prefs": [], + "schema": 1583875388267, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1621571", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "965435ff-2bb1-4b17-9e0b-b157d5c53a91", + "last_modified": 1583964274232 + }, + { + "guid": "/^((\\{eb5bd821-bb2c-4968-bf17-8e9336fbdfc4\\})|(\\{84be56c9-16ae-4bde-a556-0b9f7946c1be\\}))$/", + "prefs": [], + "schema": 1583914825954, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1621578", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "dc0fc5f1-511f-4fc3-9558-fc7d9fa61e75", + "last_modified": 1583964274228 + }, + { + "guid": "{dcfd7c5e-ca9e-461f-a748-74f187332e5f}", + "prefs": [], + "schema": 1583788354663, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1621247", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Emoji Library (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "320f94ad-5e02-4209-a066-22f415100d06", + "last_modified": 1583875387847 + }, + { + "guid": "/^((\\{d8e66734-06c1-44f4-968e-9af179ca3fcb\\})|(\\{3e836ac4-d85f-4e8b-8647-6e922b3b2b2a\\})|(\\{71d690cd-dff7-4d28-9cfb-d92bd6522d1f\\})|(\\{f20a4998-3973-4094-b61b-cbaeff99ff84\\}))$/", + "prefs": [], + "schema": 1583757249813, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1620356", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d10ec091-688b-4906-9ed1-42aedc998e1e", + "last_modified": 1583788354268 + }, + { + "guid": "/^((\\{8890a066-99e3-417d-96b6-f71db4f5fec0\\})|(\\{49f9bb4f-f470-48e7-a04e-25baa35209d4\\})|(\\{38799169-e35a-4034-8e62-8d82ae8b8a85\\})|(\\{a67a2dc1-081e-4efb-aa62-5dc7f499fe86\\}))$/", + "prefs": [], + "schema": 1583759516487, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1620970", + "why": "This add-on violates Mozilla's add-on policies by loading remote content into the new tab page.", + "name": "Add-ons using remote new tab pages" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d4568b40-4efd-4cca-9680-ba50dea36ec9", + "last_modified": 1583788354265 + }, + { + "guid": "/^((\\{16004b4c-8fcd-43e8-8867-233023c6fdeb\\})|(\\{79fa9c23-e61f-4434-8b27-0bd44c37edff\\})|(\\{15d5bc6f-15f8-4d0d-9483-7dbbcf722c56\\}))$/", + "prefs": [], + "schema": 1583759652565, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1620971", + "why": "This add-on violates Mozilla's add.on policies by collecting searches to 3rd party providers.", + "name": "Add-ons collecting search terms" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a1caad16-4ec4-43ca-a145-04fbd5fd81c9", + "last_modified": 1583788354262 + }, + { + "guid": "/^((\\{deb55b0e-7064-4552-955c-15ba6a46ad79\\})|(\\{15d967ca-cb54-444c-a7ad-b0dad1dbb723\\})|(\\{7953fef3-37b3-4954-a0e0-806cbec35a63\\})|(\\{2deadd5b-1edc-4634-b342-4e20ad03fd2d\\})|(\\{f74e5fe2-4f8a-489a-8f75-f965a9d4a98d\\})|(\\{0df2d5d8-b197-4022-ad3f-c24a44e9d23f\\})|(\\{e972a079-8e0f-4426-91e9-2c1cf9c8b89e\\})|(\\{d9ce0e4f-e41e-4160-b58e-b99300fa5d83\\})|(\\{43473acc-f92a-445c-9ce3-efc297fb62d0\\})|(\\{5b86a98e-5ce8-4664-9051-3d8f7127d1c5\\})|(\\{5f1e99d2-147f-48bc-aa87-41ed02d65965\\})|(\\{ffd007b3-c085-4bcb-9629-19a528f346ba\\})|(\\{df4e0189-a17d-41aa-af86-b6de65952842\\})|(\\{06880fd6-ffaa-4c77-a5bb-97dcd0ce554b\\})|(\\{f4e654cc-7ee1-401d-ac52-ced86f38e575\\})|(\\{d207c7e2-8a92-4f8e-807d-027fa2eed42b\\})|(\\{8d9fbceb-756f-417a-a691-de9a2c1c5648\\})|(\\{3602c279-d63c-4f81-b832-ef82fda60514\\})|(\\{2f27430f-99b9-4594-83ae-60a4df917e38\\})|(\\{be81f239-4a22-4b02-8878-358a3dcd6539\\})|(\\{e7c3277a-c312-4072-837a-4417aae7269a\\})|(\\{d5783778-d85e-4768-9b91-16b9068df780\\})|(\\{8c6f409e-f740-4807-9a28-c898266c518c\\})|(\\{9b1a4630-5873-4b3c-bcf2-4b4d59cc9739\\})|(\\{8522aad0-47c1-47e6-afbc-1e53a113bb2d\\})|(\\{d0e1738e-e2ce-4e75-bcdf-fdef4c0b7403\\})|(\\{4ac16c6e-ff72-45b2-b641-bf92144e1118\\})|(\\{e9c99267-487c-4fcc-9585-4a8e08d4342d\\})|(\\{0fdf70d2-0dbd-4ead-b746-9cbf4cacac6c\\})|(\\{0393ecde-cfd9-4a72-aa9d-6b64949b890c\\})|(\\{e80bc052-25fe-40ea-813a-dc98dffc2a77\\})|(\\{bfbe94d8-3389-4e4b-8cad-67eedd3bfa08\\})|(9\\.1\\.2\\.1@qamypersonalzodiac\\.xyz)|(9\\.1\\.2\\.1@qafungamesnetwork\\.xyz)|(9\\.1\\.3\\.1@qafungamesnetwork\\.xyz)|(\\{1985b6a7-5f69-484d-b462-667392e9ceae\\})|(\\{0d802f03-a2bf-433b-8124-7a2126bc53d9\\})|(\\{4c7d34ec-3bae-40ab-8300-38136f1bbcaf\\})|(\\{f0a11d18-866f-475d-8804-93d16729e59b\\}))$/", + "prefs": [], + "schema": 1583769750285, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1621039", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Data collection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e3a700b8-0177-4b11-bf7a-ae7c97e5115b", + "last_modified": 1583788354256 + }, + { + "guid": "{2abaf8da-5ca6-4896-8bf4-33cced7d1eec}", + "prefs": [], + "schema": 1583770759014, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1621043", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Pluto TV" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6b7c877c-cf5b-478c-bc1a-d1a4f17f5599", + "last_modified": 1583788354253 + }, + { + "guid": "/^((\\{cf7362f7-d34e-423d-ade0-ff408145d75d\\})|(\\{225a2fd0-2266-4bbf-b1db-b4cd7ee3b774\\})|(\\{89ac12b3-2da6-423f-afdd-a755925070d9\\})|(\\{3c56ddf2-a46b-4338-9dc4-bf79a6947838\\})|(\\{4aec95dd-4720-4b7b-9032-c53c71c69531\\})|(\\{19efb4dd-72e6-4e76-9814-9185f0cebe1a\\})|(\\{a73d9f4d-dff4-47ff-ad33-1747dc74faf8\\})|(\\{58c16ee9-4ac8-466c-b8ac-5d8b019945f3\\})|(\\{a88f8f80-e2f4-4357-9f85-d49fcc22662e\\})|(\\{a623f590-df10-4f74-a281-27457212e744\\})|(\\{14df2d69-1ca8-4e68-8289-23816420eebe\\})|(\\{7789cecb-ccd2-4a7f-a75b-8d0243ab68b8\\})|(\\{0e2b182e-05c6-4830-9d20-b402c5598e55\\})|(\\{fd03573a-9361-4f90-9c60-be6013ebfb8e\\})|(\\{5ae01022-7989-4620-b46e-7ba5859e20d3\\})|(\\{d3ced839-59e1-4389-8631-a9f153187990\\})|(\\{07d66467-bc14-433c-84d7-905e8d2f550f\\})|(\\{3019ef01-bffd-45cf-8cd1-46f6c56cdda4\\})|(\\{1f0c9873-0598-4132-b20a-f0ab42c5c8a3\\})|(\\{b955bec7-392d-4074-bac8-60c6d1f402f5\\})|(\\{9f45dcf0-549e-4f15-a0ad-99ac2821fd8d\\})|(\\{16d5d1cf-0aef-4adf-99ce-214eb32d38fa\\})|(\\{b38602ca-4ac6-40b1-b20c-55828f514b3d\\})|(\\{55318007-10db-4d47-b4df-3946ed3653af\\})|(\\{cd2d96b3-e8ec-4ef9-95ba-72ee0dae011a\\})|(\\{b8250196-9419-4b06-9634-48f6c3570b53\\})|(\\{2a738b9e-5147-4441-91cd-e40a1ef50a27\\})|(\\{777580ee-50ee-4c37-98a9-844111149404\\})|(\\{a44f69d9-9c70-4b33-9502-b19e79399e3c\\})|(\\{2e6daca8-d5b4-4611-bd81-b964ab97bee8\\})|(\\{6fda5ac5-a41f-4905-a5c0-860313ab535b\\})|(\\{d51b14e2-4c5f-4601-b2b6-af8b572171cc\\})|(\\{dc5b9553-f925-43c5-936a-fd9ff0e56e47\\})|(\\{b0d8908e-ac38-473b-b20f-3a8d775c23f9\\})|(\\{d96abf04-438b-45a6-b6ec-3036124b5458\\})|(\\{469003fe-c00d-45db-82b7-c04635c227e9\\})|(\\{7d04eb8b-023a-4966-b6b9-06c706081d74\\})|(\\{41987721-6948-46f5-9e68-bcaf776e35ea\\})|(\\{127f13e3-f58a-44f6-bff8-955dd3688448\\})|(\\{09c02fc4-4a23-43c4-b1ad-854a3e1e6a29\\})|(\\{d061e0a3-7554-4a78-b7cf-e3f57df09b00\\})|(\\{28e445aa-892e-42e3-a0f1-30ab0ebba44c\\})|(\\{bc158e4c-6514-4b0e-9da4-6018326fe634\\})|(\\{fde60598-cfe6-4e9c-9f7a-28ea348a7f09\\})|(\\{a7addae9-82ab-4d91-a2b9-be373207bd9b\\})|(\\{71cc8c13-7aab-49d4-a419-b6e2880daaf0\\})|(\\{9a43f082-11c5-4062-bbad-04b63e6ed433\\})|(\\{19cdad5d-2a97-46b4-ba28-3a191d18b174\\})|(\\{99ee7e8c-61c2-4ffc-8f7a-c70c0bd4f1f9\\})|(\\{47706cc2-917e-4a2d-aba1-ee5c05613e0d\\})|(\\{a1a40297-cfac-455a-ad94-ef20eda2672a\\})|(\\{d807c94c-fbd3-48b7-9c07-a54336ace9f3\\})|(\\{0645230a-9e65-4a9d-ae90-97416f33f29d\\})|(\\{064d021b-9424-4700-b550-80c7a983c240\\})|(\\{59722380-31f0-4588-ac19-670f021cd67b\\})|(\\{0f504621-b4e1-4d5b-89dc-b57399b10c29\\})|(\\{ba135229-de15-41d9-8a35-fd198698fda4\\})|(\\{2117ec43-2df6-4bf2-a468-f067ea721432\\})|(\\{fd3fa8d5-be30-4e57-ba9e-ad11d0f70c41\\})|(\\{cb8e0410-17e0-4866-8075-b3224d52ea6b\\})|(\\{dff54861-9936-440e-94a0-92d39794be5c\\})|(\\{72c03815-0ef1-427f-b577-9c73bd19a7ca\\})|(\\{f13a447c-857b-4d93-a5cd-cb2578ede3d9\\})|(\\{8717ef97-76f5-4729-8a94-1ce396c0d2e9\\})|(\\{5e192df0-d31d-42b7-b866-155068118d2a\\})|(\\{d9238c4c-4259-4c0a-92c5-d03006959c1b\\})|(\\{95716b5d-ce17-4a97-9691-40f62291649e\\})|(\\{a0f9afee-bc26-4fa9-a1df-c705ad21cc94\\})|(\\{8f02a7c1-c6a1-4d3c-923a-59bd7e373205\\})|(\\{2452d750-d1dd-4cef-be51-3cc75f7a62d7\\})|(\\{e077a5e8-62c9-41a6-8427-10445a3d7818\\})|(\\{1685d632-1737-4bae-83a4-b0df6541f187\\}))$/", + "prefs": [], + "schema": 1583005290759, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618688", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons violating multiple policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "41a53d4b-120a-4ab0-9dbc-8a8d3802b734", + "last_modified": 1583233056644 + }, + { + "guid": "/^((jid1-h7qSFwT2a1FJOp@jetpack)|(jid1-w4wG5nJhx4LJZr@jetpack)|(\\{74b0af75-8791-44e2-95a6-7f0ab94143ec\\}))$/", + "prefs": [], + "schema": 1582980182934, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1619067", + "why": "This add-on violates Mozilla's add-on policies by adding abusive content to websites.", + "name": "Add-ons including abusive functionality" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "0.1.7", + "minVersion": "0.1.6" + } + ], + "id": "c4c0b595-6481-42a6-9e94-998b808dd143", + "last_modified": 1582981347968 + }, + { + "guid": "monstercropper@gmail", + "prefs": [], + "schema": 1582726441204, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618273", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Extensions Manager (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4c706648-260d-42c3-8461-3ddcf9db11f9", + "last_modified": 1582916527189 + }, + { + "guid": "{df73c71d-e4b6-46e1-a853-9831ad860a7b}", + "prefs": [], + "schema": 1582752497018, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618518", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Dark Theme (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "59f951ab-6183-4ae8-9ed0-99b667fa2a86", + "last_modified": 1582916527186 + }, + { + "guid": "/^((\\{fa59fb58-b15e-4dec-a9e4-145fbdaa2f03\\})|(somethingadguard@ok)|(somethingsmartadblock@ok)|(somethingublockorigin@ok)|(somethingadblockpro@ok)|(somethingadblocker@ok)|(somethingadblockerforyoutube@ok)|(somethingadblockerlite@ok)|(somethingfacebookadblock@ok)|(somethinghostadblocker@ok)|(somethingnanoadblocker@ok)|(somethingublockmobile@ok)|(somethingurbanfreeadblocker@ok)|(somethingwebmailadblocker@ok)|(somethingyoutubeadblock@ok)|(somethingyoutubeadscleaner@ok)|(somethingyoutubevideoadblocker@ok)|(\\{d3360051-c575-4f6e-938e-ac3bd4598573\\})|(\\{e16f9fba-1ccc-4b59-b52b-69c452bddfd1\\})|(somethingadblockforfirefox@ok)|(\\{b707de34-1ebb-40e3-b44c-d46a2d10443f\\})|(somethingadblockforyoutubeyoutubeadblocker@ok)|(somethingadblockplus@ok)|(\\{4fead186-a35f-41b4-b794-6c90e6f8886b\\})|(somethingadblockerultimate@ok)|(somethingadblockerx@ok)|(somethingfacebookadblocker@ok)|(somethinggmailadblock@ok)|(somethingpandaadblock@ok)|(somethingyoutubeadblocker@ok)|(somethingmublock@ok)|(\\{b49fea32-c666-4259-98bc-50348f3909c5\\})|(somethingdudenmentortextprufung@ok)|(somethingemaillanguageproofreadingandgrammar@ok)|(somethinggrammalectefr@ok)|(somethinggrammarandspellcheckerlanguagetool@ok)|(somethinggrammarcom@ok)|(somethinggrammarlyforfirefox@ok)|(somethinggrammarlytomarkdown@ok)|(somethinggrammarnazimkd@ok)|(somethingjacindaspellchecker@ok)|(somethinglinguixcom@ok)|(somethingmaxigramar@ok)|(somethingngspellingandgrammarchecker@ok)|(somethingpruuf@ok)|(somethingspellcheckanywhere@ok)|(somethingthaispellchecker@ok)|(somethingwordeepproofreading@ok)|(somethingwordy@ok)|(\\{8097be72-4409-4b72-809c-062d67ec1a9e\\})|(something1passwordx@ok)|(somethingapricofreepasswordmanager@ok)|(somethingavirapasswordmanager@ok)|(somethingbitwarden@ok)|(somethingbondarpass@ok)|(somethingbrowserpass@ok)|(somethingclearlogin@ok)|(somethingcpmautofill@ok)|(somethingcyclonis@ok)|(somethingdatavault@ok)|(somethingelpass@ok)|(somethingethernom@ok)|(somethinggpass@ok)|(somethinghakopasswordmanager@ok)|(somethingheroauth@ok)|(somethingid50@ok)|(somethingikeyvaultdev@ok)|(somethingintuitivepassword@ok)|(somethingkeepasswordmanager@ok)|(somethingkeepasstuskpasswordaccess@ok)|(somethingkeepasshelperpasswordmanager@ok)|(somethingkeepasshttpconnector@ok)|(somethingkeepassxcbrowser@ok)|(somethingkeeperpassword@ok)|(somethingkeycatopensource@ok)|(somethingkeydepot@ok)|(somethingkeywi@ok)|(somethinglastpass@ok)|(somethinglesspass@ok)|(somethingmanageengine@ok)|(somethingmasterpassword@ok)|(somethingmaxaccessmanagement@ok)|(somethingmonapassword@ok)|(somethingmyki@ok)|(somethingnextcloudpasswordsclient@ok)|(somethingnordpasspasswordmanager@ok)|(somethingnortonpassword@ok)|(somethingonetouchprotect@ok)|(somethingpassb@ok)|(somethingpassbolt@ok)|(somethingpasscamp@ok)|(somethingpasscell@ok)|(somethingpasscodepro@ok)|(somethingpassff@ok)|(somethingpassfort@ok)|(somethingpassman@ok)|(somethingpasswordcrypt@ok)|(somethingpassworddepotaddon@ok)|(somethingpasswordconfidential@ok)|(somethingpasswordstate@ok)|(somethingpersipass@ok)|(somethingpersonaforfirefox@ok)|(somethingpfppainfreepasswords@ok)|(somethingphashword@ok)|(somethingpsonofree@ok)|(somethingpwdmgr@ok)|(somethingroboformpasswordmanager@ok)|(somethingsafeincloudpasswordmanager@ok)|(somethingsafelypasswordmanager@ok)|(somethingsavemypassword@ok)|(somethingsecurden@ok)|(somethingsteganospasswordmanager@ok)|(somethingstickypasswordmanager@ok)|(somethingsubsfreepasswordmanager@ok)|(somethingmteasierpass@ok)|(somethingtrustloginidaas@ok)|(somethingtweakpassfreepasswordmanager@ok)|(somethinguniqkeysecurepasswordmanager@ok)|(somethingvivokeyvault@ok)|(somethingwebsphinx@ok)|(somethingwebvault@ok)|(somethingxtambroker@ok)|(somethingxton@ok)|(somethingyotipasswordmanager@ok)|(somethingzohovault@ok))$/", + "prefs": [], + "schema": 1582832492598, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618810", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name and/or injecting remote code.", + "name": "Deceptive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c21543e2-3f39-4ac9-b39e-6a2a18249351", + "last_modified": 1582916527183 + }, + { + "guid": "/^((\\{0aa583da-e323-42f2-b4d2-0bc61b493171\\})|(\\{20a15a74-371f-5098-a362-bd127db4f8bc\\})|(\\{c11016db-e96e-4eb7-bc19-7121d96d0e2f\\})|(\\{0fadbf07-bb25-4737-9800-b879a6e1c417\\})|(\\{e7fefcf3-b39c-4f17-5215-ebfe120a7031\\})|(\\{f85238e5-862b-45aa-9d66-0ab56a032375\\})|(\\{ea3f3dc3-6fbc-450d-9120-07b3b03cd9ec\\})|(\\{aa909324-7520-4dcd-9eb0-9f0a9ec3c003\\})|(\\{807833d9-8ea7-42f8-a8a4-46ff7519dd8b\\})|(\\{92047279-0910-4abb-beb7-a7f2cd6cf04b\\})|(\\{94036cd5-1829-4480-ab0b-e2455deafb9c\\})|(\\{05d0e324-7d90-3e2d-2eb0-6f1a9ec3c003\\})|(\\{abd0e324-7120-3dcd-3eb0-9f1a9ec3c003\\})|(\\{578e48b0-7c9b-4890-91ff-f6ce3e958edb\\})|(\\{0aa583da-e323-42f2-b4d2-0bc61b493183\\})|(\\{72d08da8-8277-47f0-8bee-ba5ad40dda6c\\})|(\\{9fd0e085-1545-13de-a831-ab9a05dcf253\\})|(\\{ced9def2-2d86-4a1b-a9eb-29e2f3c9eb48\\})|(\\{364f2138-c271-47a3-9ddc-466c4a27feef\\}))$/", + "prefs": [], + "schema": 1582891948231, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618814", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ae0ffe5f-a7a2-48ec-9b01-2c0c9d3eb2e8", + "last_modified": 1582916527180 + }, + { + "guid": "/^((o-o-0-o-o@codobir)|(o-o-0-o-oSearchBar@codobir)|(o-o-0-o-oAdBlocker@codobir)|(\\{93e2581c-ec13-4be8-bb22-d2544356fca7\\})|(\\{7582f270-d0ea-4f78-a1fe-851def5d06d0\\})|(\\{089fbef0-49fc-4237-ba6c-2604af7a6e18\\})|(\\{7f897f01-98aa-4e5b-b40d-dbdd7c968b8b\\})|(\\{5f6ae810-5186-466b-b7d6-29ab4355e3dc\\})|(\\{02dc7465-0eaf-44ef-999f-9dad3f9502ed\\})|(\\{0135039f-a362-4296-9c30-e8626bb484d4\\})|(\\{8775e445-b777-451b-8d4a-dd10985bf95c\\})|(\\{a244077a-0017-472a-b1af-8f0b4a167a3a\\})|(\\{ebcf3de2-902e-4690-a35a-d6160175639d\\})|(\\{209a92d8-da4f-4342-81b7-ed2b92863cbe\\})|(\\{a2cd102f-34e4-4ef8-87e9-b1d642b6716d\\})|(\\{6382c930-913c-4bfb-a82d-f098cf3e84e8\\})|(\\{323aca10-65de-4392-8ea2-591642470c58\\})|(\\{70508e0e-9f91-4bad-853f-cd76764a5a5a\\})|(\\{b673c390-83d1-43d7-b0e3-a7a2c79b23a4\\})|(\\{b74e188a-c975-4725-a6a7-53fe0645d424\\})|(\\{46ed97fb-8411-4e04-9332-0c89ea8cfbfd\\})|(\\{2b484d2d-5b4b-411d-b8bb-bc3e7d9672bc\\})|(\\{6e707a00-b889-4b91-8cf9-19674f09d849\\})|(\\{f24e3561-ecb6-4539-b9cb-95af4624811d\\})|(\\{46f41d8d-4bb0-4549-86e7-34c09c65b5fd\\})|(\\{99a15fc0-dcf0-4669-a895-664f286c6a88\\})|(\\{0585f5cb-8f50-4e76-82e2-75a0cf852d35\\})|(\\{783f9072-d29c-4567-93f9-ca424cd71fcb\\})|(\\{05d62c84-a9f1-401c-85f8-a657e6a85940\\})|(\\{8c44b1c4-364a-431c-98ec-9c7a88b8fef1\\})|(\\{44da520a-6bd8-40bd-8992-87ab3f866cad\\})|(\\{4dc3d7a3-58bc-4ace-ac3e-2776377595e1\\})|(\\{5cd1ffd4-2899-4b2c-a226-453c85d957c1\\})|(\\{74c7755a-16dd-4cf8-b407-20c47cb4b78e\\})|(\\{865fc230-1a34-4c95-a718-a53276d77a02\\})|(\\{a233e339-2ae5-4d5a-ad1e-d82db722a85d\\})|(\\{23e06ed5-bb15-4cd9-828f-df483e65e1f5\\})|(\\{840e5e73-880e-45dd-a2d0-fbefd09f368f\\})|(\\{dcc9f6a8-5306-4662-a2d5-9da72e9e6641\\})|(\\{a7714acb-7a85-472c-97e7-3310eb47fbc2\\})|(\\{88aeeecb-1924-4c1f-aa22-9d43e8757004\\})|(\\{55295427-ab86-40e0-a56e-f913aba2ae8d\\})|(\\{b80386b3-c4ae-426a-b755-e38999447aff\\})|(\\{71a36a35-73cf-4d33-8f9e-3e1b1f838f2a\\})|(\\{504212ab-b2e3-4157-a36a-b02bca78e727\\})|(\\{c4a86ea8-34d1-4a72-aeff-8b227a52f562\\})|(\\{c1413369-3202-4ec7-873a-23dc5715e180\\})|(\\{97ea8e7f-a2f9-4009-95bc-d9f8be5405dc\\})|(\\{76c146d0-ad1a-4457-979a-a0b84a7ca86d\\})|(\\{ea6a2984-78b8-473f-9850-f3406a8c2b79\\})|(\\{b005573e-35ca-4c55-a87a-25cb53ff526e\\})|(\\{00a73039-4695-4559-820e-e17b5fb5ccac\\})|(\\{fc05507e-2482-4928-b943-a7a6ad59d166\\})|(\\{e8cb2def-d6ab-435c-b2f0-af701a40350a\\})|(\\{524b9704-5aca-4283-aadb-fba6eead9b92\\})|(\\{7d629677-6ad7-43f1-b182-8734474c3fc7\\})|(\\{0edbf5ac-c258-4c65-af7e-8dffbdf847f5\\})|(\\{c5c906de-c84e-4b48-a738-e8e32546f496\\})|(\\{3a9abaab-8451-4909-978a-bf1d4e716137\\})|(\\{1195cc1e-0cc5-4e48-8b08-7160bdab5511\\})|(\\{3e3217ad-344f-4a2b-97ba-d4380a6d4f91\\})|(\\{ee4a70aa-687c-4176-9392-7e47b5c09afe\\})|(\\{7d1d1676-b23a-4249-9c01-f7f1dbcb18bf\\})|(\\{51e9d221-2349-4764-a4a4-00552369c210\\})|(\\{30b40c71-198b-4b3e-8ff1-569860189be4\\})|(\\{beef68e8-c078-45c0-a593-64a9bfb4b2bc\\})|(\\{6ce76b14-1c76-4774-919d-7a3c820a4d56\\})|(\\{522b149a-3a6f-4bc3-ab7d-1bf41e4c29cb\\})|(\\{17a2d3ca-1505-46aa-836a-85ea62b5b617\\})|(\\{d7b81889-d34f-467a-a690-9507b5d72647\\})|(\\{66463498-8185-4874-9fdf-be281b08df26\\})|(\\{5a144fd3-9e6b-4b3a-ba29-bba7fa187e40\\})|(\\{bb405e27-949e-41e2-9930-26cb8360dc19\\})|(\\{f7a40a38-b3e4-4852-9981-8078ae99374a\\})|(\\{8675dfc9-9953-474f-8b2c-16b1307f66ee\\})|(\\{62d44bdc-60ea-4a78-a738-e59dd50589bc\\})|(\\{a7875dff-b031-4588-9d4a-1f989704ebe5\\})|(\\{3cfd3ce1-f358-48a5-a30b-5cc144d14f38\\})|(\\{9ce2677c-ede7-45be-b281-2e0c9d24fff3\\})|(\\{d13d4d9d-e9ec-4833-948f-a8e2c4bb1ec1\\})|(\\{87d402fc-7bbe-4b48-8c1a-38984a9dff7e\\})|(\\{69b186c0-e619-46b9-a978-b6ac97abd26b\\})|(\\{408bfadf-6f2b-428e-aca7-e4b95037bd72\\})|(\\{2a047ecb-a3cf-4ef5-8420-d45ea9cb246b\\})|(\\{c171fea9-aa8e-4348-8b9a-1b371f4821ea\\})|(\\{4f24b616-1f23-4c60-aadb-2386900b690d\\})|(\\{766d6d39-6592-4f47-bd1f-84639a2e5ad9\\})|(\\{b7caa882-3108-487e-9f69-3f43168789ac\\})|(\\{e96c08ad-1eff-4687-94ed-a14a76c7152c\\})|(\\{cfddeb9b-5339-4df3-af2e-88e26987cb8a\\})|(\\{d558cc36-a306-4288-9888-a68c419b2cf7\\})|(\\{0765615e-cc53-4ed8-9501-8e33b627db77\\})|(\\{0de24de8-5130-43bd-b041-2d284a26b9f0\\})|(\\{114a7625-4642-4013-bf25-9d8c8a1e428c\\})|(\\{d2554ad1-d14c-43b1-9f3f-84ae454748be\\})|(\\{374319ec-ad46-450b-928c-feff04666d58\\})|(\\{f8a572d2-a614-45aa-a8a3-3cb6bdec3388\\})|(\\{d55d0631-bd77-42b3-8ca3-ff5889d69e4d\\})|(\\{4bf71a8f-3f21-4de0-8cc6-12a53df60922\\})|(\\{5787258d-5a12-4153-bd9c-5825c53786a0\\}))$/", + "prefs": [], + "schema": 1582892445504, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618862", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name, violating Mozilla's No Surprises policy or including non-disclosed monetization", + "name": "Misleading or deceptive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7c948a9b-2340-4886-b8fc-29ca66c1790f", + "last_modified": 1582916527177 + }, + { + "guid": "/^((\\{e78b3b45-89ad-4210-9a6b-3150506851e1\\})|(\\{c5874e9e-f46f-4220-b356-476122247a0b\\})|(\\{941c102c-7b8d-40af-9f49-a0496d89b486\\})|(\\{97c53a72-d50f-4e1c-8d26-f0aa8578a4aa\\})|(\\{65d9977d-e895-490e-ac9e-83031d46551f\\})|(\\{6e27edd2-f950-4b69-9a4a-f4941f2b0621\\})|(\\{af4d7096-c51a-4cb6-a30d-d2790fd66325\\})|(\\{b666da6b-b171-465d-ac69-20ea2a8a9971\\})|(\\{8e83ab0b-d34a-49ce-9d64-6394637cd9e4\\})|(\\{763aa5bc-8faf-4103-bad2-d15aab97146d\\})|(\\{40613d6c-a8f9-4a8f-a048-72ca8f1206a5\\})|(\\{865cf362-afee-472d-b2ae-6186e59f8e3c\\})|(\\{6778c1de-8ab3-4ee8-9e3d-788b17060ffa\\})|(\\{a501a160-de33-4c20-96a8-b660fd18db01\\})|(\\{b27d330c-3ac1-4421-9c96-1a5781ee9bff\\})|(\\{3997ee99-bd51-4f18-95b1-3b8a887ea014\\})|(\\{ffb40ecd-2ef3-4dfa-8c69-9a847c853b0e\\})|(\\{061c317b-b050-46a9-a7a5-2f9dfc0ef3c7\\})|(\\{e30d3343-fc0f-4526-8b9a-eebd6b7b1af2\\}))$/", + "prefs": [], + "schema": 1582898093853, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618862", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name, violating Mozilla's No Surprises policy or including non-disclosed monetization", + "name": "Misleading or deceptive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7548e625-f059-4976-ab43-de78a1124dd2", + "last_modified": 1582916527173 + }, + { + "guid": "/^((jid1-C96jkOWH1jEZ5A@jetpack)|(@pdfit)|(@youtube_downloader)|(\\{503b5ae9-dcb1-41c4-9cf8-0c70a5c2bc70\\})|(\\{a9de7157-1c27-45bf-ab59-bedcbf57cff9\\})|(\\{908c4278-9f44-4b59-ad6f-91e2aabc8682\\}))$/", + "prefs": [], + "schema": 1582659695507, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618172", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "98dbf563-0a8e-4796-ac48-1be0e4520610", + "last_modified": 1582726440807 + }, + { + "guid": "{6059d268-eb5b-491c-9879-964e1c67c854}", + "prefs": [], + "schema": 1582723099417, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618174", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Second Tab Search (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e15a83e1-60c6-475d-977c-83e180646e6a", + "last_modified": 1582726440804 + }, + { + "guid": "@searchencrypt", + "prefs": [], + "schema": 1582723268591, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1618185", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search Encrypt" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3c782d96-5d25-45d3-8f07-016a791e316c", + "last_modified": 1582726440801 + }, + { + "guid": "/^((\\{374c327c-fbd0-4769-a378-1cb0fef54b1c\\})|(\\{11d9b7d9-acea-4a66-94ff-3fbbcca15abc\\})|(\\{e9e3ac53-8384-4f2a-a451-78af21417b13\\})|(\\{61539a14-2728-448f-a071-45043287cf10\\})|(\\{9ca7082d-0e18-465e-8588-8d3990c5b9ae\\})|(\\{0444bcb0-66d3-49b1-ac1f-b79f7f682f15\\}))$/", + "prefs": [], + "schema": 1582486893083, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1617571", + "why": "This add-on violates Mozilla's add-on policies by intercepting page loads with affiliate urls.", + "name": "Affiliate add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f8ce5794-a1db-4a63-af32-1750de36a383", + "last_modified": 1582562264396 + }, + { + "guid": "/^((bazaarhero@jetpack)|(boingdeals@jetpack)|(boingdeals-1@jetpack)|(boingdeals-200@jetpack)|(\\{e9edde2d-5dda-46b5-b6ca-f732d96d941a\\})|(\\{29c11376-f72c-489b-a9eb-8fe309b1623e\\}))$/", + "prefs": [], + "schema": 1582548114995, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1617577", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1406ee6a-4eab-441d-ad1f-ce09b650ce2d", + "last_modified": 1582562264393 + }, + { + "guid": "{6ac0938d-7ca6-43ac-9c26-653d37820440}", + "prefs": [], + "schema": 1582549126839, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1617578", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "414cf401-fbc9-4218-85fe-5880068e8452", + "last_modified": 1582562264389 + }, + { + "guid": "/^((\\{d551e6af-55af-4295-92f0-ba0e3d15bb00\\})|(\\{36858137-f125-47fc-8e0c-499f161abaf0\\})|(\\{7edb2ea5-61a9-4447-87b2-d37ed4198d1c\\})|(\\{0ea11f50-96b6-4a8b-92f4-1eb6ca77acac\\})|(\\{1449de8a-cdb8-4f65-b909-c6391e49615f\\})|(\\{e7cdc797-edcb-46a0-a66e-f5dab3c808d6\\})|(\\{d57b5f9f-b35d-4879-84a3-3547d44249ec\\})|(\\{d4e41bc4-cb00-42d0-b756-ed8deb827cbc\\})|(\\{083acfab-6228-439a-b796-62647803b216\\})|(\\{ed2df6fc-df45-43ac-a923-bd3fbc3534c2\\})|(\\{ebc63925-e43d-4cc1-b234-4c8b6abc331c\\})|(\\{231652e4-ad9a-4dd7-89b1-d79876c0dca7\\})|(\\{2cb0bdaf-5789-415b-a92d-2adbc695cb63\\})|(\\{429a08cf-84c4-4eae-9ecf-fc6c3a07249c\\})|(\\{ec4fb55e-d94d-4196-9ee6-a6f19e3c9405\\})|(\\{bd55549f-9853-427c-a45d-6aba2b4776d2\\})|(\\{bbfa0b1d-c75b-4483-8331-b5b01c82fe5d\\}))$/", + "prefs": [], + "schema": 1582237690303, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1617192", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Lusha" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b12eaf8d-77ff-4bd6-8b9f-797af110f86c", + "last_modified": 1582301451440 + }, + { + "guid": "beeline@beelinereader.com", + "prefs": [], + "schema": 1582298543556, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1617198", + "why": "This add-on violates Mozilla's add-on policies by making use of obfuscated code.", + "name": "Beeline Reader" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "2.10.1", + "minVersion": "0" + } + ], + "id": "42642ca5-9342-4e51-a2ec-b5eed799468f", + "last_modified": 1582301451437 + }, + { + "guid": "/^((\\{e046e345-918c-4fe8-89a2-0987c5d12636\\})|(adi@fmt-tools\\.com)|(\\{90e41842-755d-40e0-9136-8129df55a64b\\}))$/", + "prefs": [], + "schema": 1582296975853, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1605160", + "why": "These add-ons violate Mozilla's add-on policies by making use of obfuscated code.", + "name": "Add-ons with obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b96957ff-ca4e-4ec4-97f7-3dbb4ddbc479", + "last_modified": 1582301451434 + }, + { + "guid": "jid0-G6461UajDjhNAwSukoedlkhD0XA@jetpack", + "prefs": [], + "schema": 1582141291510, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1616735", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control and by collecting ancillary user data against our policies.", + "name": "Ratings Preview for YouTube" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "dca3181b-35a3-4dd0-9efb-8e7a19fab4d5", + "last_modified": 1582237689888 + }, + { + "guid": "{e0a905db-294f-4e91-be75-8b5c7c5df90a}", + "prefs": [], + "schema": 1582103702651, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1614333", + "why": "This add-on violates Mozilla's add-on policies by making use of obfuscated code.", + "name": "Gladiatus Time Saver" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.2.7", + "minVersion": "0" + } + ], + "id": "a98992d1-8a30-406f-944d-c0161d0aaa21", + "last_modified": 1582237689885 + }, + { + "guid": "bookmark-this@page.xpi", + "prefs": [], + "schema": 1581968493407, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1616217", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "24b4f43e-106f-44a6-a992-7d07634bb5eb", + "last_modified": 1582103702173 + }, + { + "guid": "/^((addon@faster\\.com)|(\\{09bfa06b-5e73-4daa-96bd-88092fe71c52\\})|(\\{eb4d44d6-4ad4-4b77-9509-8e6eadaef074\\})|(Blockfbunseenads@addon\\.com)|(Blockfbunseenads0@addon\\.com)|(\\{a8180cdc-19f4-4299-9bb5-5b6b2f2c2de6\\})|(\\{2454bb7d-4cf7-40f3-a5da-40e6889d83fa\\})|(\\{cd6999f9-c84e-47b4-a355-d54bedfe9c17\\})|(\\{53d5a956-eecc-456b-a6ce-b137c511a04d\\})|(\\{9b2dba6e-c179-47ca-bbd8-6b7033c4f2fc\\})|(\\{d77d7463-a323-4b3a-9b96-69b31e4d488e\\}))$/", + "prefs": [], + "schema": 1582023416031, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1616233", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e9119771-cb28-4853-8aae-72aedfe27b2f", + "last_modified": 1582103702167 + }, + { + "guid": "/^((\\{6fb37444-4339-4a36-9e4f-c4c47b550c4e\\})|(\\{430b4bcb-4487-471e-82b8-a3056ae137ba\\})|(\\{394990b1-4ede-4bdd-a3fe-58506591f00b\\})|(\\{3ca06113-1cea-4e7a-b775-19666a9c77a2\\})|(\\{deebf5b9-62b8-4b5c-8e1b-8f8d83939441\\})|(\\{15c41cbc-18a2-4756-80a9-418ba03f533d\\}))$/", + "prefs": [], + "schema": 1582027274373, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1616234", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons injecting remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cc8e1b77-eab1-42d7-9625-bf4153547903", + "last_modified": 1582103702162 + }, + { + "guid": "/^((\\{9664bedb-6f24-4672-8b72-2565e0b3fc21\\})|(alex\\.todor@gmail\\.com)|(alex1233\\.todor@gmail\\.com)|(alex123333\\.todor@gmail\\.com)|(alex131\\.todor@gmail\\.com)|(alex13\\.todor@gmail\\.com)|(alex12223\\.todor@gmail\\.com)|(alex1214122223\\.todor@gmail\\.com)|(alex1222214123\\.todor@gmail\\.com)|(alex1222214fawfaw123\\.todor@gmail\\.com)|(a\\.todor@gmail\\.com)|(abvcaasgawgaw\\.todor@gmail\\.com)|(abvcaasgawgaw122141\\.todor@gmail\\.com)|(abvcaasgawgaw122fwafaw141\\.todor@gmail\\.com)|(abvcaasgawgaw122fwaaaafaw141\\.todor@gmail\\.com)|(abvcaasgawgaw12aa2fwaaaafaw141\\.todor@gmail\\.com)|(abvc\\.todor@gmail\\.com)|(ateststn@example\\.com)|(abvc1\\.todor@gmail\\.com)|(test@gmail\\.com)|(test1112@gmail\\.com)|(as@test\\.com)|(stagod@test\\.com)|(stagod_qa@test\\.com)|(stagod_as@test\\.com)|(fop@test\\.com)|(\\{e83eb1bf-7847-4253-8108-4c1ec0601757\\})|(\\{3cfa4fd4-a747-4666-b97e-2262f6255eeb\\}))$/", + "prefs": [], + "schema": 1582038056642, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1616286", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Addons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "32ebf2f0-a321-48fd-a5a4-94a4980f4697", + "last_modified": 1582103702156 + }, + { + "guid": "/^((\\{04871291-3f6e-4969-a991-4183decfacef\\})|(\\{9493fc95-5a66-445e-9200-c9b617942c05\\})|(\\{a89d4fea-98ea-42bf-aeb7-e14059e809a2\\})|(\\{aabe1376-bc1f-440d-a275-284d026f3058\\})|(\\{b6aefc39-259d-4485-b8fa-c9b2ea77223e\\})|(\\{5ce16faf-66ca-401c-bf43-7c75cf75487e\\})|(\\{dfdf383a-eff4-4ae4-a8b8-97640748e795\\})|(\\{6a563873-967b-4644-afb1-bc3e9da68128\\}))$/", + "prefs": [], + "schema": 1581545094971, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1615250", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1c5195a4-afb7-47ef-83c6-7c3c6f5d0ed1", + "last_modified": 1581702872026 + }, + { + "guid": "/^((something@ok)|(\\{4eefc8e1-086b-43a3-bb21-17c05f43cd3a\\})|(adv@blocker)|(adv\\.3\\.2\\.8@blocker)|(\\{f56e22e4-f6f9-48ca-aca3-480d9b293a16\\})|(\\{b26aabf7-6b17-424f-a71d-e8ca37ccb672\\}))$/", + "prefs": [], + "schema": 1581687835975, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1615601", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons injecting remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d5259975-b1c3-4f50-861b-d7b4c082b58a", + "last_modified": 1581702872020 + }, + { + "guid": "/^((\\{6e27c586-e50c-49b1-9925-5427f3e9f028\\})|(\\{ae6dcbb6-d0a6-494b-bdfc-ea9d7b8f7667\\})|(\\{14aec2a8-0289-43f3-85fb-5d52491a0538\\})|(\\{b541ce5d-e99a-4629-9167-e71ac65bf32f\\}))$/", + "prefs": [], + "schema": 1581450092560, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1614884", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ba0f5f7c-8998-49f1-89a8-7f3a15c41cc8", + "last_modified": 1581545094556 + }, + { + "guid": "/^((\\{0bbf5ce8-0bfb-4589-a4e8-4f1e435389d2\\})|(\\{820847ac-fb62-47a4-a6be-00d863584c76\\})|(\\{703d2caf-edff-4198-baa3-615135f97890\\})|(\\{f459049d-939d-432e-83c7-07ced47e629a\\})|(\\{0d56e009-957a-4575-a984-ecd22ccf3120\\})|(\\{88386103-f99d-4aab-83c2-64106a77748e\\})|(\\{29e30d06-1030-4a82-8e29-25706012da97\\})|(\\{c0d5c1cb-e676-4ff7-8189-793efc86fa2f\\})|(\\{aade33ec-2184-4759-8de5-5d61f1e29e72\\})|(\\{72dc5fd5-179b-40b6-9218-e88434939ed8\\})|(\\{8c9ec486-bd7b-40dd-ab49-1ca3ff452484\\})|(\\{67c0ce2e-2359-44e7-b9d9-1061a3c4a041\\})|(\\{149c0e63-b3f6-4f8d-bb1d-94b7e36d8aea\\})|(\\{c3a1e646-c155-4a40-b5bf-ac9252e6b632\\})|(\\{6876f3d2-97d6-46d6-b9bf-aa46264f6c3c\\})|(\\{330a08b1-d93e-48a2-9081-e93e25190eec\\})|(\\{443305ec-55d7-411c-bb4a-96e83b4e631e\\})|(\\{945964bd-5c29-48e3-9dd6-26741626c1ba\\})|(\\{ec8513c5-2bcc-45b0-ae77-da6685cfafd9\\})|(\\{57703f70-e1b7-462d-bf7e-657bac5eb30c\\})|(\\{8c1d6a6c-3745-429e-8ec5-2a374320e703\\})|(\\{2ff583b8-72a9-40bd-877b-b355ad33ce44\\})|(\\{f2ed910e-ab21-4ad3-a70a-8adca5e683f6\\})|(\\{4ae1f921-575e-4599-8b77-e8e7ab337860\\})|(\\{cfe7c709-6df6-4d54-9f4c-6fc3967904ce\\})|(\\{4e0cceff-558b-4902-9870-55ecb16f78a6\\})|(\\{093726f6-5907-480b-81fc-fafe4f04b5e8\\})|(\\{4a2650e6-fb1b-4e83-b0ad-4e991c9ac01e\\})|(\\{2dcd1f94-6a18-47c3-826a-d8f1044b3ade\\})|(\\{90e61a54-35d6-44c8-bb33-88623e6a03ae\\})|(\\{61389b8e-55eb-43ad-a68d-bebefe7476ad\\})|(\\{f8890846-fcc1-479f-a90b-dce3e486b0ba\\}))$/", + "prefs": [], + "schema": 1581438380344, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1614707", + "why": "This add-on violates Mozilla's add-on policies by collecting search terms or intecepting searches that are going to a third-party search provider.", + "name": "Add-ons collecting search information" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d33858b8-d192-4be2-a844-b151f0f287ca", + "last_modified": 1581441570691 + }, + { + "guid": "/^((\\{312951c4-a455-4886-a2f1-e4fb05b9fee7\\})|(traderibis@gmail\\.com))$/", + "prefs": [], + "schema": 1581429986729, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613706", + "why": "The add-on injects remote HTML code not compliant with our policies.", + "name": "TraderBiS" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "52b9033d-de65-48ad-9062-7c340835feb0", + "last_modified": 1581438379932 + }, + { + "guid": "/^((\\{ebba9c65-d04c-4c02-afd1-476d7c06fc2f\\})|(\\{117f05e4-3403-4cbe-a041-30fa60054e9f\\})|(\\{03417bad-eb5b-4bd4-89f8-f10d78fce082\\})|(\\{42f0a4e1-710d-4154-b34a-731dbbe2097b\\})|(\\{6a0a9103-3482-4717-aca8-f22ee2228d25\\})|(\\{23d5c4a6-1231-4f87-ebf9-e5800eae221b\\})|(\\{99a32eb1-5da4-4a35-beb6-7c3b5222ce04\\}))$/", + "prefs": [], + "schema": 1581277291027, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1614308", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "39473bb3-638d-4b7e-a847-f197e6320564", + "last_modified": 1581340917940 + }, + { + "guid": "/^((\\{0E64F71A-6B21-4324-A5D9-69231DE8C20C\\})|(\\{747e7f5b-c9Fa-4e4f-b466-639b88c8998f\\})|(\\{8693cb76-1caf-4115-9bd7-6bab02330326\\})|(\\{cf8452e1-0ddb-44d2-95eb-4cb8e7a35707\\})|(\\{79bbada5-9050-4d8c-9367-0aad8185cd90\\})|(\\{C7928956-827D-4649-A234-BB758377C005\\})|(\\{512d597b-9bbb-47cf-b0e6-526456ee6d26\\})|(\\{fd591fc9-480d-4f04-8ae5-0be02121b425\\})|(\\{e3258209-0248-4092-bc59-3032565d2f0a\\})|(\\{725fc0a6-1f6b-4cf9-ae17-748d111dc16d\\})|(\\{3e30848f-3b39-425b-8538-d85da166cb19\\})|(\\{9e27a3ff-0131-4aed-bc7c-2364bd89d7c9\\})|(\\{74e039b8-a2db-4a41-9155-4ccfc2c86682\\})|(\\{cfd9177e-c8c6-4d94-b5e9-d87850e2c8aa\\})|(\\{e21459c1-5604-4324-a3a0-8d23800725ec\\})|(\\{a563f3c0-d631-442f-ae27-1467cd47f1f4\\})|(\\{cbb3237c-ed1f-4735-a364-1849c0907b3b\\})|(\\{5c88311a-c457-4d3a-961c-3dc0cf01e6f9\\})|(\\{0bdf29c1-0c18-40cf-b3a2-c938b5cfdabc\\})|(\\{56e0ce0d-ffff-46a4-b2cf-1260d6898b94\\})|(\\{673d181c-5a1f-42c9-a8a2-f2f5af4a2a2a\\})|(\\{63dc2763-30bb-45fc-90ce-0d2a5c07a46a\\})|(\\{2751af61-65b3-4e7b-b098-497d6d6d7629\\})|(\\{5ce7c96e-2e99-4a6d-8f5e-cbcd68fd64b6\\})|(\\{e7e1a9ef-2755-445f-a639-d9bbf5e1982f\\})|(\\{ef5483ba-d634-468b-93d0-1bed3f09306b\\})|(\\{10367a10-912e-47c8-ab86-9a752230e097\\})|(\\{bf13ca91-6471-45c9-999a-e6176ffa9681\\})|(\\{7b8ddd2d-c2cc-428c-8Ede-604995d8B4da\\})|(\\{d05d4f3c-74ba-40bf-afa0-ad944df067ae\\})|(\\{fed8f5bc-0324-4007-8e90-84709e0c57c8\\})|(\\{9ef981d5-0b21-41fd-9412-008633de79f2\\})|(\\{1569b001-9f51-46a2-a4ed-f2122c475799\\})|(\\{340eab9c-f3c6-4a6c-9439-79733e12df5d\\})|(\\{1ab78f66-5390-4db2-8d1e-c759599ef2a4\\})|(\\{ce00b27c-1a7d-40f9-be61-57ae046ebe1a\\})|(\\{9fc1abcd-3044-4bbb-b04f-c233f1d20bfc\\})|(\\{78255a39-463e-4fce-9a2a-acca442414f3\\})|(\\{159bf803-5c0e-4863-bfb1-7a036174bef5\\})|(\\{7828da87-6bf1-4799-91cd-6e9167511fa5\\})|(\\{9aa7920f-677b-4904-9ea3-cae4c4320d15\\})|(\\{3e78b05f-90aa-496c-ac3f-cbc92a738fa1\\})|(\\{cd9d2474-fff2-4f19-8452-0ec2f4422117\\})|(\\{c7722662-0964-4341-9232-0fdebb37811e\\})|(\\{fdb0fb76-f05d-4732-9348-6f8765673e14\\})|(\\{05abc1ee-f871-4ce6-a51d-6011cdf545ba\\})|(\\{300e263c-aed7-4df7-b197-82d5122ccf6a\\})|(\\{14775497-c7c4-4209-9d6b-a8a40c0aed1a\\})|(\\{10f8343f-0f77-4cb4-8266-e06dde15e8ca\\})|(\\{d2a58c6b-be02-4ea5-9e7e-6982454e9ef3\\})|(\\{0f27c13a-5921-4e7c-86c0-b43ad63a4bea\\})|(\\{f1922123-3aad-41cb-8ab5-281ec8cbe351\\})|(\\{be0f8a88-522f-43a3-8146-5b2ad0985987\\})|(\\{44056c1f-6259-4ec2-8e60-b1c65f9b1039\\})|(\\{1e4c4a84-ea2a-45c0-935e-59cc375575ee\\})|(\\{62a11f62-84b9-425f-b506-93c3711b73e7\\})|(\\{bcb5539d-0913-4d90-94a0-e3144ab239ac\\})|(\\{e4dd1b2b-dcd2-494e-b69f-8c76e89a6234\\})|(\\{48eeddb6-f7f9-415d-9835-b859e37ed024\\})|(\\{ddf2a2fa-a5ed-4765-a44c-4b56077cd588\\})|(\\{2a208cb8-5297-47b4-91a7-0e66475218d8\\})|(\\{7ec57eac-456d-442a-85f2-477cb20c3dde\\})|(\\{20aa1960-d5c2-48fb-ae09-fa6261381537\\})|(\\{b4ebedae-d18f-46d6-8b87-679d1fd27f3d\\})|(\\{67fa631f-29fe-4001-892c-1dfdd56e5ed3\\})|(\\{ae92cb39-b2a6-4865-a125-1b273ffb4a1c\\})|(\\{5b2245da-f41b-4fb3-88b3-5e9a097e06c7\\})|(\\{69272cbc-110d-4b5c-b903-16bd118ddf45\\})|(\\{d4d22de7-54af-40a1-99f7-c2b0f2d86a23\\})|(\\{e1445f3b-92ee-4085-a57f-380e96ed8316\\})|(\\{f9830d93-3782-44e9-b199-d2355f61b98f\\})|(\\{218769cc-3b47-4978-ac59-8a9447bcb193\\})|(\\{f472ddca-60dd-4d6b-90c7-244a84d0a487\\})|(\\{8af6985d-1eef-47be-8213-bd6e25dad273\\})|(\\{da95491a-1812-442a-bde4-7a29f69044d4\\})|(\\{f8d10888-29b8-48d3-8e5f-e02e41fb642e\\})|(\\{e9091337-2af8-440e-a318-ca9e0f053fc0\\})|(\\{3ba1e00c-8339-4ae3-a87e-f4af9f1ee33f\\})|(\\{b40e5bcd-5966-424f-8a15-6ecc3dba050a\\})|(\\{c376e851-c30c-48a1-9f59-d528d34560b8\\})|(\\{8ad33b1d-31a7-47e6-bb46-6c91bc1daea3\\})|(\\{dac8f2fe-662b-4017-bf62-5052d0b23af9\\})|(\\{c27b6b15-fd63-458e-91b8-5974a7b2242b\\})|(\\{b222e127-a52c-4884-8a0a-065342b0c74d\\})|(\\{f53f1857-3f7f-4cf2-9e4e-2533f2c253ee\\})|(\\{eb260b8d-f7d3-48d8-a29a-c2b07e1ed36e\\})|(\\{49f6227b-4803-46aa-b189-466869e8dc5d\\})|(\\{a8694ce8-04ee-49c6-81cf-cf005e7009b4\\})|(\\{c53aa3f1-3385-4e2d-9d38-daf09b0db7bd\\})|(\\{9b230441-bb88-4060-bcc9-637cc5a54639\\})|(\\{895ac4f5-bd07-41c9-bbe1-cdb4d2be9975\\})|(\\{6886d657-954f-427b-b52f-9ddb1b52da62\\})|(\\{d1bc06d5-8470-4120-a2af-8e05b252ccca\\})|(\\{2aad1b2d-c29f-4623-83ac-6291c169c4d5\\})|(\\{e54137a1-794f-482d-93b6-13e2dddf7bf7\\})|(\\{773b7d0b-dc32-4125-83b4-a74887588698\\})|(\\{dd8a0577-a2ef-4719-b138-8aae7cf05add\\})|(\\{9b0dc71f-13e1-42f7-9e64-518cf228c079\\}))$/", + "prefs": [], + "schema": 1581005656992, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613891", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8b5feaac-be07-4612-a090-4714b80191f0", + "last_modified": 1581082030075 + }, + { + "guid": "/^((\\{7b931d6c-4721-482d-8fcb-8c789bafc9a4\\})|(\\{03ea1c25-7fae-4ca7-9e9a-4b161936232a\\})|(\\{06580af3-ba0f-4e49-966d-932b19f6914d\\})|(\\{7f2afade-1aa3-4acd-8161-4a7d111535c4\\})|(\\{45fc7dea-37bf-46ca-9ccd-4b156778ed55\\})|(\\{b8f12b3f-ebe5-4599-9c51-1414ba5783be\\})|(\\{6440c999-2444-49d0-b8b4-188503a14a32\\})|(\\{0741d175-fd06-414e-91ad-095d6484b8e9\\})|(\\{db88d1a7-fc18-46b5-b015-bd9599966cad\\})|(\\{4f3df885-1268-43ce-9e2a-45668ce523c3\\})|(\\{938ec54b-e728-4e04-9a05-108b2246cf51\\})|(\\{bd474d1d-02ad-40a4-9237-4cd64d16de55\\})|(\\{622b8278-44d4-4f9a-ad96-1363247ae6c6\\})|(\\{9fd1d710-ebe9-4da5-a488-d51eb1e88ac7\\})|(\\{ee4314d4-00a2-48ce-bd11-4364496faa8c\\})|(\\{a06cfa87-55e7-4c65-a71c-a5e984c38e94\\})|(\\{1e36e1a3-6756-4ecc-9138-6939b23ed54c\\})|(\\{219cb4e4-5e4a-484a-844f-fee741dc65fc\\})|(\\{057669e1-0925-4d28-bd20-0fb643ad290e\\})|(\\{466bf5d6-c235-4731-9539-ebf90346f096\\})|(\\{a96faf19-5035-4149-9664-773abbc0881b\\})|(\\{656b685b-a458-4651-bb39-66242c67dd6d\\})|(\\{1acdd00a-744a-4875-be39-5a136aa20d7d\\})|(\\{58873ce7-60eb-46b9-a4af-ae62095b3fb1\\})|(\\{387033d3-4292-486e-80f4-44e531349a54\\})|(\\{5b8b07d1-4e0a-4c96-aad5-672a00c1358e\\})|(\\{15a8fac7-2bf1-4536-b4a3-7f51c9b7fa1a\\})|(\\{668952b1-4609-453e-9972-e9557192070a\\})|(\\{5f017950-0824-4806-9cda-e17d454f1b18\\})|(\\{008db332-feb7-447c-8359-829c2e5fc374\\})|(\\{3d412505-479b-47c0-b330-3599533e783a\\})|(\\{117a7b8a-6c7e-4fff-8d48-e12b02871b91\\})|(\\{8753c4a4-e083-49db-872c-eef47d1d58ab\\})|(\\{f439338b-3af7-4afe-bd48-77ee1039077e\\})|(\\{32588122-b871-43fa-b845-60b548a30235\\})|(\\{89a92c73-87b1-48b1-bc68-b814d1348d1d\\})|(\\{dbb477cc-8be4-44f4-9cc2-845632a7e433\\})|(\\{da48acfb-08ed-484d-90b3-0e63c759a4f4\\})|(\\{af40910c-c446-4a65-843e-5c39b6ef38f7\\})|(\\{0121e1fe-e88a-452f-ba88-7c330f87c137\\})|(\\{9cc2b320-5edf-4b20-be94-108583cc9b66\\})|(\\{babab679-6c9b-40c2-b62b-e21a3b4b5734\\})|(\\{5e4d38a3-eefd-4fb6-bbe6-5eb69295f37e\\})|(\\{ffb5e280-9fb1-42c8-bfbc-ac2db8232d9c\\})|(\\{50198f5f-f09b-4d09-96e7-ca94e327ce7b\\})|(\\{eee19d83-6edb-4b9d-b483-1070d60595a7\\})|(\\{7c5e2b18-56ad-4249-8390-03dd99aa5b09\\})|(\\{61af5d39-dd37-46fb-8ead-c7756764458c\\})|(\\{fb0dabc2-2a3d-45b2-8ed4-dc22b74f0ab4\\})|(\\{e3eb4df8-0cfb-4380-a7c0-856d4deda887\\})|(\\{180a38cb-115e-4794-a039-696446dc6b4a\\})|(\\{91dc3cb5-ab77-4e1c-af09-d827a0df9e08\\})|(\\{c4fec1da-703f-49fa-bd3a-5d7f939d2ae0\\})|(\\{a75f51d6-8d4b-4a28-b706-300403b82859\\})|(\\{d79394db-944c-4820-a90c-a2f48ab5fff1\\})|(\\{d6f11f95-a27b-47cd-bbcf-a9b5f2dd2a36\\})|(\\{3e64646d-9618-44ca-bf27-e424e76bd622\\})|(\\{43b57421-3ba8-4116-82c8-afd142c05674\\})|(\\{30935b8f-9a74-4e49-b6f5-8bb95d3ab3a7\\})|(\\{1ec5af15-c738-492d-bdcf-c14fd5be5e13\\})|(\\{feff2b30-7349-4e7b-9a9c-541f97dbc9e5\\})|(\\{48dbfb68-13d3-4372-820d-d52ea58ebdfd\\})|(\\{1a3a904a-3daa-4d1a-8260-c20b44fec6e3\\})|(\\{3794f3f1-6a11-48fb-a7c9-b33344ee82ca\\})|(\\{e3587df7-7358-4a36-aeff-944db7ab30ff\\})|(\\{43a526a3-28ea-409f-933c-2ef3d9a0629b\\})|(\\{2d3e88ab-b4af-47d4-b79b-a0becf1437b2\\})|(\\{0e02d0ec-97b5-4b46-b42e-d4179b067478\\})|(\\{dc7083b2-64f5-4ec0-a84b-3e5fdd552f11\\})|(\\{69d07419-67e5-4465-ad46-b969d5e5c3f8\\})|(\\{f31bdeec-878f-4465-b9f5-e844b45eb9a2\\})|(\\{9b63b79e-32c8-425b-ad18-753b58b73cbe\\})|(\\{8caf71a8-6c65-4cd4-95c6-9913dd169278\\})|(\\{e1f6fe90-e0fe-418d-9ff6-566cdd5b60e9\\})|(\\{e34bd75e-5e7e-48e6-a84e-1d18e5fcfb2d\\})|(\\{087940d8-dc20-4e9e-829b-7bd96c37b02d\\})|(\\{48defe55-de7a-4051-a5cf-ac6a649e66bc\\})|(\\{8bd54503-66cb-49dc-81ec-9fa0e9c42fe3\\})|(\\{ed9f9df1-9f6c-4e4a-a4ac-5d422a7c2a5c\\})|(\\{13261711-0eeb-457e-9035-7c415e286830\\})|(\\{E3F6115C-B027-11E8-AD5B-6E4CAE35F1A2\\})|(\\{93654046-f548-4d3e-9370-cc2244406725\\})|(\\{943ba377-5410-47cb-a025-30d55960622c\\})|(\\{426123a8-3e04-4887-a4a7-18931eaa428e\\})|(\\{4c50cefd-8f14-41ab-b719-8606b116d1c2\\})|(\\{ab2d09e4-07c0-461b-94f3-b2ea2a6773b9\\})|(\\{3f20ebf2-5869-431e-a73d-53d435176b04\\})|(\\{5b845034-25ed-4b92-bfac-ed7b305a3e13\\})|(\\{24c45b33-30e2-4d41-adb1-2a1bd9942ca6\\})|(\\{46f426fa-dc37-4b1f-ae63-11370ec65b39\\})|(\\{d678f055-e538-4c58-8a61-746166ac5063\\})|(\\{f852266a-7f88-47d8-b610-6ca130d9774c\\})|(\\{dce62517-e86a-40e0-9361-64c2e61f011a\\})|(\\{f7883a1d-0f04-4f8b-85ec-f339b791335a\\})|(\\{204f251f-feba-487a-9bbc-ca8b22b222af\\})|(\\{59c8031e-40de-485b-9988-69a4f4f51e52\\})|(\\{a5207f0f-109a-406f-8d86-3a2c806c9c7b\\})|(\\{3b66b98a-8782-4f16-a2f6-33175f9b0101\\}))$/", + "prefs": [], + "schema": 1581076280856, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613891", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0e546e4e-6d89-41a0-92fb-987b98efea6f", + "last_modified": 1581082030072 + }, + { + "guid": "/^((\\{0970b6b2-6702-443d-84fa-90f5b5cf7c10\\})|(\\{9ba7732f-7e77-4dae-9885-a9cb91930b0b\\})|(\\{dd43f6d2-30c7-43df-bce6-6edc46c84d9d\\})|(\\{72207416-61e9-4960-9cb2-87d2df8486fa\\})|(\\{d53274cb-43e2-4cbc-b1f4-5463cc026ab0\\})|(\\{e1faa92b-b124-463e-b247-2609d534733c\\})|(\\{654068ac-aa86-4f05-aab3-f9dbea380021\\})|(\\{58d9fc43-39c7-41be-96a6-a27b1e179191\\})|(\\{f5469e0b-d4cc-4c30-9922-f0c82f1e04e2\\})|(\\{2ebc9fc9-8642-4f97-935a-6885e66ed6db\\})|(\\{b7d87fb6-afc7-4544-b798-7fcc1c8114f0\\})|(\\{d36916ad-9b5b-4390-b302-321c43d85753\\})|(\\{1e67d5ab-aff2-4540-a2e7-cd19ee112ab7\\})|(\\{82f9d6fb-cbbb-4862-8c18-c0876aa00c3a\\})|(\\{2a78a205-7363-4d76-9eea-a862be445724\\})|(\\{bb65aeb0-db01-4f8b-893a-634d2977269f\\})|(\\{5cac0db7-8b8d-44e1-8932-687b152feb8b\\})|(\\{6cade252-0973-49b2-acda-36960804c0f3\\})|(\\{61e466b4-00e9-4ed2-94bc-dbfade21f066\\})|(\\{316b549c-841f-4f2e-9e91-5a8cf5c22808\\})|(\\{73571d19-5073-4b32-915a-a2350e814cf1\\})|(\\{2e5d7ab6-cdea-4a23-8bb5-3245ccec2c5f\\})|(\\{fbf35eb0-14de-483f-90aa-3f8c4ea773f0\\})|(\\{4a9619bf-edd4-4f6c-a787-c919cd4b1d17\\})|(\\{167fc8e3-1bcf-4f46-a8f1-1722db81c4a0\\})|(\\{b861c901-a92c-475a-9efd-805d01a1c6bf\\})|(\\{6800f73d-3133-4697-a368-16c1f6e63894\\}))$/", + "prefs": [], + "schema": 1581076281891, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613891", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1dbaa501-a6b0-439b-9871-79f081f4afdd", + "last_modified": 1581082030069 + }, + { + "guid": "/^((Search_Secure_clone_oRrmYYtiAX@www\\.searchsecurepro\\.co)|(Search_Secure_clone_rHXTjLgNGR@www\\.searchsecureprime\\.co)|(Search_Safe_pHUandPyRQ@www\\.searchsafe\\.site)|(SearchSafe_IEHxXeEbnC@www\\.searchsafe\\.site)|(Search_Secure_ByKsqSMauFtest@www\\.testsearchsecurepro\\.co)|(Search_Secure_GyQyTTzqnYtest@www\\.searchsecuretest\\.co)|(Search_Secure_NhrEwtJEhM@www\\.searchsecurenow\\.com)|(Search_Cipher_bBzgbIQyiC@www\\.searchcipher\\.co)|(Protect_My_Search_Online_clone_jtypmmXRwz@www\\.protectmysearchonline\\.com)|(MyAstroFinder_LTEnkWRkmt@www\\.myastrofinder\\.co)|(Live_Weather_Check_PZjCbwpxyH@www\\.liveweathercheck\\.com)|(Travel_Deals_Center_clone_GvEXeOfSMc@www\\.traveldealscenter\\.co)|(Dictionary_Pro_ARbsoVmMdl@www\\.dictionarypro\\.co))$/", + "prefs": [], + "schema": 1580931692537, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613657", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e480bd4d-7ff0-425c-9c1d-4153fa55ea81", + "last_modified": 1581005656617 + }, + { + "guid": "/^((\\{0fc22c4c-93ed-48ea-ad12-dc8039cf3795\\})|(\\{0A2C2098-F04D-11E5-A933-5334BC8E7F8B\\})|(\\{8504399b-e635-40fe-8943-977a58521db3\\})|(\\{347d4451-8da5-4d67-96b2-a2e8a6de8e09\\})|(\\{176c8b66-7fc3-4af5-a86b-d0207c456b14\\})|(\\{8692b95e-1a13-4118-b5b9-be8f3d2fc9b7\\})|(\\{b3e1e418-986b-4231-8579-0fc754574d9c\\})|(\\{88619e16-f0f3-4606-837f-a4496d11c0b4\\})|(\\{b4229471-001b-4960-bea9-795d91cb943e\\})|(\\{16390554-6fc5-4dfe-b7bb-809e378df660\\})|(\\{5fa01132-d07b-40e1-b958-e7825b878422\\})|(\\{07ebc943-2c8a-44a9-8d4d-5bbb78b2d2e5\\})|(\\{02405c0b-a202-483f-ba02-b09bab55cebd\\})|(\\{094f9b39-0561-4cb4-8b51-cb8ee5bd5b90\\})|(\\{a66e7db5-fc74-42c8-8e8c-d7a401a577d2\\})|(\\{58f9a1bb-0635-4b79-bb41-166c3e810329\\})|(\\{9da80afe-bcd4-4271-8f70-f986370d954c\\})|(\\{2671041f-f659-43b1-a400-0f7a3b852f74\\})|(\\{d9c98668-f0a1-43d7-800d-0c6d11321663\\})|(\\{1d6f99a8-d100-446e-8cc5-85231e7ab7fa\\})|(\\{ae170991-a8c8-4caf-b6cc-a3cc994abe83\\})|(\\{bfdd7357-3692-4d5b-86c5-4d86cd5e39e3\\})|(\\{aa5e865e-1e21-4ed9-b80a-f374be86b5cc\\})|(\\{26c58d0d-8514-42d5-87fd-701fd53ce3b8\\})|(\\{c934c41a-5de5-4086-b2da-1afc7d744162\\})|(\\{e87de7b9-5994-4b91-9c1a-a1d4d12a3969\\})|(\\{0eeab47a-73f3-48e7-977f-08815b4ee5ad\\})|(\\{1df850c2-38cb-46f1-87a5-308af6409c14\\})|(\\{1f481c59-fe50-4148-83d1-ff551f6dddf9\\})|(\\{70cfab72-ee99-428a-b5fb-26d924be3acb\\})|(\\{b156eeb6-cf37-48d8-b15b-bb863c431ba1\\})|(\\{a81f7ce6-0cc3-41fe-a33a-eb856636c887\\})|(\\{3949b4a3-bbb6-4119-9fa0-249fdff22c45\\})|(\\{abe2755c-a3b3-4714-a354-51eb5b8129fe\\})|(\\{547f048f-4fbd-40ef-9365-3d54559eae61\\})|(\\{e3e293e3-f18a-42f1-98bf-71d8166aef54\\})|(\\{ca742d81-6e6d-473e-ab68-f757d480e159\\})|(\\{6ac89db3-5ee1-43d8-a12b-a1b6e0ceafe0\\})|(\\{4eac966b-28db-477f-a471-a3bf74621110\\})|(\\{2c413992-ba94-4917-bd57-57eef39b4f8a\\})|(\\{6935560b-a856-42be-baa8-a06459785ed7\\})|(\\{503d9872-3db8-4f05-9c9c-b8bcd6d08ae3\\})|(\\{2055799b-b1ed-44ab-9200-4190467a3c59\\})|(\\{fe27a89e-a1b6-408e-b0b3-b2ccc0cbdf3c\\})|(\\{3fab2a05-1bfa-458d-93f8-16c523d72804\\})|(\\{32939492-d835-4540-9c03-1af0b715268d\\})|(\\{e9c96255-4eed-49fa-9740-54ef684b8197\\})|(\\{45fc39ef-f2c8-449a-b533-77e4d6202777\\})|(\\{d3ce94d3-a9ee-49e2-9290-031e56a4a5d0\\})|(\\{6b8a371e-ce42-4355-b4bc-ad4c83d5f932\\})|(\\{cfacd4cc-9f18-40f2-8711-795e738d51be\\})|(\\{d41f259b-c827-46d9-891a-33b6caf2370c\\})|(\\{3e90457a-815b-4b0a-812d-f02a43c23951\\})|(\\{063de0bf-2da4-48c6-bed2-ed11ecb3bfb8\\})|(\\{cd253156-e140-433a-a0fc-ccab28bf069b\\})|(\\{9a6f884b-3b8a-4925-8f7e-a975e2f6ec20\\})|(\\{5986d98c-b75c-46d8-9c5a-0fef03cddd5b\\})|(\\{8d586a4b-f00f-4a5b-940b-2fe00dc2905c\\})|(\\{ab0f5841-11f0-4c92-9bf8-b885f4431253\\})|(\\{eb1bafab-7f4d-43a6-8f78-fb0dbb099cff\\})|(\\{e6b152f0-5457-44ab-a6cc-d7869dc694a4\\})|(\\{942fe5ef-e9a6-4791-b840-a2e74baaeb4a\\})|(\\{d1c67270-c2af-47af-a4bc-2c020df200c5\\})|(\\{8a934de2-3238-4c24-aa29-52e1fafc64d4\\})|(\\{c65f3bbe-684e-43d6-b030-615118b38e54\\})|(\\{286f65d2-6b01-4f04-871e-2cef4095065f\\})|(\\{2FF4B97E-A47A-11E7-B621-7403A54193D8\\})|(\\{da482f08-4b6f-4c59-942a-75e1fedc8c6b\\})|(\\{3a961d08-8ca9-45af-9c30-2ba4d673e10f\\})|(\\{e87de7b9-59a4-4b91-9c1a-a1d4d12a3969\\})|(\\{942fe5ef-e9a6-4a91-b840-a2e74baaeb4a\\})|(\\{b15a65af-8dbe-46ac-9537-f91fb1640809\\})|(\\{da48af08-4b6f-4c59-942a-75e1fedc8c6b\\})|(\\{ce2c3653-8d63-4cd0-ad8a-31f03703820a\\})|(\\{5d987ae9-c201-4352-a219-a34ee28a6f9e\\})|(\\{b15ac5af-8dbe-46ac-9537-f91fb1612309\\})|(\\{3fa556a3-3bfd-4e4b-b403-072938701c66\\})|(\\{57baae18-26fa-4ec0-9fe8-a0e197c1a220\\})|(\\{4de7b432-e5e2-45d3-95cc-df3e913b68d4\\})|(\\{a15a65af-89be-4fac-9127-f91fb16c0809\\})|(\\{c2c8b504-f5bb-489e-858f-38ca5224d033\\})|(\\{7f458bd6-4841-4373-97a4-a3d140b02552\\})|(\\{e20bc238-0831-4d4b-9386-d2fa5abe804a\\})|(\\{e51fa7b7-4c3f-4f66-8bc7-e864a413b790\\})|(\\{f9c04a94-59d8-47f7-a97d-980ff99f81cf\\})|(\\{d6ea5103-78ff-4736-b2e0-c0ce94bee77e\\})|(\\{f8fd0172-1b1a-4084-b440-4119996ab2c9\\})|(\\{a52e4eea-9923-443b-98a6-942eb27ba324\\})|(\\{a1fcb660-c2cd-49b7-a327-88e887afb43d\\})|(\\{cea42340-da28-4c95-89c4-0a66d69050d4\\})|(\\{c2f47878-f9d3-4c89-be58-a69dd9d1484a\\})|(\\{7ddf85d3-7014-4fdb-836a-fbbe4385347f\\})|(\\{36f452d0-6154-4be8-a388-174fc98d9333\\})|(\\{c69d7a2e-ad00-4e9f-80d7-fa0604ac3954\\})|(\\{8d40db57-9287-473d-b398-259d702d92f1\\})|(\\{bf51b8cc-a07d-475a-9013-91e2c089ae60\\})|(\\{32030663-2a21-4d20-a2e6-ed3d4a51b704\\})|(\\{1204abf0-0409-4934-ab58-1f5424134bc1\\}))$/", + "prefs": [], + "schema": 1580758890801, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613073", + "why": "This add-on violates Mozilla's add-on policies by redirecting searches or collecting search terms without user disclosure or consent or other privacy violations", + "name": "Add-ons violating Mozilla's policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2c6f59bf-90a6-4fdb-a12d-f1b4a1446331", + "last_modified": 1580842055124 + }, + { + "guid": "/^((\\{a88e625f-b840-43b2-9feb-99c259b6751e\\})|(\\{0cc6c564-63c6-481d-9dbb-ddfd40ced202\\})|(\\{329e1c15-fcee-4063-83bc-59c894861cbc\\})|(\\{98e2e7ef-e544-4426-a182-621e8b85c15d\\})|(\\{d8869a56-bccc-4011-9b86-2962a57d7377\\})|(\\{255138e4-d9f7-4779-8713-e5970090ab67\\})|(\\{ee5907df-603f-4684-8f2a-425742ed37cb\\})|(\\{0e725b0e-9f26-4bb5-b9b1-738df2692e19\\})|(\\{2bf69cca-c4cc-4200-ab56-1d27fef48e03\\})|(\\{4edc1ea7-7037-4b54-8a64-a43339545710\\})|(\\{6edff7c6-97a8-4355-a074-e191b0a9a4ff\\})|(\\{79c5d05b-a6ba-470b-b06d-d33fe04418e5\\})|(\\{a1ebe305-b540-4dc5-91a2-8a2a0d646efa\\})|(\\{1211b4db-1ed4-4630-82e6-cdf69d58f035\\})|(\\{4d4e8ee2-78aa-42f5-a30a-c3727241cb96\\})|(\\{b6178891-7400-4b86-8a96-d57b1b7eee20\\})|(\\{3cfcb0bb-f7fc-440b-bae9-1050a966013b\\})|(\\{747931c3-bf5f-49d0-a4d2-615847200653\\})|(\\{989fcf36-da63-4d83-a036-b996788e7a4c\\})|(\\{a0dc99d3-7b9c-4fa2-8d42-39179b1e1e5a\\})|(\\{d4ce8b88-6b17-4ed8-b695-29da839dfb41\\})|(\\{cb2af2e8-ca55-4545-864f-96686760b59e\\})|(\\{3961b434-016c-4598-9ab2-38425197f32e\\})|(\\{b4e25be7-a4a7-408b-8759-8b185b4421f7\\})|(\\{0cb56fc3-e3f8-4ea3-aba7-4a2d351f82be\\})|(\\{b3e9621c-3194-4cf8-8b28-4ad32a988d5a\\})|(\\{241b64a7-7b2e-475b-b195-b1cfeb099745\\})|(\\{b727f9fe-9345-4ce4-8797-f65f85dfad7d\\})|(\\{a90d260f-7c39-4539-9948-8f3ee4406a9e\\})|(\\{35a57c90-ce66-42f6-ad0f-52b92690f4c2\\})|(\\{83048c90-baca-477c-8774-054f80eca4e6\\})|(\\{91c1e004-2e1a-4103-89c2-2f585c2d306d\\})|(\\{a5701591-3a6d-43b1-b74b-c47a1b7aa5b7\\})|(\\{641eef82-a96a-4702-b3fb-f50f8d7cdc85\\})|(\\{b436362d-94e9-4209-a3e5-26dc7d1ec86a\\})|(\\{70dc19f1-1312-402f-a370-cd8a1e231116\\})|(\\{c9a68220-ad02-4b09-9456-12ed03121344\\})|(\\{dc0d8992-0c9e-4473-b8c4-72240fc7be39\\})|(\\{1c3ca6ca-1427-4b6e-85be-315e18f81135\\})|(\\{96309a2c-339e-4c3e-84ff-ef7dac131a18\\})|(\\{95d07270-fb53-40c3-9b9f-46ed78658a1a\\})|(\\{15700ca0-8d4b-40d3-99aa-bff59aa48676\\})|(\\{5d308759-368c-4b5a-804c-acb6e18c6436\\})|(\\{b47ace66-3443-4d26-a858-7bbcfe1c18b6\\})|(\\{10dc172a-38b3-441b-a2d4-52b4ce8e4b7b\\})|(\\{73a47b20-be2c-43e4-a728-c46d33612ccb\\})|(\\{5f3f1314-618c-4c3d-ac4d-f83ae6d247ac\\})|(\\{bb81241e-b093-4c55-8c3e-7dca2250ada6\\})|(\\{4d8be4aa-4790-4807-add9-af62132e675d\\})|(\\{4295e30f-80bc-4d48-b794-c609de6f2dc0\\})|(\\{057c6b57-46f3-43b4-9576-438afdd3b3ca\\})|(\\{60a58bf1-08ae-46d7-9010-5cfe8eb5f282\\})|(\\{bffd69a1-5a8a-4b0d-ae14-f8744adb92b2\\})|(\\{b5c0b80e-dbc6-4701-98c2-3b0b8e182404\\})|(\\{48306d77-a699-4cbe-9ed3-b3162dfff00b\\})|(\\{f1f285dd-4a5c-48ad-81bd-78fe204ed582\\})|(\\{da0b9335-8edc-4429-8889-c5872ad02417\\})|(\\{6e834570-8580-44c2-91b2-d30f687aeb07\\})|(\\{9ea1bae9-5dca-4a1f-9fcf-a325a39b85c1\\})|(\\{69149c7f-92c7-4d41-b88f-e68c237d1d63\\})|(\\{985e1bca-d152-4fc3-89a0-cba2c0ac44bb\\})|(\\{746c7167-56bf-48b1-bd6c-08b05b48a863\\})|(\\{4bc87632-de6e-4c23-8192-f561f185a823\\})|(\\{1d21ed2a-94c9-4bae-983e-5c1e5094060a\\})|(\\{148df2dd-451b-42eb-952b-f608f26cfb6f\\})|(\\{2c6d6dea-438c-442a-ae11-a943804d90b4\\})|(\\{2c7e143c-9f4d-412b-b552-17033d4992c6\\})|(\\{e2309514-8386-413c-856e-21b54ebc3d9c\\})|(\\{4b417a17-ff90-4f16-bb8b-fb1e0d9ca824\\})|(\\{7c68360c-d03c-4c26-bbfa-2f9c9064701f\\})|(\\{eefb2906-cb27-4801-9ae8-67b49807b151\\})|(\\{b7c790c9-aaf6-46f2-9462-812d3e129ce7\\})|(\\{aac68138-e60d-48e2-92dc-d28577e553d9\\})|(\\{5959fff0-d04a-4147-8d4d-aaa7bb314a00\\})|(\\{40bf9b7a-9c36-40a8-8e68-91b79eb3bd44\\})|(\\{96429801-73cc-403b-be68-fb8a992f9307\\})|(\\{218e967f-59b5-41f6-a22f-7fe3c1580956\\})|(\\{ba51f209-a2f6-42a1-9608-536058540d0c\\})|(\\{d993e7df-00f9-445f-9082-294017eeec36\\})|(\\{f1990255-00e8-4d34-91d5-11d09066e4f3\\})|(\\{dbb78ed6-8449-475f-a152-0148596539eb\\})|(\\{ac798a99-00f0-421c-a3c9-a13bea8ef728\\})|(\\{e486268f-9299-422e-91df-9706ea220c46\\})|(\\{ecf1e24d-cfeb-4fa1-9953-202c3e62a820\\})|(\\{caf7fac5-0784-4701-a50d-2d0c27074b9a\\})|(\\{8f85f7d7-7182-4a53-a18a-bf83f0bfd1b4\\})|(\\{be556a9a-df8f-45b9-87aa-ae0eaf50a2b4\\})|(\\{9f80406a-ba8a-4548-bb0e-e987fb1d7921\\})|(\\{d0386a6e-e364-4e67-b340-782492bdb3ee\\})|(\\{cb0d2b3a-929c-42f1-8874-07a0650d0298\\})|(\\{cd9f2f81-3674-4d3e-a1b3-824fd3e5e906\\})|(\\{58366e2d-3a39-4fea-b631-521e3e73162b\\})|(\\{d854ae81-f759-48a9-8732-2371664fe2fe\\})|(\\{4747285e-3ec6-4d8f-9d1d-d0430297f182\\})|(\\{2f1919a8-4ca8-4a80-9175-c2da3a98490b\\})|(\\{32b30902-43a3-4f37-8e86-860ea525e923\\})|(\\{60376450-fe48-4d30-b551-b35535df6e96\\})|(\\{22456782-c5e4-437f-9369-5b23b875cd6a\\}))$/", + "prefs": [], + "schema": 1580814638830, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613073", + "why": "This add-on violates Mozilla's add-on policies by redirecting searches or collecting search terms without user disclosure or consent or other privacy violations", + "name": "Add-ons violating Mozilla's policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5b3ee1de-0c37-4883-a22f-822c8d6f8991", + "last_modified": 1580842055121 + }, + { + "guid": "/^((\\{35417623-1a2c-4e75-846f-e2a7b95b3b24\\})|(\\{d6dfebf0-51d4-490b-b03d-a17bf7fe4a6d\\})|(\\{50c7fb39-c03e-4d82-a071-fac7eaf960b7\\})|(\\{28999f9d-6e83-4c52-bb2b-5d2c46b7ef58\\})|(\\{9409fdd1-d78d-4df8-bb5e-f178f42702f6\\})|(\\{4c356115-33da-49e5-877b-090c64d76a66\\})|(\\{cf674a88-861f-4357-aea7-37f09e534552\\})|(\\{43238f2d-ddba-4604-803f-889bfaa22ebf\\})|(\\{36405669-81af-44c0-83cc-32ae8f901db0\\})|(\\{2ca5cfff-58dc-47d4-b56c-c206d281a9cf\\})|(\\{b22df1a7-f312-41a8-8fd7-cdd160ba7be2\\})|(\\{cf019c95-1c49-438a-9398-3863642fb606\\})|(\\{c7a9d852-c30d-4e6d-b040-a445551bee61\\})|(\\{379932ed-2938-4ccb-b57e-2dc44c3b5121\\})|(\\{d2395ba4-96e4-4ffb-a4da-608f27328c9f\\})|(\\{7ffbda76-3f9f-403b-a5e1-559c4f18c1f3\\})|(\\{361db730-4c11-49e8-b189-fa461f738786\\})|(\\{677456f1-2716-4a23-9a74-f3867e860dbf\\})|(\\{46c19d03-16ce-45f2-82ce-d8ccaeb3ceea\\})|(\\{6339b6ff-da02-4821-b5a0-48490a54b14e\\})|(\\{7d4d815d-798c-4cc0-9561-2f84b4598f94\\})|(\\{40fc3311-9612-4cb7-b337-fd581dead5f2\\})|(\\{9e639048-d366-4db0-a15a-f1953130fc8e\\})|(\\{ce2d009a-47af-4e94-94aa-23bd8973addf\\})|(\\{41984c46-3ec2-4684-99db-a5c4356f7ba0\\})|(\\{a63f9261-e05c-45f8-8b43-f987eaf5b27e\\})|(\\{37e06cae-3367-43e2-bdb2-0c087f2603c6\\})|(\\{f9d80e53-acb1-4483-bf3c-f58920551ded\\})|(\\{1672a22a-f6cc-4318-9616-22e1e3b3049d\\})|(\\{1cc6cbe4-38ad-44c4-a462-a2a222bab5be\\})|(\\{0255dcf8-ac71-4403-ab5a-c8f7abf66a39\\})|(\\{564f7511-ba68-48a0-9a85-636a100ce3d1\\})|(\\{4ebf29d7-102f-497c-bc8d-696dd66ecde6\\})|(\\{4c4139ca-ffbc-4f10-a534-22dd4c787944\\})|(\\{a12c1827-a5fa-4c11-8403-e02e9520f0ec\\})|(\\{2178817b-1d32-4068-9675-8fffe11a88f8\\})|(\\{b779151f-7a4c-46b2-83e4-3dbad231a9b9\\})|(\\{fe8a8186-80ef-4112-a41f-7d00ffceb63e\\})|(\\{0ee72846-9a34-422e-b202-382aa19bc0bb\\})|(\\{c92d2fba-d002-426d-acc4-0891fa40040f\\})|(\\{e3bdfd08-fa29-4a56-9851-f6d0b965004f\\})|(\\{0017666d-27b7-4347-bbbf-6d389b4430dc\\})|(\\{c9db55d0-61e4-4515-a9a7-24e3783bf106\\})|(\\{fbf87239-ec23-4907-bb6c-93daa5015ea3\\})|(\\{44801e5e-a635-455e-b08f-f3da88b06bed\\})|(\\{59517a31-ea51-44cd-ac7c-02d952ecf04e\\})|(\\{ee075bcc-2101-4c9e-8f04-7e7281ca5c74\\})|(\\{fd073c3a-d7d8-43f6-9d1c-1865bc0ff940\\})|(\\{a15f4275-8f19-4508-b548-a88597092bbd\\})|(\\{568ad6c6-0b91-46bd-9093-bc394a84c257\\})|(\\{11f798d4-003b-41a8-aa65-1f566ff53f06\\})|(\\{a02f26f3-49f7-4e95-a69f-78b1ce2d3471\\})|(\\{ed48226c-94b9-4878-86ba-de38851290d7\\})|(\\{97e5276c-a627-4c62-8659-4772a81203c0\\})|(\\{4cb19fea-1568-4f0b-809a-0763fbb36888\\})|(\\{599cff19-2ba7-4bf3-93a9-87f7f277cadc\\})|(\\{f0564e7e-f154-4612-b50e-e0d11c47b359\\})|(\\{3cc8efd6-4808-428a-9e0c-ef6fa45d17b4\\})|(\\{3842feba-bb44-49f6-9511-de1a1b78d348\\})|(\\{ebc3de57-de6d-43ee-b76b-676893dd7035\\})|(\\{a5f552f7-7279-49e5-92cc-70f952534726\\})|(\\{3ece0c93-9723-4280-8356-d1eb2025e2d5\\})|(\\{8579047e-a427-4ac8-87b9-ee9651c1f856\\})|(\\{c9bfdfd9-4617-4749-afd1-265f0b7158cf\\})|(\\{0c570e22-54e7-4d93-86f2-36e19707018a\\})|(\\{cfc0fa6e-0d6a-4fcb-9dc2-58b220307293\\})|(\\{8640eaec-8d51-4d6c-b01b-a671f4aac012\\})|(\\{a0ca05be-c6f2-4c18-993f-de55abca7000\\})|(\\{de066e14-6527-4be4-9751-e53ea16fd60f\\})|(\\{5a8f8da1-3994-43da-98be-3a58ed4d2ec6\\})|(\\{bcd8524a-8f39-4eba-a795-aabdd95305b4\\})|(\\{7d76001d-fda5-4abf-93f6-a947dd3cca24\\})|(\\{d18219fc-b632-47dc-bea1-73451164d187\\})|(\\{e97fbca9-d513-4fa7-9524-576620470399\\})|(\\{069b10e7-f7ef-40df-87bb-95783401a54a\\})|(\\{12a978dd-a7b9-42c2-b431-65aaa56c2a77\\})|(\\{66ba2e20-9378-493b-837f-4b3a028ab5d9\\})|(\\{9ad5b1e9-a19c-4a65-be1b-8b888da7cf58\\})|(\\{87ce9b48-e659-4b14-ab54-c80ffbeaa77f\\})|(\\{4da94d09-4730-4277-a544-f3b7890b6666\\})|(\\{30e6c020-4ec6-4b18-8f29-3dc294fbbb44\\})|(\\{5bfbdd70-ce54-4686-9351-0d90a7dd011f\\})|(\\{17eb23ec-f059-4857-a405-9d06242e99b5\\})|(\\{b4170b6a-0af2-45b1-9214-3665e870ab3a\\})|(\\{bc20b5b7-46c3-4af6-942f-f0323445b576\\})|(\\{44edc268-9242-461f-9d8a-fb59337101b0\\})|(\\{301321f7-e0a2-4cc2-9cc6-7ee1390ea895\\})|(\\{b57a503c-6370-4ed1-90ed-1b6444ca1e52\\})|(\\{c4707969-efc7-46ce-845d-28a46c78fda6\\})|(\\{22714afe-0f06-496d-9897-31e52e83a12e\\})|(\\{a07fb8cb-91fa-4799-9ff5-115e8a88b57f\\})|(\\{f539ec45-16dd-47a3-a9d5-8643b021d4c9\\})|(\\{b9348282-9380-4e55-9939-e4c10254a496\\}))$/", + "prefs": [], + "schema": 1580814639945, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1613073", + "why": "This add-on violates Mozilla's add-on policies by redirecting searches or collecting search terms without user disclosure or consent or other privacy violations", + "name": "Add-ons violating Mozilla's policies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ae074a33-05f8-4721-a9ff-7111502291c2", + "last_modified": 1580842055117 + }, + { + "guid": "antimalware@titansurfer.com", + "prefs": [], + "schema": 1580240491748, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611797", + "why": "This add-on contains deceptive code that is not in line with our data collection policies.", + "name": "Titan Surfer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c703b7fd-4b40-44f8-9de6-786b66ef8e1b", + "last_modified": 1580742890204 + }, + { + "guid": "app@OnlineFilesConverter", + "prefs": [], + "schema": 1580380822975, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1612169", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Online Files Converter" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "bcb1821a-25e6-4b9b-a54c-bf4543f895c0", + "last_modified": 1580742890201 + }, + { + "guid": "{f83128d7-ef15-47a2-a99a-70d181413b81}", + "prefs": [], + "schema": 1580381170095, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1612294", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake banking add-on" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1ca7b6ce-30d5-4290-b46f-9fad59b65f9c", + "last_modified": 1580742890197 + }, + { + "guid": "{d281b854-0c99-4e4b-b647-32038ae53c27}", + "prefs": [], + "schema": 1580727235989, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1612868", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "SApp+ (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "93cceac3-803b-412a-b413-fdb1b0689626", + "last_modified": 1580742890194 + }, + { + "guid": "/^((\\{236a5a66-132c-4d7b-a62f-66f1a76bb7b7\\})|(\\{0fadbf07-bb25-4737-9800-b879a6f1c417\\})|(\\{c8ec696d-935c-45d0-a604-180244e839e3\\}))$/", + "prefs": [], + "schema": 1580740785659, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1612869", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "183a1de1-2625-44d6-90df-cbdbee9d23bd", + "last_modified": 1580742890191 + }, + { + "guid": "/^((mozilla_cc4@internetdownloadmanager\\.com)|(\\{4509d977-32a4-480a-ab95-6ddb5bfc6616\\}))$/", + "prefs": [], + "schema": 1580741006002, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1612871", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ad2f927a-eb67-4360-92e0-87f567608c1a", + "last_modified": 1580742890188 + }, + { + "guid": "/^((\\{f2431eaf-2b0d-4f0b-8148-3188db294d73\\})|(\\{c43e5363-2c61-4c3d-afa4-9cbe06e767d0\\})|(\\{bb8eab9f-4611-496b-bd31-a1b2ee66d8f9\\})|(\\{5f61d055-ae45-4ecd-9570-555609f66f5a\\})|(\\{532d8a77-86b6-4a7a-87a4-d973dc0cf9d0\\})|(\\{bfd10065-670d-4477-98dd-8bb4285040d5\\})|(\\{79f33a52-631a-406b-afcf-9be8b4bb480a\\})|(\\{fd994367-ecfe-44dc-a595-cb155110492d\\})|(\\{9a01416a-758c-4fd7-8e56-998acc588f9b\\})|(\\{517a72f3-e9cb-4e9f-8a2a-9639f4daed76\\})|(\\{4fda7f17-eaa0-44d1-91e5-7d8305a2de0e\\})|(\\{695bd646-ba09-4a3d-9616-191b355aec33\\})|(\\{6893ed50-b006-430c-bbfe-5f049e57470b\\})|(\\{7a2f84c6-d1ef-4902-bae7-b6c7807cb32f\\})|(\\{afde1624-c4a4-4494-9ed5-1ad15799dbd4\\})|(\\{e05b53dc-910d-47ce-9c9d-5195e8e8f3e5\\})|(\\{11b0ec0d-ae49-4ce8-a13c-198affdb4d9e\\}))$/", + "prefs": [], + "schema": 1580741081005, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1612872", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "dd36267c-dbd3-4df1-bc01-5f566ba56de0", + "last_modified": 1580742890185 + }, + { + "guid": "/^((\\{6b7df170-e6fa-4b9b-bd76-d6b866a5bc6c\\})|(\\{7e1c1f98-4c89-4668-b9ea-e258e7c9e22b\\})|(\\{1f2128cd-bcaa-4e60-b555-0713054df0f9\\})|(\\{9043971c-ce88-400b-b56f-7a9e3853eb32\\})|(\\{afcbdf71-be6d-46cf-b5ff-6a6b9ec7f920\\}))$/", + "prefs": [], + "schema": 1580067691094, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611725", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5b4a4d34-1341-49ea-8b53-96a1ecfac49e", + "last_modified": 1580207330647 + }, + { + "guid": "/^((\\{0ded4ebe-965f-4de0-89d2-91ed13ae15ee\\})|(\\{5c143da2-6a9e-4afe-9ca2-b758aebe6e64\\})|(\\{c6566b12-297b-41fb-8189-f32a7d1c1b87\\}))$/", + "prefs": [], + "schema": 1580078568605, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611806", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "70af2927-6552-4eae-a91a-89e4cdd61b88", + "last_modified": 1580207330645 + }, + { + "guid": "/^((\\{9237d0ab-aaf0-41a2-b873-c0f131b09ce4\\})|(\\{18b14b7d-e228-416a-bab8-37acf6d6dfca\\})|(rwkaddon@racewarkingdoms\\.com)|(\\{2e2d6d72-2634-496c-a8db-db869b639a21\\}))$/", + "prefs": [], + "schema": 1580137101921, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611807", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons injecting remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "303a149e-8262-4d16-b24b-348b84d5051e", + "last_modified": 1580207330642 + }, + { + "guid": "{c6ce41ee-a4e3-4fd7-ab6a-988b6916d66a}", + "prefs": [], + "schema": 1580137557625, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611810", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Qwicky Advertisements (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "18581952-6448-4728-9fd4-2860b3e6534e", + "last_modified": 1580207330639 + }, + { + "guid": "djuvt@czgnp", + "prefs": [], + "schema": 1579817517633, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611297", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Update De (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ad789bcb-4c12-401d-b6fe-202a5b6697b6", + "last_modified": 1579987441268 + }, + { + "guid": "/^((zdphb@swlguy)|(czgnp@hhjl)|(mjrxg@zdphb)|(fuevm@czkvq)|(bgufa@djuvt)|(czkvq@bgufa)|(axvij@fuevm)|(swlguy@axvij))$/", + "prefs": [], + "schema": 1579863493098, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611403", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "83d7f7c0-8a56-4feb-9e66-b77af6343430", + "last_modified": 1579987441264 + }, + { + "guid": "/^((\\{50fc0c97-b405-4f63-9c10-465cab131ec7\\})|(\\{e08f09a1-e8e4-421c-a460-ada7b13077c1\\})|(\\{887c8e31-c11d-4b67-8bbc-e5b5e8fea9dc\\})|(\\{d67b6e83-976d-458f-aece-0af986c16db7\\})|(\\{d0c0679f-9fa1-448c-a78b-f8fa1591597b\\})|(\\{23747ea4-fb31-4349-9954-2728f1d4bda2\\})|(\\{5d492e1a-7e3a-48ca-9745-784a5b61980a\\})|(\\{bec8dea7-eed3-464a-b99e-92cab1e10373\\})|(\\{48d63231-1796-4b26-a3f7-1aa7f5bed1f4\\})|(\\{fdc94eff-5212-416e-b2e4-d67c088c4907\\})|(\\{ffd2c868-9f15-48f1-a77b-c5a1842a2e41\\})|(\\{9b0e5b96-2b47-4ee4-b511-7402c05ab43a\\})|(\\{f01f803c-fb6a-4c0f-9dfa-d8f6173b3b17\\})|(\\{b2ed387a-02c8-4bff-bf6f-00ce6b5f067f\\})|(\\{7222c5ee-8154-4e47-8521-1dafcd00d902\\})|(\\{c085ea8c-57cf-41c5-a41e-38dd6288e808\\})|(\\{2cc9608c-dc69-4bc0-8d3b-95852face3ac\\})|(\\{5977f159-e17c-48d4-8e2a-8b48962a57cd\\})|(\\{10e4d201-689d-4864-a04e-f21186f3d4e2\\})|(\\{79119982-95ef-4cfd-9fe2-b193018503ee\\})|(\\{520dd821-08e0-4821-abf8-347474c78f72\\})|(\\{6ff93655-17b6-4bab-bb0e-40abfcd5a853\\})|(\\{af670583-4dde-4e1f-b169-efc5aec481f0\\})|(\\{b053d2ab-0c28-41a6-99c4-4d276af55169\\})|(\\{4b70886d-6215-492d-8330-c220b714a216\\})|(\\{21855045-7800-4467-923b-096efe6ded40\\})|(\\{6c1f8c3a-47dc-480f-8007-3204db00a8c2\\})|(\\{2dc676a9-bc76-405c-9252-b76c60cc172d\\})|(\\{d464e0a4-8798-4356-8cb1-b1b819d80d2d\\})|(\\{b4cc995f-aca5-43dc-bde2-dc5b5de620ba\\})|(\\{448b1043-fc5e-471a-8a10-5ec74fb16054\\})|(\\{becb255d-985e-4f59-9e7b-a3f678bb53ba\\})|(\\{f200084c-a7cc-46a1-a84e-289a8c033124\\})|(\\{c6b5b880-ae5e-4cad-8eaf-fce059892c0b\\})|(\\{0ea7199a-707d-457c-95d3-cc84436c5634\\})|(\\{dc702775-072a-42c4-8a7f-0e02b202a48f\\})|(\\{4a1c6922-5b77-4f07-84f7-47e504ec5249\\})|(\\{145fa75f-ba31-4f91-8664-c2559887a664\\})|(\\{b9d3f331-5a3c-494e-a0dd-5b7dc1b949b3\\})|(\\{f7801e2a-e119-434d-83c5-e87eedffecf2\\})|(\\{1b88498d-c70e-41ab-98ef-4239582e77d6\\})|(\\{b0162d70-142b-43f2-8414-30414a2b1ea0\\})|(\\{1f7166b4-c765-4129-b727-ef077814e0af\\})|(\\{ee2cd2a1-b4f8-4ef1-9fe8-db89e8844e47\\})|(\\{569c8641-f0f7-4eda-afd2-4ca6f6fc8bc9\\})|(\\{85c5a731-0b8b-4d91-bbad-07790b7a0165\\})|(\\{02197da2-c4b2-4a55-b323-f56aba8d3ed2\\})|(\\{d5250fb9-6db6-4eb3-bfe7-b8d3bf326c6e\\})|(\\{947c328f-b462-4618-bc4a-a967d1d7bc77\\})|(\\{c04bf263-7edd-481f-a3b1-0ea9d2aff14b\\})|(\\{f69637b8-b48a-46b5-b1ad-132afc1dcaa0\\})|(\\{8f2b4de9-599f-452e-9fe5-a3ea7ee2d633\\})|(\\{ebc34c54-8004-4281-8321-84145bda54ec\\})|(\\{c3f94bd2-0391-44c0-89a1-212ca3844abe\\}))$/", + "prefs": [], + "schema": 1579863851954, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611277", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2af6cf76-8f55-405b-8f5b-d784616768ba", + "last_modified": 1579987441261 + }, + { + "guid": "/^((zddx9wbjta9g23vk5ejo@zddx9wbjta9g23vk5ejo\\.com)|(\\{0629026f-e941-4a98-8975-b8cdcc20fbdc\\}))$/", + "prefs": [], + "schema": 1579796394954, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1611198", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code and/or showing malicious behavior on third-party websites.", + "name": "Browser Kompatibilität (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d94be063-b34b-4ec1-bb02-59bc343029a4", + "last_modified": 1579817517239 + }, + { + "guid": "{c77fdf50-1880-4914-b553-6e3500f43f2e}", + "prefs": [], + "schema": 1579617431416, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1610552", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Pdfviewer - tools (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cb6be8a0-0600-439e-85f1-512cc1d0e48f", + "last_modified": 1579796394592 + }, + { + "guid": "{669207af-aef4-42e5-b1fa-675995be9cf9}", + "prefs": [], + "schema": 1579604778159, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1610480", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Data collection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "54396dc1-f469-4a88-b0f8-352985a85b13", + "last_modified": 1579609877650 + }, + { + "guid": "/^((crisstehn@ffmust)|(jjwalter@junior)|(qlobthedark@ggmay)|(ficlever@roentgen)|(nzclever@roentgen)|(ukkinda@rottaai)|(GOEORG_RF_ANA_N@BDOFsKOKK)|(ioadjf9340joif024rf@ioadjf9340joif024rf\\.com)|(f1a1cz46o6rzl335xcrg@f1a1cz46o6rzl335xcrg\\.com)|(03t4joaijlcvjja@03t4joaijlcvjja\\.com)|(bdojfkobidjfo9e@bdojfkobidjfo9e\\.com)|(0iwtjvpvfhqyv2go1237@0iwtjvpvfhqyv2go1237\\.com)|(g68xhmxwozq8xtp4emty@g68xhmxwozq8xtp4emty\\.com)|(idyzvr0haermejvwfaqm@idyzvr0haermejvwfaqm\\.com)|(czaoyj52ki5owo07318z@czaoyj52ki5owo07318z\\.com)|(4sgly4c7s5pca7o220g0@4sgly4c7s5pca7o220g0\\.com)|(8sd351lwavakull4dcqd@8sd351lwavakull4dcqd\\.com)|(ukkindaa@rottaai)|(besth@lgimm)|(canaddxd@ptrx)|(nlextt@awes)|(bestseg@bbcd)|(swlguy@swlg)|(germctr@prx)|(pro@socialsmonetization\\.com)|(nicejohnus@lg)|(beta@lvvtqmq4qrhgmjb2zd7o\\.com)|(beta@b96mupkh82zywdrmxecz\\.com)|(dvhi19naabzond6ikvl6@dvhi19naabzond6ikvl6\\.com)|(approver@vdmqsgm5nyfiirwh8ryy\\.com))$/", + "prefs": [], + "schema": 1579603732879, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1610462", + "why": "This add-on is violating Mozilla's add-on policies by showing malicious behavior on third-party websites.", + "name": "Malicious add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "448ff9b0-94eb-4d24-af8e-b56141824c80", + "last_modified": 1579604777796 + }, + { + "guid": "/^((f53pabhktayw2qusajt8@f53pabhktayw2qusajt8\\.com)|(weatherpool@bwv9ggnrvitryck9k8tf\\.com))$/", + "prefs": [], + "schema": 1579549290940, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1610359", + "why": "These add-ons collect ancillary user data or take action on behalf of the user without consent.", + "name": "WeatherPool and Your Social" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "30ab36b8-c080-4ded-8531-07259112779c", + "last_modified": 1579603732509 + }, + { + "guid": "/^((\\{293476ee-263e-4cad-8dc4-2fe03209adc7\\})|(\\{622303be-705e-4247-bc2e-9016d8867e3d\\})|(\\{6ac09a19-8de3-418f-a4e1-1ee3e8810990\\}))$/", + "prefs": [], + "schema": 1579601336651, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1610456", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Fake premium products" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3db3364a-0e20-4635-bc63-f2bba4a10415", + "last_modified": 1579603732505 + }, + { + "guid": "/^((\\{525f4b51-8ea1-4db8-bc81-829cf10a14a0\\})|(\\{2411143d-8afe-41ea-874d-ea4a8dc8b1c7\\}))$/", + "prefs": [], + "schema": 1579250266121, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1610061", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b6f17555-93b5-4721-a92d-53500d1fe0fe", + "last_modified": 1579530103133 + }, + { + "guid": "/^((\\{5335fd1c-3baf-4578-b339-516dbdcec832\\})|(\\{1bf381aa-a819-4067-a537-eadb0d6538ba\\})|(\\{0e3703a0-46ae-4d18-bd04-8f8f570fdb77\\})|(\\{b350dc7e-cfcc-4ffe-9225-9feefe922bdb\\})|(\\{eeb3bf29-f1db-4f75-a6cb-8675ace58390\\})|(\\{9a216bb4-d664-4535-baef-ee1f4db012d2\\}))$/", + "prefs": [], + "schema": 1579191500704, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1609718", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Tamo Junto Caixa" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d845bc55-9b05-4dc7-ba4a-57462a51be39", + "last_modified": 1579250265716 + }, + { + "guid": "/^((_65Members_1202@download\\.fromdoctopdf\\.com)|(_65Members_1202test@download\\.fromdoctopdf\\.com))$/", + "prefs": [], + "schema": 1579193594086, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1609721", + "why": "This ad-don violates Mozilla's add-on policies by loading remote content into the new tab page.", + "name": "FromDocToPDF" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ebca1ceb-555d-4ce0-8c06-5b1475f08d45", + "last_modified": 1579250265714 + }, + { + "guid": "/^((\\{61e9b862-ef9b-4cc1-9dc2-ec00e437118c\\})|(\\{29828604-0f21-4ce2-8df7-b840aa53d713\\})|(\\{9430316a-f94c-40b6-9cea-8ac0df5c6638\\})|(\\{87ad96b4-86e8-4d94-aee1-7b607d02effb\\})|(\\{26f61847-2e07-4b10-a030-267eea1bf3b7\\})|(\\{450eb888-9bba-4de7-8821-4bb806ff82bc\\}))$/", + "prefs": [], + "schema": 1579193819569, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1609365", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7e5aad13-e8ee-43bc-91fc-4805c8195f3e", + "last_modified": 1579250265710 + }, + { + "guid": "{f6766565-1c5d-4eff-bda7-20f00aaedd11}", + "prefs": [], + "schema": 1579101106448, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1609550", + "why": "This add-on violates our policies by attempting to install other malware", + "name": "Fake Youtube Downloader" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "21b59bc5-57db-4f31-9e18-614c03858e5c", + "last_modified": 1579191500334 + }, + { + "guid": "/^((\\{15ebdf9b-c3d7-4aee-9568-b42a11a7b071\\})|(\\{a3d09db2-d3ac-4403-9bfa-878de450fbb4\\}))$/", + "prefs": [], + "schema": 1579039175200, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1609265", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake anti-malware add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2e3a3e97-c35a-4ec8-abce-a95c17d96f27", + "last_modified": 1579039687654 + }, + { + "guid": "/^((\\{976ef2b7-3bae-470e-84d9-3212d9200733\\})|(\\{d8fa8e34-e84c-4554-bfd7-16fd023b1c71\\})|(\\{d1ad5122-5caa-4986-a639-ce19bd1bc582\\})|(\\{599fb5a5-a334-4547-8a42-afccc1ddb347\\})|(\\{4bff9999-2ede-4b60-8572-a2915411abbf\\})|(\\{3a9a4c90-d87d-48e9-a799-072a7aa5be64\\})|(\\{822d29f8-9376-4da8-a7a0-4fa2eb5964c7\\})|(\\{c4926e5b-5b37-4781-87a2-28ce0b522d6b\\})|(\\{e36ea66b-30c3-481c-9e00-057b364c1c4a\\}))$/", + "prefs": [], + "schema": 1578927737593, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608886", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "361f3333-3cdf-43cd-b99e-78b666ec33ae", + "last_modified": 1579039174816 + }, + { + "guid": "{b7037d81-2c5c-4747-99e4-f4fc1c888b85}", + "prefs": [], + "schema": 1578938241613, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608887", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "RoliTrade" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "bd9c30ab-d112-4f58-be0d-0e213350b6e7", + "last_modified": 1579039174813 + }, + { + "guid": "{5cc1b399-c98f-483e-9799-be29c6627246}", + "prefs": [], + "schema": 1578656865603, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608432", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Rolimons Plus" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "05061ed7-378d-45a0-b105-3216c256b486", + "last_modified": 1578927737176 + }, + { + "guid": "/^((hddenobdjekcmnkgfpkodhohcjghiijm@chrome-store-foxified-1800284493)|(\\{90e41842-755d-40e0-9136-8129dd44a65c\\})|(\\{edf47ed5-7efe-4725-85d9-5e7a30d42998\\}))$/", + "prefs": [], + "schema": 1578665901864, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608433", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6975177c-1dc8-4fad-94f0-7a43ed397977", + "last_modified": 1578927737173 + }, + { + "guid": "/^((\\{3acb1e80-e126-4024-840f-3297659f9448\\})|(\\{44e3f210-6036-4364-90b9-3e8bb6fb3d98\\})|(\\{51e7e0fa-69e1-43f8-9578-d8372c2885b7\\})|(\\{1c2393b0-f2f7-497d-a34e-399dc6002d26\\})|(\\{a6a02c49-fafc-45d0-bb60-9f940a64c99a\\})|(\\{02a0090d-026a-4d02-a530-1b4d96e80c14\\})|(\\{9a988579-6773-48c2-91ab-8e917b20ab90\\})|(\\{22dfee7d-aa7c-4765-95e6-e81513cc7d37\\})|(\\{8c6d03f8-db65-4d5c-8431-ff66365847c4\\})|(\\{168b7acd-43d3-46d5-b76a-de3139dd9570\\})|(\\{abc95bbf-2548-4bf3-a0b9-9cb028496277\\})|(\\{d1282b8a-c467-4b02-9c11-e63e614ee8a8\\})|(\\{338422e6-bcf6-4171-9541-1c0f8c3dc3db\\})|(\\{10d5b345-535a-472b-8e8f-4f1a1cec9f2b\\})|(\\{64813672-8b55-4ac3-8dd2-c1da80132b77\\})|(\\{00e7df6e-7a0f-44d1-9fc1-0ddbdb473f4e\\})|(\\{564c2b95-b70b-4243-84dd-21fead791642\\})|(\\{71deda20-495f-4061-9c90-f46d1f7dfedd\\})|(\\{054d7610-9edb-47a7-af57-aed4be023015\\})|(\\{2b3f6877-99b0-4d35-8b85-9f75dd53ac92\\})|(\\{299f2568-3330-4466-8b47-4240643f8200\\})|(\\{077ead0f-8e84-4d6e-8fb1-a22126f9bf4f\\})|(\\{8ae52853-efd8-4d3d-b8f4-f70b048a389c\\})|(\\{36321783-e1c3-4f95-859a-d1c88eb75327\\})|(\\{7f878add-6b95-4b57-ab16-d8688819373a\\})|(\\{10148e15-b7f7-43bd-89c0-01957aad8188\\})|(\\{65e3540e-c3d9-4831-9dbf-598f2ef38d7f\\})|(\\{6fe77565-de36-4d06-ae30-59e3c98fc974\\})|(\\{7c79599d-ddde-4f62-9561-3c7aaea788a6\\})|(\\{d2fb1b99-98c4-48ea-ac43-8d729a1a8963\\})|(\\{38387674-fcc2-4292-a20b-08931ea0936e\\})|(\\{f00678a9-4b60-4a24-bdb0-4ce6c960cd28\\})|(\\{15d08bce-4f8a-451f-bdb5-5d1f720dde7d\\})|(\\{3a1da641-0fcc-4c06-b4b9-21d8d5dd3720\\})|(\\{6b7a8c7d-3956-4ac3-8c98-423a0bed1d75\\})|(\\{28f786b3-64a1-4152-9629-efabdede0b4c\\})|(\\{b1535617-e25a-48fe-b47d-c57affc65d5a\\})|(\\{255f303c-d5ce-47af-a925-1ad2c84c710f\\})|(\\{0ab25c60-4750-45f1-85f2-913440d6c6fc\\})|(\\{6cf0ef3e-d911-44c0-8b58-7abcb99d0243\\})|(\\{991c3933-0b3b-49f5-b3a3-1a60bf62f269\\})|(\\{bc5e31d7-42da-49c2-9624-1b4d5707b5ec\\})|(\\{8d8320f0-df3c-48f1-8839-f6969cbd3c17\\})|(\\{35f35aaa-506e-41d4-8468-3c4f4a56b434\\})|(\\{f1f6e2bb-32d1-4d79-8e5a-659e5af15b78\\})|(\\{1109f231-559f-44dd-bd84-85fac05f845b\\})|(\\{b6111372-b58f-4130-a6ae-a1445a196d85\\})|(\\{b21099c0-e496-443b-8d43-610a9aae60fb\\})|(\\{d8a40da5-bbca-417e-9ea5-e77332739366\\})|(\\{5dcaea9a-e152-4667-a4d5-b29f5afe9e61\\})|(\\{98b95d2a-1de8-4234-a73f-568531785850\\})|(\\{b11ad72d-2f64-4494-9f5e-6ad2e36bfc16\\})|(\\{3503a09e-76e5-4fba-8d65-d8bbb198b2c1\\})|(\\{f45fac5e-f3b0-4932-8c8b-254c6dcd3219\\})|(\\{4210d4ec-9e2a-40d1-83de-53a9728c01d5\\})|(\\{10691e9c-7399-4fb2-b824-256ed6c8c08e\\})|(\\{148338c2-ee0d-4659-baed-5b9aca28407f\\})|(\\{f3a2a32b-ec49-4fc5-bb46-f00f86d4cbff\\})|(\\{425bd894-b282-4a58-a2d0-3054fe3fd856\\})|(\\{7ccc3a62-7f92-4a1e-8e32-af734721a136\\})|(\\{91c939ae-00db-400d-a814-a964dc85fcf8\\})|(\\{83fdf43f-c064-4ae0-ac5f-6668fca576b0\\})|(\\{824f975f-a740-47d5-b4c8-0868fdcb154f\\})|(\\{2404f236-28ae-4852-b50d-50e66312f69f\\})|(\\{3f307709-bdf8-4dc5-afd7-4aaeb2a85176\\})|(\\{24f3c174-f09b-41fe-8c26-dfc051b2f352\\})|(\\{ecb81864-05bd-45f9-a1a3-5f56ad62c1c0\\})|(\\{fecad0e5-5f8c-4539-8893-9a4c9e4ab567\\})|(\\{03badfb9-bba5-4a3b-ae1e-0bde1bef38f3\\})|(\\{1f2defb6-af80-4822-b1d2-816c0575dd8b\\})|(\\{195450ff-844e-4ec7-b111-166c28e54b6b\\})|(\\{abd20b0f-3f79-4cdd-b2f7-6ed4f9a3e546\\})|(\\{b1a2d328-e1bc-46b5-b6d3-d4c5366a6262\\})|(\\{49d113aa-c37c-4a49-b73e-69eaaaec519d\\})|(\\{f9fe81b9-29be-4e30-ba7e-821e96b74c00\\})|(\\{c47a57e5-9486-4b58-b4e9-2e49e02c39df\\})|(\\{48aecb91-709c-4660-b0f8-681c177628bb\\})|(\\{0033af2c-0017-4237-821d-24e1ce20ed20\\})|(\\{d95d2ab6-2728-4d89-b4f2-74fc3abcfe26\\})|(\\{813bec70-cac2-497b-9117-8873b5e33cbc\\})|(\\{b613e7fc-3274-4874-b59c-b2afaaba60b5\\})|(\\{f88e19b5-9434-455a-a2bf-de13250a642b\\})|(\\{562fec92-ba72-4461-a73b-48b0cc87c943\\})|(\\{c8d2d52a-5617-420e-9568-983c0c7a6982\\})|(\\{7e32bebe-f9ef-4328-a4f8-7a86afdb9568\\})|(\\{b95fa449-7f1d-44dd-84c7-65565334d1e1\\})|(\\{788d6386-606f-45fb-83a6-af4956d9aa11\\})|(\\{00a646ad-3d34-4ca9-9633-9ea96be7a225\\})|(\\{a745c5df-b386-4906-80ba-ec9b6aa6b37a\\})|(\\{a3b508d5-864d-43c5-a4ec-9d5523cd01a6\\})|(\\{4e945e2a-b553-45e6-8dcb-495c60a663a0\\})|(\\{1a6ea1a0-1a2a-4a57-91c8-07cb629588ce\\})|(\\{20ef853f-2c16-496c-bcd2-5109a7a1be59\\})|(\\{cb5604d4-d166-4060-978e-1c7ddfda4a94\\})|(\\{9207a89d-db88-4ef1-bf2e-a89c12d882fa\\})|(\\{f561afa7-165c-45c5-beef-754e16b40794\\})|(\\{09021ead-28e3-415a-8f9d-ed250954147c\\})|(\\{1d6ef53c-0407-4eb6-aa80-b672788f9d0a\\}))$/", + "prefs": [], + "schema": 1578858092043, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608815", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "2Ring" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1491ea56-7723-431d-be05-5808a3101bf5", + "last_modified": 1578927737169 + }, + { + "guid": "/^((\\{2b046d1e-b392-4073-ad8d-bb882b073be0\\})|(\\{69a8db6c-b55c-4560-bfa7-edf21ed9f265\\})|(\\{d41d46ca-a557-48df-a951-e968a1168137\\})|(\\{845d89e8-e3c0-4447-ad36-9fcba4d3d28c\\})|(\\{da572bae-f959-4f9d-aade-afda00b83e2b\\})|(\\{fe334432-8ba6-4444-8cf9-1bb59c308aa0\\})|(\\{b8242421-8cd0-4064-a6e8-bbfdd62d67fa\\})|(\\{7d863da3-4f8f-4b2c-87cf-5bc7a03b24c8\\})|(\\{fc53c14b-9bdf-4a0b-9f07-421109bcab31\\})|(\\{7771103f-bf4a-4757-b679-2ec30eae8cfb\\})|(\\{66196147-9b28-4050-a46c-c9bf668511d2\\})|(\\{a41db617-17b7-429d-b135-5fd5d54b9ad6\\})|(\\{aa0e1b27-728c-4e45-9b7e-7731e8d6451f\\})|(\\{6e12cb13-d166-47e9-b1a6-ce980e1489bb\\})|(\\{3b3e3db5-2710-4bfd-bce8-bf3a1b5e0bc0\\})|(\\{3e4763fc-2175-4a69-9854-f437774da824\\})|(\\{63708597-3f14-494c-981c-bbecb10fddcc\\})|(\\{ec2c5f61-37e6-49dd-a430-6f0060f6e152\\})|(\\{8eb7ade7-96d3-4be9-b20e-f321fe07c9ec\\})|(\\{b9b60c96-b158-495b-87ab-db97fe2aecac\\})|(\\{5d885234-3fff-4c81-bfe0-7e01ca9701f1\\})|(\\{79cb8cb6-a0fe-434f-a6d6-2af167fbb069\\})|(\\{ec41b4cb-5c60-471f-a68a-d52e91f54292\\})|(\\{fc64c6fc-4356-42e4-8253-75facb478836\\})|(\\{d68b6b30-81e6-450b-aa7d-b0e3f1b11e2e\\})|(\\{6E01B689-3E52-44B7-A33A-197C48498C0D\\})|(\\{D4EE626F-0BE2-4CE3-B635-11498D2EAF24\\})|(\\{bea54a21-f8e6-4a47-a739-84aa5a03a391\\})|(\\{57FE0157-8BDB-4C11-BCD6-6654E0C1AE3D\\})|(\\{8B8E2B7E-7DBE-4EDD-ADA1-8AA93098E3F3\\})|(\\{66c281ce-2682-4182-91da-6d3742ce9a9a\\}))$/", + "prefs": [], + "schema": 1578922331209, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608815", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "2Ring" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d8ef0aae-2659-4076-be2a-ef0cf0388c49", + "last_modified": 1578927737166 + }, + { + "guid": "/^((\\{d26e41d8-8dfa-4a08-ad90-6df0240c8290\\})|(\\{9ee44d55-47d9-45bb-b56c-79ab2fecd93e\\}))$/", + "prefs": [], + "schema": 1578582285426, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608291", + "why": "This add-on violates Mozilla's add-on policies by executing remote code and/or collecting user data without disclosure or consent.", + "name": "Converto Wiz Ads Search & PDF Converter HD Search" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0c502c85-3f2d-4a3b-9abf-249eff0968f0", + "last_modified": 1578656865193 + }, + { + "guid": "/^((browser-safety@browser-safety\\.org)|(facebook-bookmark-manager@fbtools\\.io)|(facebook-video-downloader@fbtools\\.io)|(extensiondist@browser-safety\\.org)|(selfdestroyingcookies@dirtylittlehelpers\\.com)|(googlenotrackpro@dirtylittlehelpers\\.com)|(youtubemp3@yttools\\.io)|(youtubeadblockerpro@yttools\\.io)|(videodownloaderpro@dirtylittlehelpers\\.com)|(simplysearchpro@dirtylittlehelpers\\.com))$/", + "prefs": [], + "schema": 1578653962604, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1608387", + "why": "This add-on has been blocked by Mozilla.", + "name": "Several add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6368943f-3d77-491c-82bc-f29591e170d6", + "last_modified": 1578656865190 + }, + { + "guid": "/^((\\{1c1a344c-b8d3-4783-ac5b-9a9d241c29b6\\})|(\\{a4fccc0e-a372-4127-983e-c0f607427a89\\})|(\\{275f2631-9a1b-4ab3-8c6a-4529ecd8512b\\})|(\\{0b1104bc-8ec5-437e-9d81-2cc641cbe8c6\\})|(\\{4e373c31-1942-4c9d-93b3-38b0ad701f27\\})|(\\{7f1af5f0-b450-44f2-aa5f-bc4c793553db\\})|(\\{68c84054-b43a-4a78-bb35-27ec06974d1d\\})|(\\{f959a2e9-f211-424b-b0cd-ea7ecf269753\\})|(\\{fef99996-e542-45b6-b383-86132e67a93f\\})|(\\{c1bd56b2-4b48-4366-8d04-16a0f69f7b4b\\})|(\\{17456fc3-4777-484a-b177-9b82752a738e\\})|(\\{9c746226-1a87-49e2-a083-725c5fc10885\\})|(\\{3056ec65-736e-4a77-abf7-9d0b44ba0b74\\})|(support@seovpn\\.net)|(\\{fbb6a675-8923-40da-86cc-a547eae63594\\})|(\\{bf9abe9d-4589-41f2-acd4-e5dd9d4a4595\\})|(\\{d012bf08-e9b1-43a2-b6b4-b60c0fdd6fb7\\})|(\\{0cc696dc-6214-406f-8831-1d0ae14c2eed\\})|(\\{39ef127b-a7c5-4cf6-8383-333ce2300707\\})|(\\{77af48b1-2786-430a-a2f8-f0e666ecdb86\\})|(\\{9807c7c0-0d7b-4ee9-b39e-4a2b10f74b74\\})|(\\{21ceb717-4e26-4843-9229-b0a55c629c6b\\})|(\\{ba87282f-fb6a-464b-b2b4-18ab718a6b9e\\})|(\\{f844e48e-929c-4dc7-8224-c829b67d453f\\})|(\\{2af80abc-e031-42c6-b800-02dd4dade3bc\\})|(\\{849583eb-dd9d-424e-af82-d64205b79bb2\\})|(\\{8d8963e3-810b-4e72-85d9-d6e8affbe8e1\\})|(\\{aa385b5c-14b4-4d2a-ad81-db3cd054efe6\\})|(\\{c30a6fea-44b9-4f3e-945a-401485d7e152\\})|(\\{327735cb-5842-4296-a8c9-660118c9dbe8\\})|(\\{2809097a-41e3-468b-9c33-2ce449bc18f9\\})|(\\{91b36c25-1dd4-4a1c-a722-a1868deed9a1\\})|(\\{3b61770f-a22e-4704-be5b-310a729e6652\\})|(\\{b1741149-382d-447a-b8fe-bd2dbf03b252\\})|(\\{8a5d399d-3716-4627-ad97-750cd10783e6\\})|(\\{a1bb458d-6a71-4efb-a032-04f44f638a0d\\})|(\\{8a86326c-5b59-4c87-af77-becea91bdc7f\\})|(\\{21b022a6-b739-4787-a071-268f45bd4f0f\\})|(\\{59e225c1-5a78-40b4-bb6a-9cd783eea9fd\\})|(\\{d0ba8c04-3d8d-4c87-8eec-355d3c1dbc57\\})|(\\{d0b408ae-c1b4-45b1-8d0f-5a9135dab115\\})|(\\{c802afcc-8fb2-410c-a50b-0dfc4e502364\\})|(\\{8ef92c49-fe9b-4354-b943-8fbce6156ab5\\})|(\\{40d41864-a14a-45e9-bc75-ff95189975cc\\})|(\\{b5f5f423-85e5-4641-8517-2549ef2597ff\\})|(\\{bd4e3180-94a8-4d2a-9186-b15ccbca2abb\\})|(\\{1099ff05-78d0-4a0f-a348-0e60d8e627f3\\})|(\\{9d7450f3-9f39-4dda-820b-ac50797229a5\\})|(\\{9762a546-d483-4d82-9e97-e8293b67ab3b\\})|(\\{233e675a-f2e9-494e-a62a-56a8e75440d1\\})|(\\{44b31737-7370-41ad-9a16-7e92f993d651\\})|(\\{5e8c48e7-43a3-4f5c-8f61-018e994ed581\\})|(\\{cabb5a82-b018-40eb-b551-08b7c6b28d25\\})|(\\{19c2d564-4774-4b52-b175-0d9a674a1ee7\\})|(\\{4d6bc7a3-8d17-44d9-a1c9-34b6c72d2a82\\})|(lin\\.Vin@userx\\.com)|(\\{a74a99d0-f5ac-4775-8af5-ad48341865de\\})|(\\{b8331308-eaa2-40ca-94e3-d5184b9555ab\\})|(\\{43a11cab-4d48-468d-88ac-6632c2b90f0a\\})|(\\{13fde54f-87aa-4966-801b-189946617b03\\})|(\\{051e900e-73a7-42e9-bf92-54a05b592814\\})|(\\{389a79d4-7af9-4c22-8e3f-0a424a9d16a6\\})|(\\{d80ce712-fad0-470f-bb42-7f0f5d10ac66\\})|(\\{b08e2ddf-304e-4336-b7b8-15698239be1a\\})|(\\{5bdae4dc-1859-4766-97c6-5d4b7d5ccb68\\})|(\\{a8b80325-5125-4d50-95b8-e3548eca99f9\\})|(\\{16f5bfef-6792-48da-9458-c15a904a3202\\})|(\\{821251e6-48aa-4eee-aca2-d2003047eba7\\})|(\\{e4d3e60e-2980-4899-aaa5-aa2c8571e8af\\})|(\\{00bde975-f669-4fef-aca4-de3335e5e629\\})|(\\{815e064e-df14-440a-a9c7-6191d47f302d\\})|(\\{12267241-c4e5-49b7-afc0-1ba02cd86146\\})|(\\{1461d5d5-0180-4e5c-870c-1648fd13eb3d\\})|(\\{09f52a46-5e5e-4bb7-92e4-5a802ba2dc65\\})|(\\{89fd7bd0-b6cf-49cb-981d-6eaa3515e09e\\})|(\\{309cf23f-2ca7-4308-9c31-03ef0cf0e8df\\})|(\\{604184f4-c000-4071-8b27-ae7c191dd575\\})|(\\{45c9a869-b55f-4792-aefd-e353229b48f1\\})|(\\{1b7f62a2-5dfb-4a7c-8613-a2333ae83337\\})|(\\{6e7e98a6-d7e7-4319-92bf-283cdcbf06b0\\})|(\\{2e60f9f6-f57f-46e5-9876-b50f84154a04\\})|(ali_nasiri@mefa\\.com)|(\\{d7909c43-9b01-44e8-aec2-d569a3461183\\})|(\\{d64d110d-2783-4663-b533-036338b0fabc\\})|(\\{8c5a0a9e-480c-4571-acb3-602cb081f9b9\\})|(\\{1efb16f1-34cb-4179-b27b-301f1a8d023a\\})|(ali@mefa\\.com)|(nasiri@mefa\\.com)|(nasiri_ali@mefa\\.com)|(\\{de57334f-24fb-4b0d-9087-d2945212ba76\\})|(\\{e45a208c-582f-4549-8c39-3e3b6c3f503d\\})|(\\{7cf37c0c-5cf2-4945-8c82-ffd5a1206abb\\})|(\\{aea2f3bb-b278-4ab3-a2b1-370b74a2795d\\})|(\\{c0d80342-fa84-4366-90d5-0d2c965a6c71\\})|(\\{e7953669-b2cb-49bf-8f53-9b2cedbe4df7\\})|(\\{0f5248f4-bb0b-41ee-9162-397ac92538c8\\})|(\\{f2a73021-b834-410c-95c9-6e9e826c5523\\})|(\\{ac9759e0-3657-40c0-8da8-aa83b2df14e1\\})|(\\{AABB3453-4EAC-421A-34FA-4789107489AE\\})|(\\{e471c14f-775a-474c-afde-e2562f35a1ee\\})|(\\{8a420af6-7de8-4d13-bc74-1b90e3a7c45e\\}))$/", + "prefs": [], + "schema": 1578325482610, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1607277", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "87a59976-a41c-48e0-a05f-b00ceddc3990", + "last_modified": 1578409770946 + }, + { + "guid": "/^((\\{29e8ec68-5717-4d25-8465-c8ad11c2493e\\})|(\\{1807ab11-ce49-47e8-b29f-2c7f3b27957c\\})|(\\{d497ce7b-9308-4a00-81c8-f1e20769d9f4\\})|(\\{3e2a7fe2-d800-4300-adbc-85d45101ccbb\\})|(\\{41a6db99-eb5a-436f-add8-6655030cd3c4\\})|(\\{d0041bb0-914b-4105-8335-f19dfe4000c5\\})|(\\{16c0318f-441f-4996-9420-9582cd8dc4a8\\})|(\\{11560f98-1a72-428e-901d-19eee028703d\\})|(\\{200fc207-8d2d-491f-a14d-898c2af6005c\\})|(\\{6098201e-19fe-4930-8235-507abc271c92\\})|(\\{47247604-b3df-42ef-a7d6-e6f54f3ed34d\\})|(\\{d9e45250-0a78-4fad-bda9-435289e1117d\\})|(\\{7897756a-f527-42fc-8327-23afa5a73037\\})|(\\{ebb45fe8-b626-4d4a-9b51-4c61be7d8ec8\\})|(\\{17a3a525-b5e9-4bdf-9f2f-0a6b82489261\\})|(\\{7d5e14ab-7973-45ee-9b5e-fb2af9105a58\\})|(\\{3db27392-c030-4b1d-a461-df246030e8ed\\})|(\\{687dbc1c-4640-420e-b5e0-69ffbff851a4\\})|(\\{99f71699-61a0-407e-9a1e-4f7f2b24032e\\})|(\\{9608a917-807d-426d-8b3d-4bd8e6669d3c\\})|(\\{6d0d1145-1b99-4c1b-9112-a0698ce1ad93\\})|(\\{02373fa8-bfe0-4f6d-bf65-cf843ae16ca9\\})|(\\{76e05242-0ea0-47dc-99ab-85a5768d24ee\\})|(\\{b2671579-9ae7-4e10-9aad-e484189f2599\\})|(\\{f8574a50-a306-455d-96b3-9a95590e3351\\})|(\\{d2a79680-fab2-40d5-93c7-3842a3c7a170\\})|(\\{6d3d4ff6-42b4-4ae6-8273-e55309e194ef\\})|(\\{e26d666b-5437-4b51-9a35-6a32ef2ebc93\\})|(\\{1d1e0112-694e-499e-9a6f-fd4fdd8a0400\\})|(\\{8fef18c6-29cc-44db-a373-2476eaa98d07\\})|(\\{bca08103-f769-42c1-9e83-c3dac1d073e5\\})|(\\{9be3ea2f-3def-4ace-a4ca-a36bd350ebe3\\})|(\\{e4a7d768-fe07-4b2d-8722-782ecd2d857d\\})|(\\{ed0bae9a-6c29-4615-b7b0-8134008f866d\\})|(\\{6a230256-13ad-4912-88fb-35dfe6fc8405\\})|(\\{f33c98b2-f1fb-4e82-a472-b27aa763b697\\})|(\\{692d8158-fc1a-43b9-901c-9211c5f00129\\})|(\\{f43b5702-0a0d-4f50-a012-a9fa25d19ac4\\})|(\\{e5d75e88-cd31-492b-a63c-ab6759658d4f\\})|(\\{706dac82-d1ce-43b6-81fd-77652fad15a9\\})|(\\{8c403e0e-104d-46ec-8355-5b7d57d35e6b\\})|(\\{c5a32b03-4044-4085-91d8-af1d688c0c66\\})|(\\{76e7835b-8600-473e-870f-aa1e9ac922de\\})|(amz@userx\\.com)|(\\{d2b50060-69cd-422b-bc5a-77590580adfa\\})|(\\{e9d1c5e0-e297-439d-a536-efd94b0baa06\\})|(\\{80816d1d-664a-4710-915a-99c298970b38\\})|(\\{3cf55a2e-ab5b-4a2a-8b5a-0760f46ffcc1\\})|(\\{be762a22-7fcf-4c67-a8e4-7f2b085a9f50\\})|(\\{2df2d33c-e1fb-4eeb-b276-a84bd3338e0f\\})|(\\{133ce0f4-83f3-4fec-b672-0067748ccf1f\\})|(\\{b575f7f6-5afe-40f8-8bb3-92c0ca24371f\\})|(\\{d514a041-1260-4512-9759-55c30b63bc20\\})|(\\{4129ed8c-e3ad-4e88-832e-66314cf44268\\})|(\\{9e97ac90-9eb9-429f-99f5-d61f60052068\\})|(\\{3366bcb1-8b0d-4451-9c1b-cf8aeb8e5df0\\})|(\\{3d06b84f-0b8b-4a7b-9a55-22c226c85a55\\})|(\\{e2f8e0bc-62bf-40f4-9dcd-3379a64db2bf\\})|(\\{3e998c2a-c05c-4445-9567-7a1948077970\\})|(\\{51cefcff-f7a1-4c57-b6bc-bad9fc9f275d\\})|(\\{84d6d305-ccc0-4879-b80f-708318ebcaa6\\})|(\\{05a2137a-9dde-476d-a63e-d80400d6bf36\\})|(\\{c2627d22-c3cc-4c5a-a69a-eca6cfe9ac90\\})|(\\{3cfe4756-4994-4c92-a97b-adc57243a277\\})|(\\{5a05e509-59a9-4358-b994-a062d97a7d0f\\})|(\\{791bc86d-dae4-45d5-957c-fe56843770f3\\})|(\\{8976df61-4480-4d4d-bfd1-2e9d0635eaa8\\})|(\\{f4ee4889-fed0-4598-a8b2-c3e224250d82\\})|(\\{e26b2735-47b6-4239-a3d2-0e4ca4293076\\})|(\\{ae9589dd-3141-47d1-9449-8e719044ff92\\})|(\\{ff18147b-3853-4938-b15f-241d99f4e590\\})|(\\{758126c6-d156-4eed-8870-477eefb17281\\})|(ella@fmt-tools\\.com)|(\\{e4df007c-60db-418b-97f3-d7896c441515\\})|(\\{90e41842-755d-40e0-9136-8129df55a65c\\})|(\\{faeee13b-39fb-49dd-82c5-47604a2e2e9a\\})|(\\{c3583bcf-271e-4b41-8207-32bd3db491a0\\})|(\\{cbe6f8d9-bd60-4f27-acd1-388a976e3749\\})|(\\{18be83e0-4bc9-469a-bde5-cc671acb022b\\})|(\\{97152370-246f-4059-999d-92f26f358e71\\})|(\\{8a975636-e12e-4532-ad35-9d9e691a246d\\})|(\\{3eb5ea84-a445-4dab-8074-d29dbf4e66f0\\})|(\\{f19cf85e-47ea-446b-808d-38f4010594b3\\})|(\\{b28a1358-5cdc-442e-b851-613bdf118320\\})|(\\{90f0ad05-0267-4ea6-9809-1098ec724905\\})|(\\{d18372a9-89d9-4799-87f1-ccd4cfac80d4\\})|(\\{d18372a9-89d9-4799-87f1-ccd4cfac80d5\\})|(\\{4625fcb8-6b60-49e7-9447-31cb1e2a2d59\\})|(\\{7dc2d526-d11b-4e6f-9301-a4bc620f5838\\})|(\\{d763ea47-6889-4e23-9b6c-ac56087c96a7\\})|(\\{3c7f00c4-4e4a-411b-960a-4ce782ece6c9\\})|(\\{8c1db2e7-c550-4d76-8009-ad9fca9c0813\\})|(\\{0b23f4db-0f49-44b7-9d27-b424ba9f6d62\\})|(\\{d798f753-a0e4-4d8b-866b-3b817b7b6d25\\})|(\\{e3d7ec01-ada7-4330-899b-bce60c64471b\\})|(\\{3a210e15-601e-4204-8690-cb05ed14019d\\})|(\\{51226db3-500d-45d1-bbc3-a9bf560caf39\\})|(\\{0aa6f915-ed71-4de8-aaeb-63f799d83af7\\})|(\\{3d46809c-6c20-469c-a608-3ece3121da4a\\}))$/", + "prefs": [], + "schema": 1578335483666, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1607277", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0bb0f9b4-6807-4642-9199-026411426eb2", + "last_modified": 1578409770942 + }, + { + "guid": "/^((\\{af51d321-202e-49af-9a3c-78a5ebb22b63\\})|(\\{749b293a-1e19-494b-8a3a-cb79d5464226\\})|(\\{fbe6ff3f-2675-466a-916e-352080c27adb\\})|(\\{ad0378a7-5153-4e6d-999c-6c8e49868293\\})|(\\{2f7de182-3427-4386-a880-c601050580ab\\})|(\\{99bad30d-3792-4a03-ab9d-8e88f5f1cccf\\})|(\\{475b8326-3218-492c-b4c3-32307053c21a\\})|(\\{18571b27-500b-4604-a4ea-e0e7d8b61ce6\\})|(\\{fad11a52-2f9a-4d89-b712-f61184d756e7\\})|(\\{113c9d19-e80a-4c72-9a23-58f7b08144cf\\})|(\\{a0b384d9-c275-4670-a4be-8b0be45d562e\\})|(\\{19bac525-be97-4470-b76a-c24168d5e6e8\\})|(\\{2a40afd4-e56b-425b-bff2-62f190d80fac\\})|(\\{cbc65b60-2350-4b7f-8c83-1b9182a56430\\})|(\\{6adb7178-1105-46e6-9a4d-1824385cef5d\\})|(\\{6d46665c-4708-495d-a929-096ea137a08d\\})|(\\{fd6273bc-3b55-4bf0-872c-146b3feb73f9\\})|(\\{707783a0-6669-4e1f-8036-53f5c936304b\\})|(\\{7086907a-f6fd-46f4-b51c-a7dd2b10509c\\})|(\\{0ed69e4f-6cec-4bfa-9004-076ebd0be34f\\})|(\\{d051ddb8-1de2-403c-84d2-afd96a3f4550\\})|(\\{52699f66-62fe-4970-83eb-5ad82bf72497\\})|(\\{6c32d68b-4866-4277-b454-d824a3a3209c\\})|(\\{d608864f-5573-47eb-a2d7-46d158ee3fa7\\})|(\\{38c59c7e-bc02-4e7f-89ea-c4d9555362c6\\})|(\\{981a212a-52b0-4413-976c-a4730f1df128\\})|(\\{06e004dc-40a8-4123-addc-c6b6eb5234c7\\})|(\\{c87d86f9-d5df-4a24-9fe0-dd39232e4d88\\})|(\\{86baf51d-21b6-456e-889b-c7d96ba2fd63\\})|(\\{db83a6c1-efb7-45a1-b074-8e3e5a53793e\\})|(\\{70c00ae0-5b15-4ee8-99f9-23d85c63c07e\\})|(\\{ff72e707-d6e2-446f-a760-6ff187f107c9\\})|(\\{6f65c53b-596f-4238-afa2-347150b3ada3\\})|(\\{95da423e-90ec-43d9-b5c8-e195bff7b432\\})|(\\{4a792129-a244-4a39-a416-ce4f7743918f\\})|(\\{d69204d4-610c-4144-9d26-8cf8b2ad6e16\\})|(\\{49242aa4-83ba-4e25-a827-4d1ec86972a8\\})|(\\{d69204d4-610c-4144-9d26-8cf8b2ad6e19\\})|(\\{3505b15c-533a-4c2e-9065-6d7b2381a7f9\\})|(\\{07daa733-944d-4b0b-9bce-01638b9d9b42\\})|(\\{ebb69cbc-b69f-4d3d-a676-83811a061baa\\})|(\\{f9327eb7-2532-4e45-a551-a9f8233e4ef7\\})|(\\{4fcb9938-6716-46ce-8107-73f3cc7b3900\\})|(\\{cee1ccee-5508-4927-a939-9a557e63bbc8\\})|(\\{c6973149-51bd-443a-898c-e4b26d4ee44b\\})|(@mouselessjj)|(\\{39772968-f7e5-4a84-87d2-adcd2d032c69\\})|(\\{3ba568f0-bb3a-4744-b64f-46c428f3bd99\\})|(benalio@gmail\\.com)|(\\{70c50468-54be-4d7c-b1b4-b7378d0ff45d\\})|(\\{f77c3471-806a-40d6-9fdc-7df832f7ae74\\})|(\\{cf660d66-4aac-461b-a05f-b71b62f549c6\\})|(\\{1c58ca6f-91ad-4423-b8bd-f5323df92eb3\\})|(\\{1a393581-d6ce-454f-a503-8a321ba0f022\\}))$/", + "prefs": [], + "schema": 1578339693692, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1607422", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "18cdf7c5-6650-46d5-a536-364ce9dcfa92", + "last_modified": 1578409770937 + }, + { + "guid": "/^((pro@affiliatebrowserguard\\.com)|(beta@affiliatebrowserguard\\.com))$/", + "prefs": [], + "schema": 1578394762485, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1607211", + "why": "This add-on violates Mozilla's add-on policies by redirecting requests without user consent or control.", + "name": "Affiliate Browser Guard" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c8aa2f43-2c48-4628-af4f-0987125d0441", + "last_modified": 1578409770930 + }, + { + "guid": "/^((@adultdownloader)|(\\{6391a084-dd1c-41b8-aefb-ce207da8bf21\\})|(\\{19e997bc-8449-4ab3-b4f2-f24db1053fa9\\})|(\\{e65a0802-7bbd-4c83-9bec-50ec16dadcdf\\})|(\\{6928cfc2-eda9-4971-8d8c-de26747c27ea\\})|(\\{93c15410-f7c9-41a7-a13d-28dba750f15e\\})|(\\{d7c369d6-74e2-4da7-8eef-dc134914facb\\})|(\\{e3109828-453d-4fc1-8019-488f891720d5\\})|(\\{d825dcfa-4340-4330-b4e7-b208439ee961\\})|(\\{8e7d2b8c-6167-496d-8e4c-45d9040a4a7a\\})|(\\{62935c48-c7bb-4a6c-a1b0-449fdc8737c0\\}))$/", + "prefs": [], + "schema": 1578166891954, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1605959", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c086bfd7-15ff-41eb-ac43-d84e95606eec", + "last_modified": 1578325482223 + }, + { + "guid": "/^((\\{943773bb-c8a8-4576-bf3a-7cacca534887\\})|(\\{0afab6d1-c267-4ff6-9289-4a2b2ad78f33\\})|(\\{4278c56e-7b67-43ab-8c87-f68a6a883df6\\})|(\\{f146cde8-6dcf-4ebf-9d67-b7eccc9bc8a6\\})|(\\{d0e43d96-0e73-48d7-9a60-b235022f4330\\})|(\\{a94ed9b8-24a6-4719-97af-08eaac91a42b\\})|(\\{d62ca114-0f4e-4d25-813e-292d0c682e05\\})|(\\{908b1c05-3766-45e1-b56b-f0b4457c6451\\})|(\\{068054b5-46c0-47c2-be37-dd015fb1c050\\})|(\\{1c1cde54-c8e9-4c61-a2be-30411888ac8a\\})|(\\{b06648df-3c80-49e8-9d9e-71741ba28c72\\})|(\\{a085da90-cc10-4c26-954e-ae4eb773c6d7\\})|(\\{1d53aa0d-eca3-4bb1-947c-aebfdd0770fd\\})|(\\{83ebd575-b66b-4b02-a628-d2764194d0a6\\})|(\\{fd5f7e74-2e60-454b-b702-05e9d66b334b\\})|(\\{fdd2f5a3-9061-4fc8-87d2-cf7f7668d336\\})|(\\{f890d432-ce0b-4857-b030-dc36fc791423\\})|(\\{d5b4e84c-7991-4be5-9c26-c3e92ba0901a\\})|(\\{8d54459a-34cb-461f-b1fe-2266484cc098\\})|(\\{67b21df3-a1da-48ee-aee8-18ae8ad7fe21\\})|(\\{18feff20-5892-43cd-8606-83e8e7c33ebd\\})|(\\{554e901f-5476-4fad-8714-a08b8249068b\\})|(\\{248c9c9f-404d-4284-bd12-2fa988edb0ad\\})|(\\{9c347568-2528-4dea-8a0d-6e9fa7f88512\\})|(\\{0447e445-abb9-450b-b12c-de16fa40e176\\})|(\\{3015019d-1c2f-4310-adf1-1b33c4566d9e\\})|(\\{a3aca1ed-74b4-46d4-9c73-ca9ba8c91ac6\\})|(\\{7b2aabfa-673f-4afb-9cba-7a9329a24d79\\}))$/", + "prefs": [], + "schema": 1578083875760, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1606925", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "New Tab add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "de533f3a-d0e3-4704-916e-fd2a297cee93", + "last_modified": 1578084466436 + }, + { + "guid": "/^((\\{a91c41b7-4196-4867-84b4-d417a1b10da2\\})|(\\{e3d3ea5c-c8c0-4c9e-89c6-f6b5677186cc\\})|(\\{fa3b6777-4f64-476c-9ace-a79709ccd0a6\\})|(\\{6251b844-0c54-44bc-8c5e-891e3ba86c2e\\})|(\\{8e588566-afb2-4612-a420-3e3bd18693e7\\})|(\\{a0bf35ec-76cc-4d86-875b-09d46e8790c2\\})|(\\{fd257beb-d772-4333-9901-dc5913aee499\\})|(\\{a9acb248-93fb-4081-9d88-92468f229842\\})|(\\{7e1b62bc-3ab4-4c4d-8182-b095a492eab4\\}))$/", + "prefs": [], + "schema": 1577994095119, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1606132", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code and/or executing remote code.", + "name": "CodeScript and WorldScript" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3d3f2971-9764-4b6d-94ef-2a0b97da7619", + "last_modified": 1578066343576 + }, + { + "guid": "@mendeleyimporter", + "prefs": [], + "schema": 1576845976756, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1600910", + "why": "The add-on does not comply with Mozilla's requests to provide reviewable source code and is suspected to violate Mozilla's add-on policies by collecting data without disclosure or consent.", + "name": "Mendeley Importer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e028ecb3-be1e-48fb-9681-9c04ff7fad3d", + "last_modified": 1576881756956 + }, + { + "guid": "s3firefox@translator", + "prefs": [], + "schema": 1576845555478, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1605007", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "S3.Translator – s3firefox@translator" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "31a14241-2af8-4ec8-b597-d5c3ebedc30c", + "last_modified": 1576845976374 + }, + { + "guid": "/^((\\{cee1ccee-5503-4927-a923-9a557e63bbc8\\})|(\\{8a84b40d-84a4-40d6-96de-c504a4fcd114\\}))$/", + "prefs": [], + "schema": 1576784492013, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1605289", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Adobe Flash Player" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d940ce81-7ac8-4741-9fc8-2699cb92d4ef", + "last_modified": 1576845555097 + }, + { + "guid": "/^((therill@mozilla\\.com)|(Updates@mozilla\\.com))$/", + "prefs": [], + "schema": 1576756361317, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1480591", + "why": "These add-ons violate the no-surprises and user-control policy.", + "name": "Search engine hijacking malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e235a161-aaa1-4afd-8e9b-61ab2aeaf82e", + "last_modified": 1576771657205 + }, + { + "guid": "/^((\\{5620c992-8683-4ce1-b19d-3633b4c28bd0\\})|(\\{cbc29a75-5858-4b7b-98e4-c813a4e6a085\\})|(\\{4cf619a8-2de2-41cb-bf23-dfa52e4e7d5a\\})|(\\{3b013e48-d683-45ed-8715-a6ece06f0753\\})|(\\{9834ff7f-e3ea-485a-b861-801a2e33f822\\}))$/", + "prefs": [], + "schema": 1576756618517, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554606", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Various remote script injection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7c324513-5792-495d-a389-d7527a0433b2", + "last_modified": 1576771657202 + }, + { + "guid": "/^((\\{ab70d6ee-9d0a-4349-919f-2e3c9aa77927\\})|(\\{fe94f94a-75ff-48a9-9cab-03e626e30352\\})|(\\{085590fa-c340-423d-9b45-d8e963349513\\})|(\\{c06005f4-a53f-4503-b631-9c6fbea45e9e\\})|(\\{a1904bba-73b5-4fab-8556-95fdf0200c19\\})|(\\{4548ed4c-964e-4a53-acec-b24f5b9ea6a6\\})|(\\{99d68c16-4f64-463a-ad09-470a5ac07981\\})|(\\{14338345-a844-4c6e-9fca-d200a93f1d9b\\})|(\\{2bc78397-6bd3-4a2f-a737-dbc639ee9940\\}))$/", + "prefs": [], + "schema": 1576756684025, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1565184", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Private Search" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "92cedc61-c6c7-485e-98b0-a03c71eb2c5a", + "last_modified": 1576771657198 + }, + { + "guid": "/^((application@uk-manulap\\.com)|(application@uk-misafou\\.com)|(application@uk-nedmaf\\.com)|(application@uk-optalme\\.com)|(application@uk-plifacil\\.com)|(application@uk-poulilax\\.com)|(application@uk-rastafroc\\.com)|(application@uk-ruflec\\.com)|(application@uk-sabrelpt\\.com)|(application@uk-sqadipt\\.com)|(application@uk-tetsop\\.com)|(application@uk-ustif\\.com)|(application@uk-vomesq\\.com)|(application@uk-vrinotd\\.com)|(application@us-estuky\\.com)|(application@us-lesgsyo\\.com)|(applicationY@search-lesgsyo\\.com)|(\\{88069ce6-2762-4e02-a994-004b48bd83c1\\}))$/", + "prefs": [], + "schema": 1576756765926, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1487627", + "why": "Add-ons whose main purpose is to track user browsing behavior.", + "name": "Abusive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "42d1f27b-9cbf-4a3e-a18a-5c4418c2f5b6", + "last_modified": 1576771657195 + }, + { + "guid": "/^((\\{d1b87087-09c5-4e58-b01d-a49d714da2a2\\})|(\\{d14adc78-36bf-4cf0-9679-439e8371d090\\})|(\\{d64c923e-8819-488c-947f-716473d381b2\\})|(\\{d734e7e3-1b8e-42a7-a9b3-11b16c362790\\})|(\\{d147e8c6-c36e-46b1-b567-63a492390f07\\})|(\\{db1a103d-d1bb-4224-a5e1-8d0ec37cff70\\})|(\\{dec15b3e-1d12-4442-930e-3364e206c3c2\\})|(\\{dfa4b2e3-9e07-45a4-a152-cde1e790511d\\})|(\\{dfcda377-b965-4622-a89b-1a243c1cbcaf\\})|(\\{e4c5d262-8ee4-47d3-b096-42b8b04f590d\\})|(\\{e82c0f73-e42c-41dd-a686-0eb4b65b411c\\})|(\\{e60616a9-9b50-49d8-b1e9-cecc10a8f927\\})|(\\{e517649a-ffd7-4b49-81e0-872431898712\\})|(\\{e771e094-3b67-4c33-8647-7b20c87c2183\\})|(\\{eff5951b-b6d4-48f5-94c3-1b0e178dcca5\\})|(\\{f26a8da3-8634-4086-872e-e589cbf03375\\})|(\\{f992ac88-79d3-4960-870e-92c342ed3491\\})|(\\{f4e4fc03-be50-4257-ae99-5cd0bd4ce6d5\\})|(\\{f73636fb-c322-40e1-82fb-e3d7d06d9606\\})|(\\{f5128739-78d5-4ad7-bac7-bd1af1cfb6d1\\})|(\\{fc11e7f0-1c31-4214-a88f-6497c27b6be9\\})|(\\{feedf4f8-08c1-451f-a717-f08233a64ec9\\}))$/", + "prefs": [], + "schema": 1576756852767, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1476369", + "why": "These add-ons contain unwanted features and try to prevent the user from uninstalling themselves.", + "name": "Smash/Upater (malware) and similar" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0896a86b-a211-4c64-968c-8671a21b2b5a", + "last_modified": 1576771657191 + }, + { + "guid": "/^((\\{2926cc6a-5f5d-4ef7-9cd0-0417408e8809\\})|(\\{569a3468-ebdc-4d95-84ef-0b6cbbcb2119\\})|(\\{6ef88cf3-c31c-4a9b-973b-765c21bc6f81\\})|(\\{a36b6921-0b70-4fad-b39b-786b23716b74\\})|(\\{fcc6a194-c627-4bf1-b185-e9611dc8d56d\\})|(\\{ddc71014-7997-421d-bb86-3b70b2ab24d5\\})|(\\{453638fc-e7f1-4a3d-acfb-724a0c750686\\})|(\\{0283d25d-25e6-4710-845f-b112fb264d3f\\})|(\\{071b1860-ff82-46d0-816c-a458382beaa7\\})|(\\{f0f6892f-6d0d-4146-9de5-21cc7eb3672a\\})|(\\{845c7444-bd97-4200-8144-c95ce77c07be\\})|(\\{8bc84806-edbd-4713-8a53-19a8c345fdfb\\})|(\\{959dfd6a-fbc5-49f0-8284-34e8606c7fd2\\})|(\\{9f90e1c8-a63e-43a9-8cd5-ad1d6fdf0f3a\\})|(\\{6b7cde7b-a79d-4012-b36c-86719ebcc308\\})|(\\{57f57692-7ef1-473a-adf7-7507e0e31c7d\\})|(\\{34bc568b-4243-4001-b528-118df3310667\\})|(\\{521e44f0-374e-466a-8829-2a98af75104b\\})|(\\{a4de65f7-33ae-4622-bb17-15053f0383f2\\})|(\\{f5ca8ea6-e854-4163-bc55-1a6f4086c1b2\\})|(\\{45765127-988a-4f3e-807d-f7784d76ecd0\\})|(\\{dc95db1e-68c5-44f4-a924-daac966373d2\\})|(\\{4119a8f1-508c-490d-87d4-203e93bbc5fb\\})|(\\{85997b97-ae1f-49e2-b42b-60f9c78c2d03\\})|(\\{a39c31c3-a57a-4c26-8454-267414494eed\\})|(\\{fa47c7ee-e855-41b0-9d3d-3c1d46d98a19\\})|(\\{6255931b-744a-49a2-bf6d-69ba35556760\\})|(\\{54112e17-779d-4275-9a04-81760931d356\\})|(\\{cea0dfab-286c-44c5-b41e-79d8ca5d097e\\})|(\\{fd6b2837-7828-4ad8-9bfe-361336aba33e\\})|(\\{0a56d404-ece2-441d-8d0b-e276ae1a07c2\\})|(\\{be54a202-b6e0-485d-b60b-f77f117ae602\\})|(\\{ff927da8-a634-4b4f-9010-2e76e3a6db2d\\})|(\\{a91f82ce-675b-4940-b58f-96095eb004af\\})|(\\{885c70f6-e286-433e-8f42-d7f319321d9d\\})|(\\{3e52fa7b-34c5-48e3-9cc2-3e33464db303\\})|(\\{ca94cd4e-3aec-41af-aa3c-1a8ddba9b9be\\})|(\\{0971aa7e-3a32-418a-acca-fdb5dacfca2f\\})|(\\{4d16cfea-bbe5-4254-b98b-70267cf2a2bc\\})|(\\{5cb20af8-1a98-43b0-9f09-0ee6d494ce65\\}))$/", + "prefs": [], + "schema": 1576756919670, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1558136", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various fake Flash/Avast/etc clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c81cb302-2519-4997-84de-75aa01f2dee9", + "last_modified": 1576771657188 + }, + { + "guid": "/^((jabcajffpafebcdcaddoegpenicdipdk@chrome-store-foxified-1110252619)|(jpegcert@freeverify\\.org)|(kmrfree@yahoo\\.com)|(lets-kingw@empotrm\\.com)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-29039950)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-77744803)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-357866719)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-447115206)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-549146896)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-1084455972)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-1602969934)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-2271560562)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-2595595173)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-3103352300)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-3116340547)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-3959272483)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-4076222235)|(mdanidgdpmkimeiiojknlnekblgmpdll@chrome-store-foxified-4090031097)|(media-certificates@auth0\\.io)|(mingle-cash-extension@minglecash\\.com)|(MiningBlocker@MiningBlockerAddOn\\.com)|(multimedia@certifications\\.us)|(nominer-block-coin-miners@tubedownload\\.org)|(open-in-idm@tubedownload\\.org)|(sabqo@yolla\\.net)|(search-by-image@addonsmash)|(selfdestructingcookies@addonsmash)|(streaming-certficate@mdn\\.org)|(swt@gobck\\.com)|(tabs-re-open@gtk\\.cc)|(user-agent-rewriter@a\\.org)|(vba@vba\\.com)|(verification@bexp\\.co)|(vidcert@certs\\.org)|(xplayer@gobck\\.com)|(youtube_download_express@free-downloader\\.online)|(youtube_downloader@downloaders\\.xyz)|(youtube_grabber@utubegrabber\\.co)|(youtube-lyrics-by-rob-w@awesome\\.addons)|(youtube-mp4-downloader@tubedownload\\.org)|(ytdownloader@ytdownloader\\.org)|(yttools\\.download@youtube\\.com))$/", + "prefs": [], + "schema": 1576757015029, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1525880", + "why": "Add-ons that include abusive or malicious remote code.", + "name": "Various abusive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "eaa146be-e47f-4c20-bdbd-c8046a929100", + "last_modified": 1576771657184 + }, + { + "guid": "/^((\\{ed4c3ce2-5372-429c-ae20-fa5b1f540fd7\\})|(\\{cd11da28-330d-4f09-a21f-fae7509f1b60\\})|(\\{74bc7a66-d4e6-4f1c-a0ef-1b65baa41cbf\\})|(\\{8069effc-45bb-4caf-8b27-a135431cd6b9\\})|(\\{577fc233-25bf-4e43-a164-aa75eb9d053a\\})|(\\{f5626996-f5cd-4d00-bcea-20dda6d9edd6\\})|(\\{9bb810ef-716e-4dc5-9f03-491a2c59384e\\})|(\\{02634a24-04d0-439f-9faf-a323ab4a1bac\\})|(\\{b73f7a43-a43a-47f5-8b1f-1ef7caa7857d\\})|(\\{3b5bf07b-5964-408a-8e43-e0239219c524\\})|(\\{73d3a404-150f-4594-ac2c-24f9beec78b1\\})|(\\{ef6a2133-5ed9-4dbc-a735-6ffe8490062e\\})|(\\{76b61321-01a1-4a17-850f-b064a0366b57\\})|(\\{bc41ca18-9209-4500-a847-4e514fea2536\\})|(\\{32c4c845-9bd7-4b20-97fa-a7616e7802ef\\})|(\\{410d9002-b517-471f-956e-30129e307af3\\})|(\\{43366e90-e4be-4ba6-bec0-3fb149128480\\})|(\\{8ebc90a4-f7a1-420a-8380-f85545403f80\\})|(\\{6d3fa41f-e896-4f85-ba59-321f4b26f380\\})|(\\{05c811f2-f828-4d3e-ad02-7386373e9a28\\})|(\\{e10a0ee6-8083-42a7-bed1-35400b029bf2\\})|(\\{c925be5a-ae0e-4958-be36-44dc2e64d4f7\\})|(\\{3803ed37-c101-4b21-a678-762f51b7eabf\\})|(\\{08a15cc0-d6cb-43c5-9a40-27443554b455\\})|(\\{d0953283-5970-4ebe-b270-940c6befdbb7\\})|(\\{7c983689-80c7-46dd-b9d2-4d2db1cf94a6\\})|(\\{8f320a17-868c-43dc-94fb-9d1ab7f4fe73\\})|(\\{843b406a-9593-49bf-9365-684fe8cb2f5a\\})|(\\{42cd0cbd-248d-4a44-88b6-1a3680d159ac\\})|(\\{23efa05d-99b1-49e0-a67d-5378f2afc20d\\})|(\\{f91c606c-dd33-42a4-9219-824187730f59\\})|(\\{9e233d16-18ae-4519-a83c-2806f4fee321\\})|(\\{a93cdf30-75da-463b-865b-f49cc7fd2697\\})|(\\{dcacb62c-9096-482d-845d-10413199a89b\\})|(\\{9d5da26f-c366-46b2-b3e7-5c8e3e0b9788\\})|(\\{dd176d1f-8cd5-4b5b-8b06-839449e87b5e\\})|(\\{9ada3b66-4412-427d-8696-ac0fe0ac891e\\})|(\\{9695495e-cb65-4cd6-8a93-52c9e2b8d767\\})|(\\{e9d1a027-a84c-4e90-b602-66ffe22a0ad6\\})|(\\{f71bda5e-c591-44aa-8f84-2f04989f7e7a\\})|(\\{e6e67c6f-c010-406d-8575-1835341ec4cf\\})|(\\{22fbf524-38be-4ead-b6ce-e55cb23ed74b\\})|(\\{866dafe9-1c49-47d7-a46c-1cb50ca52461\\})|(\\{3479fadc-41b1-492e-bb16-d8f9e514d488\\})|(\\{8c02daf2-79ed-4650-89ca-1e099d28c5e7\\})|(\\{65e6b805-7f0c-455f-b1b4-c34621056b46\\})|(\\{78de7006-944c-4c18-a33a-d6931619f2b0\\})|(\\{16c8051b-2c16-4641-bf29-2daee7883fd0\\})|(\\{19263ccc-a97f-49f4-867a-b49351c42c0c\\})|(\\{bfe416d8-e8c3-469c-908e-6926770152f0\\})|(\\{a29a4a96-2fcd-48f8-bfe1-a1d1df46e73d\\})|(\\{2d651636-a0fa-45b7-a97e-ebc85959ff23\\})|(\\{ac8a3af8-e264-4a0b-b813-d7fab03ae3fe\\})|(\\{4719ad8b-354b-443d-b1e6-4d60b851c465\\})|(\\{7679a9f9-29d8-4979-86e7-a5b5cf0e2fd3\\})|(\\{099e1648-58e7-492e-8019-3418263b9265\\})|(\\{9a83d154-4ea3-45f9-ae21-28f3c1f86773\\})|(\\{475b88fd-574c-4881-98e0-0184a03593cb\\})|(\\{d7b586f8-a22d-4986-9dfb-67d49ba46a68\\})|(\\{50b79e30-a649-4477-8612-7085c0ee3ad4\\})|(\\{e9b2d453-9a98-41e4-9837-c0d68ff1aeac\\})|(\\{b1814ce5-0d9f-495f-b260-a7e1e5538263\\})|(\\{9b06d35e-2eb0-4653-886f-a3f4cdcbb754\\})|(\\{e7d6a360-69d4-4f8c-a96f-fd63388995b2\\})|(\\{68a50af6-ddad-4750-a9a7-a71c55e019b7\\})|(\\{8286a0e0-ba89-48b3-871b-8c9acff32023\\})|(\\{b3c79903-9bc5-4ddf-aeeb-7d91989ae819\\})|(\\{7c5cc4ec-9637-428c-bcf7-28bba279cf84\\})|(\\{93d460ee-879f-4d8f-8599-a1c69ed59ec2\\})|(\\{207c95d5-2bb9-4760-b3a4-8c58ea173bff\\})|(\\{b3482681-1abf-4dfa-bace-dc7b51e6a150\\})|(\\{d3516cf6-d531-434a-b80a-df72c7166744\\})|(\\{da01a2aa-0cbb-4f57-a395-2256d142c519\\}))$/", + "prefs": [], + "schema": 1576757089378, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549214", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various Keyloggers" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "71f2c72f-b4d7-4ea9-bb8f-70d4d86eb184", + "last_modified": 1576771657180 + }, + { + "guid": "/^((\\{84a9ae69-5c01-4a73-80d3-c2201410d8c1\\})|(\\{852c68da-c573-42f8-85f8-9dcf57684f87\\})|(\\{85a31d9e-063f-421f-9d3f-649a393e94ec\\})|(\\{876778c8-5329-461d-882e-d4983ae6062a\\})|(\\{8913da4a-46fb-461c-9e60-3e257ef2c0e6\\})|(\\{8aa0118c-998e-40ac-84e9-12c936e5d70e\\})|(\\{8b4a6441-811f-4461-b136-7ebf3aebe3dc\\})|(\\{8bc41c3b-e052-4fd8-8de3-970ef5224bd9\\})|(\\{8e03b200-aad9-434b-9a99-e7aae7493a5f\\})|(\\{8fe94d0d-4746-401c-ad05-e0e1be97ea0e\\})|(\\{900f3c9c-b327-4608-950b-9765119c2e7a\\})|(\\{915080f5-97a3-4584-861f-70cd91f56474\\})|(\\{91d034a6-1765-4a59-85e5-9ddeb371ed9b\\})|(\\{9207dfae-06fc-4545-9fa6-6466b7ed2559\\})|(\\{926b2440-8443-4de3-9025-9a448cf3b838\\})|(\\{952bfd34-d195-4b10-8a3c-b103786cf090\\})|(\\{95315ff2-427b-427c-a433-236fb3b5eda4\\})|(\\{971db1e5-a5cb-46f9-91f9-9b687f4e5832\\})|(\\{982e11b3-e092-4713-81d4-5da1eadd278e\\})|(\\{98a2b9a7-13fa-49ff-aaa4-83786fad7862\\})|(\\{99f52d4d-1cd2-4e17-8f57-fa2493848f3f\\})|(\\{9a467b2c-be87-4d55-80d9-998dc6243e8b\\})|(\\{9abfecfa-d53e-4aea-bb6c-4fe47367f61e\\})|(\\{9b0243a5-92fb-43a4-adcc-3161f0ec030c\\})|(\\{9c7bb0bf-1534-4805-b9fa-a91004bd7e30\\})|(\\{a00e65f6-bf34-4ef9-a0e5-b63002c823e9\\})|(\\{a0dce648-f703-4867-9f3f-9bfa7601d1b1\\})|(\\{a16a700a-35ff-4ed1-ab81-164e3c823342\\})|(\\{a1f14b23-0c36-44e8-8f0d-9c732acbb550\\})|(\\{a4ea8038-65ae-4d7c-92e2-dd95caf007f4\\})|(\\{a55cd5be-89e4-40ba-8c3b-0023a1f41c8e\\})|(\\{a57ec9eb-cbab-4ddb-bafd-80cf5fd38891\\})|(\\{a5aa1d1e-dec7-4e25-bead-0861099f9628\\})|(\\{a604a85d-ba8f-4e8f-8ca1-867ca8d13a13\\})|(\\{a7a33aad-9e17-4db3-a127-d185e31607ae\\})|(\\{a9404f9f-6ac9-4366-bfcf-50d0d3bdeac3\\})|(\\{aaf2dd6b-5ca9-47aa-b41f-5b00c5c82d2e\\})|(\\{abf10dee-7cc9-4b79-ad5b-1e4300ab24a7\\})|(\\{ac97e702-b2e2-4a91-ae3e-bf0856300737\\})|(\\{ae03577f-2d20-4775-8286-685cdbee76e4\\})|(\\{aef2e959-90c0-44cf-bbb5-e0789af93efe\\})|(\\{af0d8090-d04f-4e9a-a3fb-1c9ac89e9f68\\})|(\\{b0da2032-0da5-4cff-b91b-e0efda4d6b36\\})|(\\{b2777372-311f-4a15-81e5-c84dd845c93d\\})|(\\{b44ac98d-6101-467d-a959-d6ada2259f01\\})|(\\{b50daf26-3983-4516-836f-0b8777bc44ab\\})|(\\{b5ca55b9-d06a-4538-be4a-38b29f3a4359\\})|(\\{b783327e-a675-40c2-95c7-59eb3f00b75d\\})|(\\{b91f2cd5-4051-4e13-8848-8e92afb99217\\})|(\\{ba32ffe1-dabb-41d6-a45f-f4d3e1304ff1\\})|(\\{bb80ea9f-8263-4183-a52d-e5d45ca6e0fe\\})|(\\{c0ba2c3c-55a6-4d28-bb27-67f71de78feb\\})|(\\{c0ecc589-04de-4243-9279-100b781f7443\\})|(\\{c2f6447c-e2db-43d1-8c53-fec7c29b22bb\\})|(\\{c4492fc0-70ed-4d36-8904-61ccb663eaac\\})|(\\{c58e10ce-d69e-478d-8270-0d73599a8cfc\\})|(\\{c72781ce-8377-41ae-984e-ed5755af28de\\})|(\\{c7f51f89-f47c-45e6-aa57-177deba406a0\\})|(\\{c859eaff-3dde-4d83-9703-0a6cf9e95308\\})|(\\{ca51951b-5c9e-4c26-bca3-ed6e754ae5c0\\})|(\\{ce9f05c7-6246-4918-8505-fdc455bc0aab\\})|(\\{cf0ec4e1-5d0d-4846-aa97-380806e72e46\\})|(\\{cfa73be4-9e64-4aea-bb0a-2ab0defb27b3\\})|(\\{d12c5edd-1182-4bf7-bdb1-f2662b7ce1be\\})|(\\{d2343e30-0253-4556-9dd8-cb6cb461801d\\})|(\\{d7a1fad5-eb70-4f7f-a24d-98c3bb9a7aa4\\})|(\\{d7a7e3d1-e6f2-45e3-957a-4b2cde1b413b\\})|(\\{d946d1e8-38bd-41f4-8dc7-a255802046a8\\})|(\\{da7e77cd-4a7c-4282-a597-0694ada485b4\\})|(\\{dc905949-378e-4b8c-aacc-cff56b04370d\\})|(\\{dca4c8f5-5ef9-40fb-bd76-dcb4ec98c495\\})|(\\{dd275beb-f7dd-4ff6-8fec-23e8c0422b68\\})|(\\{de88be71-25f9-48d0-adc9-3d9a542cf303\\})|(\\{df148b39-f7c2-480d-ad8b-91b700e6642b\\})|(\\{df55df20-2e99-49fd-90bc-b548b833e2db\\})|(\\{e1348bc8-b378-45a3-95bb-4915b8910c1e\\})|(\\{e72aab9f-77f1-4e03-a4b7-9ea4b066fe50\\})|(\\{e8372510-9f1b-4b11-8e2f-dfc1d5d1a4a1\\})|(\\{eae5c7b6-8b67-4645-a1c1-a543e63ceda5\\})|(\\{eb1ed544-82e6-4785-b693-1e0799f7cffa\\})|(\\{ec37edc4-e1a6-4073-9cd4-7a5315c921e3\\})|(\\{ed240b54-8600-496b-a034-d9a153359906\\})|(\\{ee6e56cf-b963-4efb-b64e-cf6117dc9a5b\\})|(\\{f3337e21-4fbd-411c-b1fc-d0543052b499\\})|(\\{f5a4fafb-2f75-4acc-9dad-324ca00a1b84\\})|(\\{f9b00c32-2f31-436b-8cb1-720b12502cb6\\})|(\\{fdfd1815-cf54-4210-8883-a4154668b866\\})|(adobeflashplayer@flashplayeradobedeveloper\\.com)|(adobeflashplayer@flashplayeradobedevelopper\\.com)|(afplayer@firefox\\.pl)|(afplayerx@firefox\\.pl)|(aktualizacjaalamusowjeac@wp\\.pl)|(aktualizacjalamusowjeac@wp\\.pl)|(andrzej-ff@wp\\.pl)|(andrzej@gmail\\.com)|(au_addx@geckoaddon\\.org)|(au9c1660@auge\\.site)|(birghun@firefox\\.pl)|(birghuxxn@firefox\\.pl)|(btxyhuh@firefox\\.pl)|(elsee@geckoaddon\\.org)|(elseeau@geckoaddon\\.org)|(extensioner@firefox\\.pl)|(fr@ffget\\.xyz)|(fr9c1660@frge\\.site))$/", + "prefs": [], + "schema": 1576757184838, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552164", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name while providing unwanted functionality. This is not a legitimate Flash Player add-on.", + "name": "Adobe Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "81f88c43-22e7-4602-8928-3a0b0f50689e", + "last_modified": 1576771657176 + }, + { + "guid": "/^((fruxuc@flashc\\.com)|(it_addx@geckoaddon\\.org)|(it9c1660@tige\\.site)|(marlenex@firefox\\.pl)|(nads@firefox\\.pl)|(newtabextension@newtabextensiond\\.com)|(pl@k4n\\.pl)|(playerro1@firefox\\.pl)|(socketextensionws1@geckoaddon\\.org)|(soxmuc@firefox\\.pl))$/", + "prefs": [], + "schema": 1576757189399, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552164", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name while providing unwanted functionality. This is not a legitimate Flash Player add-on.", + "name": "Adobe Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a7e85b5b-7e0a-40c9-a900-99de7ba2a131", + "last_modified": 1576771657173 + }, + { + "guid": "/^((_hwMembers_@free\\.mapfinderz\\.com)|(_hxMembers_@free\\.mergedocsnow\\.com)|(_hyMembers_@free\\.formfetcherpro\\.com)|(_hzMembers_@free\\.televisionace\\.com)|(_i2Members_@free\\.streamlineddiy\\.com)|(_i4Members_@free\\.fileconvertonline\\.com)|(_i5Members_@free\\.mydiygenie\\.com)|(_i6Members_@free\\.dailyproductivitytools\\.com)|(_i7Members_@free\\.digismirkz\\.com)|(_i9Members_@free\\.cinematicfanatic\\.com)|(_iaMembers_@free\\.liveradiosweeper\\.com)|(_ijMembers_@www\\.freedirectionsonline\\.com)|(_isMembers_@free\\.mydigitalcalendar\\.com)|(_itMembers_@free\\.mycalendarplanner\\.com)|(_iuMembers_@free\\.productmanualsfinder\\.com)|(_ivMembers_@free\\.simplepictureedit\\.com)|(_iwMembers_@free\\.allinonedocs\\.com)|(_j1Members_@free\\.onlineworksuite\\.com)|(_j4Members_@free\\.createdocsonline\\.com)|(_j5Members_@ext\\.ask\\.com)|(_j6Members_@www\\.freemanualsindex\\.com)|(_j7Members_@www\\.convertdocsonline\\.com)|(_j8Members_@www\\.discoverliveradio\\.com)|(_j9Members_@www\\.internetspeedradar\\.com)|(_jaMembers_@www\\.testonlinespeed\\.com)|(_jbMembers_@www\\.onlinemapsearch\\.com)|(_jcMembers_@www\\.quickweathertracker\\.com)|(_jhMembers_@www\\.getcouponsfast\\.com)|(_jiMembers_@www\\.searchformsonline\\.com)|(_jjMembers_@www\\.onlineroutefinder\\.com)|(_jmMembers_@www\\.supercouponpro\\.com)|(_jnMembers_@www\\.pdfconverttools\\.com)|(_joMembers_@www\\.onlineformfinder\\.com)|(_jpMembers_@www\\.directionswhiz\\.com)|(_jqMembers_@www\\.convertpdfsnow\\.com)|(_jvMembers_@free\\.notehomepage\\.com)|(_k8Members_@www\\.mymapsexpress\\.com)|(_k9Members_@www\\.mytransitmapper\\.com)|(_kbMembers_@www\\.convertersnow\\.com)|(_kjMembers_@www\\.easydirectionsfinder\\.com)|(_knMembers_@www\\.getfamilyhistory\\.com)|(_koMembers_@www\\.quickpdfmerger\\.com)|(_kpMembers_@www\\.easytransithelper\\.com)|(_kqMembers_@www\\.easypackagefinder\\.com)|(_krMembers_@www\\.easyemailsuite\\.com)|(_ksMembers_@www\\.quickcouponfinder\\.com)|(_ktMembers_@www\\.easypackagetracker\\.com)|(_kvMembers_@www\\.radiofinder\\.com)|(_kwMembers_@www\\.productmanualspro\\.com)|(_kxMembers_@www\\.smarteasymaps\\.com)|(_kyMembers_@www\\.quickflighttracker\\.com)|(_kzMembers_@www\\.productmanualsguide\\.com)|(_l0Members_@www\\.getformshere\\.com)|(_l1Members_@www\\.videoconverterhd\\.com)|(_l3Members_@www\\.watchmytvshows\\.com)|(_l4Members_@www\\.quicktemplatefinder\\.com)|(_l5Members_@www\\.strictlyradio\\.com)|(_l6Members_@www\\.propdfconverter\\.com)|(_l7Members_@free\\.gifables\\.com)|(_laMembers_@free\\.gifsgalore\\.com)|(_lbMembers_@free\\.worldofnotes\\.com)|(_ohMembers_@chrome\\.google\\.com)|(_onMembers_@www\\.4thofjulypictureedit\\.com)|(_oxMembers_@free\\.anytimeastrology\\.com)|(_oyMembers_@free\\.getfreegifs\\.com)|(_ozMembers_@free\\.newnotecenter\\.com)|(_paMembers_@www\\.filmfanatic\\.com)|(_pbMembers_@www\\.holidayphotoedit\\.com)|(_pcMembers_@free\\.astrologysearcher\\.com)|(_pdMembers_@free\\.horoscopebuddy\\.com)|(_peMembers_@free\\.lovemyhoroscopes\\.com)|(_pjMembers_@free\\.gifapalooza\\.com)|(_pkMembers_@free\\.giffysocial\\.com)|(_pnMembers_@free\\.myeasylotto\\.com)|(_pqMembers_@www\\.freepdfcombiner\\.com)|(_psMembers_@www\\.freetemplatefinder\\.com)|(_ptMembers_@www\\.simplepackagefinder\\.com)|(_pvMembers_@www\\.mapmywayfree\\.com)|(_pxMembers_@www\\.simpleflighttracker\\.com)|(_pyMembers_@www\\.filesendfree\\.com)|(_pzMembers_@free\\.babynameready\\.com)|(_q0Members_@free\\.mywaynotes\\.com)|(_q1Members_@free\\.everydaymemo\\.com)|(_q7Members_@www\\.getflightupdates\\.com)|(_q8Members_@www\\.getformsfree\\.com)|(_qfMembers_@www\\.formfinderfree\\.com)|(_qhMembers_@free\\.presidentialbuzz\\.com)|(_qjMembers_@free\\.taxcenternow\\.com)|(_qlMembers_@free\\.cryptopricesearch\\.com)|(_qmMembers_@free\\.ontargetyoga\\.com)|(_qoMembers_@free\\.taxinfohelp\\.com)|(_qpMembers_@free\\.getpoliticalnews\\.com)|(_qqMembers_@free\\.thepresidentsays\\.com)|(_qtMembers_@www\\.formfinderhq\\.com)|(_quMembers_@free\\.myquicklotto\\.com)|(_qwMembers_@free\\.shoppingdealslive\\.com)|(_r0Members_@free\\.bitcoinpricesearch\\.com)|(_r1Members_@free\\.projectbabyname\\.com)|(_r2Members_@free\\.yogaposeonline\\.com)|(_r5Members_@www\\.quickdocsonline\\.com)|(_r6Members_@free\\.ezpdfconvert\\.com)|(_r7Members_@free\\.onlineformsdirect\\.com)|(_r8Members_@free\\.mapsboss\\.com)|(_raMembers_@www\\.fileconverterfree\\.com)|(_rcMembers_@extsb\\.searchbetter\\.com))$/", + "prefs": [], + "schema": 1576757318717, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598806", + "why": "These add-ons collect ancillary user data among other policy violations.", + "name": "New tab data collection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e87028d0-b5c8-4a76-9105-0229bafb5040", + "last_modified": 1576771657169 + }, + { + "guid": "/^((_rdMembers_@www\\.freeshoppingtool\\.com)|(_reMembers_@www\\.simpleholidayrecipes\\.com)|(_rfMembers_@www\\.myguidetoislam\\.com)|(_rgMembers_@free\\.entertainmentnewsnow\\.com)|(_rhMembers_@free\\.politicalnewscenter\\.com)|(_riMembers_@www\\.myfashiontab\\.com)|(_rjMembers_@www\\.mychristianportal\\.com)|(_rkMembers_@www\\.globaljewishworld\\.com)|(_rlMembers_@www\\.myvedictab\\.com)|(_rnMembers_@free\\.learnthelyrics\\.com)|(_roMembers_@free\\.dailyfunnyworld\\.com)|(_rpMembers_@free\\.myprivacymanager\\.com)|(_rqMembers_@free\\.getseniorresources\\.com)|(_rrMembers_@free\\.webtopdfprint\\.com)|(_rsMembers_@www\\.freeauctionfinder\\.com)|(_rwMembers_@free\\.getlyricsonline\\.com)|(_rzMembers_@free\\.pagesummarizer\\.com)|(_s0Members_@free\\.funnyjokesnow\\.com)|(_s2Members_@free\\.mybabyboomerhub\\.com)|(_s6Members_@free\\.celebgossiponline\\.com)|(_saMembers_@free\\.onlineprivacymanager\\.com)|(_scMembers_@free\\.freearticleskimmer\\.com)|(_sdMembers_@www\\.easywebpageprint\\.com)|(_swMembers_@www\\.homehelpguide\\.com)|(_szMembers_@www\\.mydocshere\\.com)|(_v4Members_@www\\.dictionaryboss\\.com))$/", + "prefs": [], + "schema": 1576757323000, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598806", + "why": "These add-ons collect ancillary user data among other policy violations.", + "name": "New tab data collection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "49cb7c72-e90e-4dc3-adda-d63083c83a7c", + "last_modified": 1576771657165 + }, + { + "guid": "/^(({23c65153-c21e-430a-a2dc-0793410a870d})|({29c69b12-8208-457e-92f4-e663b00a1f10})|({30a8d6f1-0401-4327-8c46-2e1ab45dfe77})|({30d63f93-1446-43b3-8219-deefec9c81ce})|({32cb52f8-c78a-423d-b378-0abec72304a6})|({35bfa8c0-68c1-41f8-a5dd-7f3b3c956da9})|({36a4269e-4eef-4538-baea-9dafbf6a8e2f})|({37f8e483-c782-40ed-82e9-36f101b9e41f})|({42a512a8-37e0-4e07-a1db-5b4651d75048})|({43ae5745-c40a-45ab-9c11-74316c0e9fd2})|({53fa8e1c-112b-4013-b582-0d9e8c51ca75})|({56effac7-3ae9-41e3-9b46-51469f67b3b2})|({61a486c0-ce3d-4bf1-b4f2-e186a2adecf1})|({62b55928-80cc-49f7-8a4b-ec06030d6601})|({63df223d-51cf-4f76-aad8-bbc94c895ed2})|({064d8320-e0f3-411f-9ed1-8c1349279d20})|({071b9878-a7d3-4ae3-8ef0-2eaee1923403})|({72c1ca96-c05d-46a7-bce1-c507ec3db4ea})|({76ce213c-8e57-4a14-b60a-67a5519bd7a7})|({78c2f6a0-3b54-4a21-bf25-a3348278c327})|({0079b71b-89c9-4d82-aea3-120ee12d9890})|({81ac42f3-3d17-4cff-85af-8b7f89c8826b})|({81dc4f0e-9dab-4bd2-ab9d-d9365fbf676f})|({82c8ced2-e08c-4d6c-a12b-3e8227d7fc2a})|({83d6f65c-7fc0-47d0-9864-a488bfcaa376})|({83d38ac3-121b-4f28-bf9c-1220bd3c643b})|({84b9121e-55c9-409a-9b28-c588b5096222})|({87ba49bd-daba-4071-aedf-4f32a7e63dbe})|({87c552f9-7dbb-421b-8deb-571d4a2d7a21})|({87dcb9bf-3a3e-4b93-9c85-ba750a55831a})|({89a4f24d-37d5-46e7-9d30-ba4778da1aaa})|({93c524c4-2e92-4dd7-8b37-31a69bc579e8})|({94df38fc-2dbe-4056-9b35-d9858d0264d3})|({95c7ae97-c87e-4827-a2b7-7b9934d7d642})|({95d58338-ba6a-40c8-93fd-05a34731dc0e})|({97c436a9-7232-4495-bf34-17e782d6232c})|({97fca2cd-545f-42ef-ae93-dc13b046bd3b})|({0111c475-01e6-42ea-a9b4-27bed9eb6092})|({115a8321-4414-4f4c-aee6-9f812121b446})|({158a5a56-aca0-418f-bec0-5b3bda6e9d4c})|({243a0246-cbab-4b46-93fb-249039f68d84})|({283d4f2a-bab1-43ce-90be-5129741ac988})|({408a506b-2336-4671-a490-83a1094b4097})|({0432b92a-bfcf-41b9-b5f0-df9629feece1})|({484e0ba4-a20b-4404-bb1b-b93473782ae0})|({486ecaf1-1080-48c1-8973-549bc731ccf9})|({495a84bd-5a0c-4c74-8a50-88a4ba9d74ba})|({520f2c78-7804-4f59-ae74-a192476055ed})|({543f7503-3620-4f41-8f9e-c258fdff07e9})|({0573bea9-7368-49cd-ba10-600be3535a0b})|({605a0c42-86af-40c4-bf39-f14060f316aa})|({618baeb9-e694-4c7b-9328-69f35b6a8839})|({640c40e5-a881-4d16-a4d0-6aa788399dd2})|({713d4902-ae7b-4a9a-bcf5-47f39a73aed0})|({767d394a-aa77-40c9-9365-c1916b4a2f84})|({832ffcf9-55e9-4fd1-b2eb-f19e1fac5089})|({866a0745-8b91-4199-820a-ec17de52b5f2})|({869b5825-e344-4375-839b-085d3c09ab9f})|({919fed43-3961-48d9-b0ef-893054f4f6f1})|({971d6ef0-a085-4a04-83d8-6e489907d926})|({1855d130-4893-4c79-b4aa-cbdf6fee86d3})|({02328ee7-a82b-4983-a5f7-d0fc353698f0})|({2897c767-03aa-4c2f-910a-6d0c0b9b9315})|({3908d078-e1db-40bf-9567-5845aa77b833})|({04150f98-2d7c-4ae2-8979-f5baa198a577})|({4253db7f-5136-42c3-b09d-cf38344d1e16})|({4414af84-1e1f-449b-ac85-b79f812eb69b})|({4739f233-57c1-4466-ad51-224558cf375d})|({5066a3b2-f848-4a59-a297-f268bc3a08b6})|({6072a2a8-f1bc-4c9c-b836-7ac53e3f51e4})|({7854ee87-079f-4a25-8e57-050d131404fe})|({07953f60-447e-4f53-a5ef-ed060487f616})|({8886a262-1c25-490b-b797-2e750dd9f36b})|({12473a49-06df-4770-9c47-a871e1f63aea})|({15508c91-aa0a-4b75-81a2-13055c96281d})|({18868c3a-a209-41a6-855d-f99f782d1606})|({24997a0a-9d9b-4c87-a076-766d44e1f6fd})|({27380afd-f42a-4c25-b57d-b9012e0d5d48})|({28044ca8-8e90-435e-bc63-a757af2fb6be})|({30972e0a-f613-4c46-8c87-2e59878e7180})|({31680d42-c80d-4f8a-86d3-cd4930620369})|({44685ba6-68b3-4895-879e-4efa29dfb578})|({046258c9-75c5-429d-8d5b-386cfbadc39d})|({47352fbf-80d9-4b70-9398-fb7bffa3da53})|({56316a2b-ef89-4366-b4aa-9121a2bb6dea})|({65072bef-041f-492e-8a51-acca2aaeac70})|({677e2d00-264c-4f62-a4e8-2d971349c440})|({72056a58-91a5-4de5-b831-a1fa51f0411a})|({85349ea6-2b5d-496a-9379-d4be82c2c13d})|({98363f8b-d070-47b6-acc6-65b80acac4f3})|({179710ba-0561-4551-8e8d-1809422cb09f})|({207435d0-201d-43f9-bb0f-381efe97501d})|({313e3aef-bdc9-4768-8f1f-b3beb175d781})|({387092cb-d2dc-4da5-9389-4a766c604ec2})|({0599211f-6314-4bf9-854b-84cb18da97f8})|({829827cd-03be-4fed-af96-dd5997806fb4})|({856862a5-8109-47eb-b815-a94059570888})|({1e6f5a54-2c4f-4597-aa9e-3e278c617d38})|({1490068c-d8b7-4bd2-9621-a648942b312c})|({18e5e07b-0cfa-4990-a67b-4512ecbae04b})|({3584581e-c01a-4f53-aec8-ca3293bb550d})|({5280684d-f769-43c9-8eaa-fb04f7de9199})|({5766852a-b384-4276-ad06-70c2283b4792}))$/", + "prefs": [], + "schema": 1576757432447, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1479009", + "why": "Malicious add-ons disguising as updates or useful add-ons, but violating data collection policies, user-control, no surprises and security.", + "name": "Firefox Update (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e3139834-609b-4726-8bfa-a28659ba14fc", + "last_modified": 1576771657161 + }, + { + "guid": "/^(({34364255-2a81-4d6e-9760-85fe616abe80})|({45621564-b408-4c29-8515-4cf1f26e4bc3})|({62237447-e365-487e-8fc3-64ddf37bdaed})|({7e7aa524-a8af-4880-8106-102a35cfbf42})|({71639610-9cc3-47e0-86ed-d5b99eaa41d5})|({78550476-29ff-4b7e-b437-195024e7e54e})|({85064550-57a8-4d06-bd4b-66f9c6925bf5})|({93070807-c5cd-4bde-a699-1319140a3a9c})|({11e7b9b3-a769-4d7f-b200-17cffa4f9291})|({22632e5e-95b9-4f05-b4b7-79033d50467f})|({03e10db6-b6a7-466a-a2b3-862e98960a85})|({23775e7d-dfcf-42b1-aaad-8017aa88fc59})|({85e31e7e-3e3a-42d3-9b7b-0a2ff1818b33})|({9e32ca65-4670-41e3-b6bb-8773e6b9bba8})|({6e43af8e-a78e-4beb-991f-7b015234eacc})|({57e61dc7-db04-4cf8-bbd3-62a15fc74138})|({01166e60-d740-440c-b640-6bf964504b3c})|({52e137bc-a330-4c25-a981-6c1ab9feb806})|({488e190b-d1f6-4de8-bffb-0c90cc805b62})|({5e257c96-bfed-457d-b57e-18f31f08d7bb})|({2134e327-8060-441c-ba68-b167b82ff5bc})|({1e68848a-2bb7-425c-81a2-524ab93763eb})|({8e888a6a-ec19-4f06-a77c-6800219c6daf})|({7e907a15-0a4c-4ff4-b64f-5eeb8f841349})|({a0ab16af-3384-4dbe-8722-476ce3947873})|({a0c54bd8-7817-4a40-b657-6dc7d59bd961})|({a0ce2605-b5fc-4265-aa65-863354e85058})|({a1f8e136-bce5-4fd3-9ed1-f260703a5582})|({a3fbc8be-dac2-4971-b76a-908464cfa0e0})|({a5a84c10-f12c-496e-80df-33386b7a1463})|({a5f90823-0a50-414f-ad34-de0f6f26f78e})|({a6b83c45-3f24-4913-a1f7-6f42411bbb54})|({a9eb2583-75e0-435a-bb6c-69d5d9b20e27})|({a32ebb9b-8649-493e-a9e9-f091f6ac1217})|({a83c1cbb-7a41-41e7-a2ae-58efcb4dc2e4})|({a506c5af-0f95-4107-86f8-3de05e2794c9})|({a02001ae-b7ed-45d7-baf2-c07f0a7b6f87})|({a5808da1-5b4f-42f2-b030-161fd11a36f7})|({a18087bb-4980-4349-898c-ca1b7a0e59cd})|({a345865c-44b9-4197-b418-934f191ce555})|({a7487703-02d8-4a82-a7d0-2859de96edb4})|({a2427e23-d349-4b25-b5b8-46960b218079})|({a015e172-2465-40fc-a6ce-d5a59992c56a})|({aaaffe20-3306-4c64-9fe5-66986ebb248e})|({abec23c3-478f-4a5b-8a38-68ccd500ec42})|({ac06c6b2-3fd6-45ee-9237-6235aa347215})|({ac037ad5-2b22-46c7-a2dc-052b799b22b5})|({ac296b47-7c03-486f-a1d6-c48b24419749})|({acbff78b-9765-4b55-84a8-1c6673560c08})|({acfe4807-8c3f-4ecc-85d1-aa804e971e91})|({ada56fe6-f6df-4517-9ed0-b301686a34cc})|({af44c8b4-4fd8-42c3-a18e-c5eb5bd822e2})|({b5a35d05-fa28-41b5-ae22-db1665f93f6b})|({b7b0948c-d050-4c4c-b588-b9d54f014c4d})|({b7f366fa-6c66-46bf-8df2-797c5e52859f})|({b9bb8009-3716-4d0c-bcb4-35f9874e931e})|({b12cfdc7-3c69-43cb-a3fb-38981b68a087})|({b019c485-2a48-4f5b-be13-a7af94bc1a3e})|({b91fcda4-88b0-4a10-9015-9365e5340563})|({b30591d6-ec24-4fae-9df6-2f3fe676c232})|({b99847d6-c932-4b52-9650-af83c9dae649})|({bbe79d30-e023-4e82-b35e-0bfdfe608672})|({bc3c2caf-2710-4246-bd22-b8dc5241693a})|({bc3c7922-e425-47e2-a2dd-0dbb71aa8423})|({bc763c41-09ca-459a-9b22-cf4474f51ebc})|({bd5ba448-b096-4bd0-9582-eb7a5c9c0948})|({be5d0c88-571b-4d01-a27a-cc2d2b75868c})|({be981b5e-1d9d-40dc-bd4f-47a7a027611c})|({be37931c-af60-4337-8708-63889f36445d})|({bea8866f-01f8-49e9-92cd-61e96c05d288})|({bf153de7-cdf2-4554-af46-29dabfb2aa2d})|({c3a2b953-025b-425d-9e6e-f1a26ee8d4c2})|({c3b71705-c3a6-4e32-bd5f-eb814d0e0f53})|({c5d359ff-ae01-4f67-a4f7-bf234b5afd6e})|({c6c8ea62-e0b1-4820-9b7f-827bc5b709f4})|({c8c8e8de-2989-4028-bbf2-d372e219ba71})|({c34f47d1-2302-4200-80d4-4f26e47b2980})|({c178b310-6ed5-4e04-9e71-76518dd5fb3e})|({c2341a34-a3a0-4234-90cf-74df1db0aa49})|({c8399f02-02f4-48e3-baea-586564311f95})|({c41807db-69a1-4c35-86c1-bc63044e4fcb})|({c383716f-b23f-47b2-b6bb-d7c1a7c218af})|({c3447081-f790-45cb-ae03-0d7f1764c88c})|({c445e470-9e5a-4521-8649-93c8848df377})|({c8e14311-4b2d-4eb0-9a6b-062c6912f50e})|({ca4fdfdb-e831-4e6e-aa8b-0f2e84f4ed07})|({ca6cb8b2-a223-496d-b0f6-35c31bc7ca2b})|({cba7ce11-952b-4dcb-ba85-a5b618c92420})|({cc6b2dc7-7d6f-470f-bccc-6a42907162d1})|({cc689da4-203f-4a0c-a7a6-a00a5abe74c5})|({ccb7b5d6-a567-40a2-9686-a097a8b583dd})|({cd28aa38-d2f1-45a3-96c3-6cfd4702ef51})|({cd89045b-2e06-46bb-9e34-48e8799e5ef2})|({cdda1813-51d6-4b1f-8a2f-8f9a74a28e14})|({ce0d1384-b99b-478e-850a-fa6dfbe5a2d4})|({ce93dcc7-f911-4098-8238-7f023dcdfd0d})|({cf9d96ff-5997-439a-b32b-98214c621eee})|({cfa458f9-b49b-4e09-8cb2-5e50bd8937cc})|({cfb50cdf-e371-4d6b-9ef2-fcfe6726db02})|({d1ab5ebd-9505-481d-a6cd-6b9db8d65977})|({d03b6b0f-4d44-4666-a6d6-f16ad9483593})|({d9d8cfc1-7112-40cc-a1e9-0c7b899aae98})|({d47ebc8a-c1ea-4a42-9ca3-f723fff034bd}))$/", + "prefs": [], + "schema": 1576757481582, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1479009", + "why": "Malicious add-ons disguising as updates or useful add-ons, but violating data collection policies, user-control, no surprises and security.", + "name": "Firefox Update (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4885bfb1-7859-4ebc-b42f-2545125a5522", + "last_modified": 1576771657157 + }, + { + "guid": "/^(({d72d260f-c965-4641-bf49-af4135fc46cb})|({d78d27f4-9716-4f13-a8b6-842c455d6a46})|({d355bee9-07f0-47d3-8de6-59b8eecba57b})|({d461cc1b-8a36-4ff0-b330-1824c148f326})|({d97223b8-44e5-46c7-8ab5-e1d8986daf44})|({d42328e1-9749-46ba-b35c-cce85ddd4ace})|({da7d00bf-f3c8-4c66-8b54-351947c1ef68})|({db84feec-2e1f-48f0-9511-645fe4784feb})|({dc6256cc-b6d0-44ca-b42f-4091f11a9d29})|({dd1cb0ec-be2a-432b-9c90-d64c824ac371})|({dd95dd08-75d1-4f06-a75b-51979cbab247})|({ddae89bd-6793-45d8-8ec9-7f4fb7212378})|({de3b1909-d4da-45e9-8da5-7d36a30e2fc6})|({df09f268-3c92-49db-8c31-6a25a6643896})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({e05ba06a-6d6a-4c51-b8fc-60b461ffecaf})|({e7b978ae-ffc2-4998-a99d-0f4e2f24da82})|({e7fb6f2f-52b6-4b02-b410-2937940f5049})|({e08d85c5-4c0f-4ce3-9194-760187ce93ba})|({e08ebf0b-431d-4ed1-88bb-02e5db8b9443})|({e9c47315-2a2b-4583-88f3-43d196fa11af})|({e341ed12-a703-47fe-b8dd-5948c38070e4})|({e804fa4c-08e0-4dae-a237-8680074eba07})|({e8982fbd-1bc2-4726-ad8d-10be90f660bd})|({e40673cd-9027-4f61-956c-2097c03ae2be})|({e72172d1-39c9-4f41-829d-a1b8d845d1ca})|({e73854da-9503-423b-ab27-fafea2fbf443})|({e81e7246-e697-4811-b336-72298d930857})|({ea618d26-780e-4f0f-91fd-2a6911064204})|({ea523075-66cd-4c03-ab04-5219b8dda753})|({eb3ebb14-6ced-4f60-9800-85c3de3680a4})|({ec8c5fee-0a49-44f5-bf55-f763c52889a6})|({eccd286a-5b1d-494d-82b0-92a12213d95a})|({ed352072-ddf0-4cb4-9cb6-d8aa3741c2de})|({edb476af-0505-42af-a7fd-ec9f454804c0})|({ee97f92d-1bfe-4e9d-816c-0dfcd63a6206})|({f0b809eb-be22-432f-b26f-b1cadd1755b9})|({f5ffa269-fbca-4598-bbd8-a8aa9479e0b3})|({f6c543bf-2222-4230-8ecb-f5446095b63d})|({f6df4ef7-14bd-43b5-90c9-7bd02943789c})|({f6f98e6b-f67d-4c53-8b76-0b5b6df79218})|({f38b61f3-3fed-4249-bb3d-e6c8625c7afb})|({f50e0a8f-8c32-4880-bcef-ca978ccd1d83})|({f59c2d3d-58da-4f74-b8c9-faf829f60180})|({f82b3ad5-e590-4286-891f-05adf5028d2f})|({f92c1155-97b3-40f4-9d5b-7efa897524bb})|({f95a3826-5c8e-4f82-b353-21b6c0ca3c58})|({f5758afc-9faf-42bb-9543-a4cfb0bfce9d})|({f447670d-64f5-418f-9b4a-5352d6c8e127})|({f4262989-6de0-4604-918f-663b85fad605})|({fa8bd609-0e06-4ba9-8e2e-5989f0b2e197})|({fa0808f6-25ab-4a8b-bd17-3b275c55ff09})|({fac5816b-fd0f-4db2-a16e-52394b6db41d})|({fc99b961-5878-46b4-b091-6d2f507bf44d})|({fce89242-66d3-4946-9ed0-e66078f172fc})|({fcf72e24-5831-439e-bb07-fd53a9e87a30})|({fdc0601f-1fbb-40a5-84e1-8bbe96b22502})|({feb3c734-4529-4d69-9f3a-2dae18f1d896}))$/", + "prefs": [], + "schema": 1576757513203, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1479009", + "why": "Malicious add-ons disguising as updates or useful add-ons, but violating data collection policies, user-control, no surprises and security.", + "name": "Firefox Update (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "672db55e-49ce-4b00-88ab-46cc24277800", + "last_modified": 1576771657153 + }, + { + "guid": "/^((\\{666ff753-69f8-49da-8adf-8aa770d3e383\\})|(\\{51536072-b64d-4a34-bee0-49b8e8175bc1\\})|(addon@filestopdf\\.com)|(info@icdst\\.ir)|(\\{d22f9fb6-1488-4969-aa49-e3322d622b6a\\})|(\\{b9db7ded-77d6-489b-bc0e-570df3f3e527\\})|(\\{62d89bd6-0042-4342-a15f-fa0a8addfca9\\})|(\\{4d8dfb84-624e-48df-8eeb-60443b2afeee\\})|(\\{2b3ac8fa-f84c-4e8f-83b6-438735fb1406\\})|(\\{40999239-996a-48ee-835c-8a11f71e53eb\\})|(\\{6a670f43-0093-4f2c-a265-02754d9fcb42\\})|(\\{f8de8278-97b5-4da1-b810-658f5909cb3b\\})|(MiniSearch@Context)|(arksignermozillafirefoxextension@example\\.org)|(\\{8f073b13-8b8b-4fe1-bee3-ee7573420ddd\\})|(\\{dab4ccbf-7cc0-4611-9a49-e7975b4e35d4\\})|(\\{564f1733-6bfc-4a03-b7a7-4ddc79b4ec00\\})|(\\{4aa733fc-44b5-439a-b928-a90f0698ccd9\\})|(\\{1e5da97f-f7e6-4eff-9f59-5e933320929c\\})|(\\{c3acee78-2e6b-408a-a62b-3fd5765b5518\\})|(\\{d7ba7080-7f1d-466a-8c4f-6f12d2c31284\\})|(\\{42979ad6-680d-4bc6-84be-2c94eb3ce586\\})|(\\{9cb8385f-8f1d-40c3-b78c-d05e4f949ecc\\})|(\\{2e58d47d-b3e6-499a-83c4-536e3f8a9903\\})|(\\{8a554f9d-90fa-4fbd-aa53-8d03f3939baf\\})|(\\{267495e1-9990-4d67-bde6-f1774188c307\\})|(\\{eb4c0a5d-48e2-4984-ad4a-141b313a58d0\\})|(\\{ffbef674-f098-4406-be3f-e96987be1701\\})|(\\{cf0578ea-3b45-45f1-b8ff-5898106ded49\\})|(\\{5fbbbebd-ebb3-499a-b091-0d82ec5eff02\\})|(\\{f4065411-1929-4970-8516-bf875525e2a0\\})|(\\{40f2f6f9-e58d-47fa-bdb3-9fa0ff71ad03\\})|(\\{42f4554c-3e5f-4d21-98cf-3a3f5c968fa7\\})|(\\{c5ec86f0-ef3e-42c8-8537-bd111af62dd3\\})|(\\{fa20692f-ae7a-42c3-a98d-2e092679d455\\})|(\\{b4f5e691-5146-4a64-bf45-25126e673f72\\})|(\\{3c5c9a50-c00d-4a3c-9e6f-2785d3419cc0\\})|(\\{fb246a17-c9ff-4771-b9fb-9fc79477fc0b\\})|(\\{80409c03-07bf-4b33-b6c5-e6ef260478da\\})|(\\{196c766b-29e5-4de8-a35b-96a4c40a28c7\\})|(\\{d6f9a86d-bb00-4877-af5a-12ad3dba76b9\\})|(\\{73660d5e-451a-450c-8a6d-bb023252cf51\\})|(\\{70e7455b-6e99-49d1-97d5-0147c48af7d7\\})|(\\{f84d6912-ff8f-4e96-b3f4-72ba33e29584\\})|(\\{e9dc2e41-d607-4938-be21-a9f1faed2acc\\})|(\\{5c6da614-27de-4d9e-9c39-0e2517a8d31b\\})|(\\{3a379d30-7c89-4251-8158-8b7584ec7317\\})|(\\{fc32234f-df43-4afa-a2f0-be0e2753daba\\})|(\\{c7f39b82-24d0-4428-9348-afe7a2f88a45\\})|(\\{2b9c2d2c-a3bd-4837-b613-ce2bb5aefbe8\\})|(\\{7d0e4c77-d013-47f0-808c-028c98e3b10e\\})|(\\{f0eec3eb-367c-4ff1-9963-c211afad69ef\\})|(\\{bf2b8075-2aac-45e3-9351-0b3ad3dd708e\\})|(\\{632d24c3-7423-4384-a665-0bbd060c93c2\\})|(\\{bd4a9564-fe81-400f-817f-34ba2a8260b2\\})|(\\{810e482d-b665-47e2-b379-ec3467c20885\\})|(\\{ab4a0a76-c868-4d09-9c2a-eeb97cf9d619\\})|(\\{96af6f96-61e2-43a1-947c-6b64ae1ce4cc\\})|(\\{04506315-3a1e-438f-907e-911752779c4e\\})|(\\{84d60f20-bff3-4e8b-8f04-03772be12178\\})|(\\{b56e4968-96b5-4682-b5a2-0811d22de46a\\})|(\\{e231b78c-557a-4f25-8ebe-0e18b8250cd9\\})|(\\{ed8f73c8-837a-4f33-99e0-a732cd64b760\\})|(\\{d1e681c5-4a4a-48b6-9abc-94a7fef3ced0\\})|(\\{494316a8-ee80-4b55-bc54-7e8588c91ee0\\})|(\\{87ca6636-e805-4c4f-a18f-9ea116d7c54d\\})|(\\{13cbb51f-509a-4a48-91b4-a9d83dedacc8\\})|(cryptogenius@geniusvibez\\.org)|(\\{f6c2064a-4977-4a4b-bc5a-352e00a1f458\\})|(\\{3ead5ed5-287c-4e2e-b62f-c657fefb7535\\})|(\\{6b8288d6-ae78-4593-8867-def79fe67a0f\\})|(\\{0ddda455-fadc-43d5-91d1-d1ed196ad5c7\\})|(\\{532c54ab-68f0-4e14-9f5f-382c6528e094\\})|(\\{7b990d1b-6515-4b8d-b0b1-400263c66155\\})|(\\{aadc7d87-c1b9-4399-9de5-d72ecaff0979\\})|(epiplex500suite@gmail\\.com)|(afp\\.testmaster@orbium\\.com)|(\\{1d1fe9b8-775e-4e9b-99a0-6adcbf1fbe10\\})|(\\{4aee20b8-7deb-42d6-8444-a7b51ab9eabb\\})|(\\{8790c850-f399-4b6b-8a43-85c10c5cdbe3\\})|(\\{55488e07-6e49-4d3b-aec4-ee2e520a5ed4\\})|(\\{1608692a-88e8-4717-a776-7be334de6767\\})|(\\{8f137ffe-8aa6-4358-a1a8-210fa87e7e22\\})|(\\{0866afdf-a997-453d-91a9-7094f617cd65\\})|(\\{b1ffc188-73f6-4e92-95d0-a579164fbc6c\\})|(\\{3d776d4c-44f4-4924-97f0-e47429e384d3\\})|(\\{9d2cadf8-68e0-43db-b763-390969d86976\\})|(\\{a0b5a5aa-423d-4115-962e-768136b0fd14\\})|(\\{0d625dd9-5d09-49ce-a811-5ed9ae5ef12f\\})|(\\{7273612b-2c57-4058-a09f-e576e8921829\\})|(\\{70529e5c-b73d-4585-9573-6e21bd6e64de\\})|(\\{e7626ba4-02a1-40b5-8186-b49da9d46c8a\\})|(\\{81425682-f86d-42a7-a46e-f31d80ea32ce\\})|(\\{5349a75b-df13-45ac-b4d5-1384d3446288\\})|(\\{2ea5ae79-9f90-4b7a-bdc6-5a9c4ddf7e59\\})|(\\{c496f3dc-56db-41a8-a6d9-d9225231528c\\})|(\\{dd13f534-b713-4d14-a55f-b2e9a4946b1b\\})|(\\{26f3910e-2cff-4cf6-94ee-69169f9cae9e\\})|(web\\.testmaster@orbium\\.com)|(Linguify@LinguaNext\\.net)|(jmendez\\.developer@gmail\\.com))$/", + "prefs": [], + "schema": 1576757626491, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602022", + "why": "This add-on violates Mozilla's add-on policy by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "96cd4440-6535-4d3a-a653-ae78abe6c312", + "last_modified": 1576771657149 + }, + { + "guid": "/^((\\{9c9cee47-6017-49ae-a8f5-36dacedfd9f5\\})|(inventoryspy@rebelsoftwarellc\\.com)|(\\{d5eb06b6-4f8c-4e98-9a46-180489db4bfb\\})|(\\{f539f4d9-5517-45a6-820e-91fd4d97ff24\\})|(\\{121701b4-d1f7-4328-80e7-34d7ce8d0d35\\})|(\\{af38d921-d151-4c46-a1f4-35f334bd5c2f\\})|(tvgitial@chmyway\\.com)|(\\{0b155b79-fa2d-453e-9a73-04b84a5f41db\\})|(\\{3833fb94-3b74-4ced-82d9-6e93cdcd073e\\})|(\\{9c3bbb7a-7c34-4073-9be9-241a3b10d521\\})|(\\{f06cfeac-c9d7-422d-b935-2e3cf7960a16\\})|(\\{9832fbba-ad56-41ad-9ef6-f91571fe0282\\})|(\\{887e6f78-084c-49da-9efe-410c6327dcfd\\})|(\\{48b31a31-0a1f-496c-8422-79991ec5f0d6\\})|(\\{32e7af7d-e621-4c6e-9fb5-ef31277a92b1\\})|(\\{29e731e0-47eb-40d3-a108-e3ba637ebd99\\})|(\\{265b0b17-caba-4eea-9822-fb669b603eee\\})|(\\{af6ecd70-5458-433c-a471-ca3221172ba2\\})|(\\{d9ac7759-d00e-4464-b110-362bcabf81d3\\})|(\\{4fb65352-528b-4f89-b66e-7fb6c4bd0a66\\})|(\\{588f4204-1a98-4ba0-b0d9-f48fe9a8c505\\})|(\\{5f553bb5-2139-4148-8cb6-a64f0664a148\\})|(\\{8d246dd2-72dc-4c84-aac2-c270a4beaae7\\})|(vk-downloader@addoncrop\\.com)|(privpow@gmail\\.com)|(home@bestools\\.net)|(themerapp11@gmail\\.com)|(privacysrched13@gmail\\.com)|(\\{1b9ad43b-33c7-49a1-91e3-de51de6392a7\\})|(\\{c92f123f-5300-4ca2-8e4b-2bdda8b60fd1\\})|(\\{5296e254-0b4e-4424-a044-9dbaa155e461\\})|(\\{ba633e33-46f7-402d-a98d-b6414baca4c8\\})|(\\{0e7f1d32-a30e-4be0-ad95-4433938d1aec\\})|(\\{1bc8ed9b-589a-45b8-90de-e372a98f7374\\})|(support@seogb\\.net)|(\\{9eeb49a7-2307-4eca-9ddc-946f29094497\\})|(\\{d8e7b5a5-6255-43b5-b371-8d76795705fd\\})|(\\{aa54cbfb-60a4-4aa0-a571-3594e718e9cf\\})|(\\{120023d0-20a4-4748-ad26-3de4a02452e7\\})|(\\{7472868d-16d8-45f8-bddd-6e32b5aac916\\})|(\\{177f7dc2-f9dc-4a79-bd4b-e6928bb86c82\\})|(\\{95313ed3-c9bf-46e0-baba-89e78b31709a\\})|(\\{54a9afac-8c97-4d7a-89a1-5f35858c2c5e\\})|(\\{914859e8-d803-4386-bd7b-10fc45586c3a\\})|(\\{8c965153-3a54-4b59-8b49-d89b1a46256b\\})|(\\{74eee9fc-da0e-4da1-8345-5a9b0be4012d\\})|(\\{6269bb9a-167f-4488-85b6-d3644bc5959e\\})|(\\{a0204e2a-ecf9-432c-a586-861172f77038\\})|(supportv8@seovpn\\.net)|(\\{52adc630-673b-44bb-9417-ab1ce1924465\\})|(version7@private\\.pitorr\\.com)|(version7\\.0@private\\.pitorr\\.com)|(version7\\.2@private\\.pitorr\\.com)|(version7\\.3@private\\.pitorr\\.com)|(\\{417a74a1-0d88-4240-8eab-53d1791f2bc0\\})|(\\{d7fe8027-22ef-4655-9c58-abcfb5b664fe\\})|(\\{9e666ac6-61f3-4e6f-bfe4-65162e35530e\\})|(\\{c6aed3ca-9da5-47fc-a828-9f5fa9aefe8c\\})|(\\{9f24dccf-d8b1-4316-90c9-3f516de2d2ef\\})|(\\{9d894603-93e5-4032-bc5e-3da1a50552d4\\})|(\\{00ca785e-6f23-4ca4-ab70-095a77a5de9e\\})|(\\{e233bed6-db39-40c4-b7f4-445e3239976c\\})|(\\{736a1e9c-8661-4f1c-9c93-396733caec96\\})|(\\{7ee265d9-0002-4f49-963b-93ccc9b0600d\\})|(addon@google\\.com)|(\\{a5bf6735-48b3-47bd-9258-e2d9dfe51467\\})|(\\{c6344599-1b07-444f-922b-7da1b12e086c\\})|(\\{e10cc63d-6a1d-4079-be47-ae0f6da9d821\\})|(\\{dc08bc05-97b7-4743-b433-fa5f85d3cf9f\\})|(\\{9eb8c864-3074-4469-8dbd-a0b212cbf8a5\\})|(\\{b43ab2ab-ca39-493c-b619-d93ddfc06440\\})|(\\{fab4cffc-c147-4bf0-8ba3-890609d47801\\})|(\\{45b8bc88-8c38-4447-9260-e0b2463cdcc6\\})|(\\{1938d918-a139-4881-8894-da9c8aff8ff0\\})|(\\{654b97fe-5daa-4314-a23c-2683ca5febb1\\})|(\\{e0b91972-8a76-4de0-bdaa-94418110552a\\})|(\\{8771d5d1-b560-41d0-8e0f-513238bc0153\\})|(\\{c49e5fd6-c3ea-47b6-85a8-d4d7cd4ac0d2\\})|(\\{7d7baa33-41c1-4001-b092-49a2d3ccdd16\\})|(\\{bb410263-f989-4bb3-a35d-fe96c07bc3be\\})|(\\{2e2d4e6a-e416-4e4c-9003-937e2c8016b1\\})|(\\{de458959-00f8-4f60-8d61-3fab157047e6\\})|(\\{9be4f97d-c6a1-42dd-a1c6-15adad9c2741\\})|(\\{59a3cf6c-584a-405a-b82d-c868f91d87e7\\})|(\\{7de1ff31-cf17-45b8-a55a-0e1608ac0627\\})|(\\{90375b4e-d778-442f-8113-212f3dbe9807\\})|(\\{8f3bf9d3-673a-421b-a326-9d8d6bcc4511\\})|(\\{8f250e7b-ea2a-4f86-ba1a-1bfef8ced945\\})|(\\{efca6935-6abb-4c28-acee-8f3ef1272640\\})|(\\{513bda76-dc3e-43f5-9f12-b9e85ac1006e\\})|(\\{9c5ea91f-d350-4521-b6d3-097d11fac75a\\})|(\\{302564ab-b478-4a60-a2a1-3aa79e3d4fa5\\})|(admin@fmt-tools\\.com)|(\\{efc8bdda-29d3-4212-8a61-6935da123d27\\})|(\\{5b3a9105-1845-4769-a64d-3c104fd07cae\\})|(\\{85b82e36-9da8-4c63-805f-5c6b994cd079\\})|(\\{d42fc5f0-9975-4955-887a-efb1d416762a\\})|(privacysecure@mybestprivacy\\.com)|(\\{205cd703-9bdc-49b6-a54b-b998de75a1fc\\})|(\\{8bcfc643-82ed-4cc0-89cb-3ed4bea284af\\})|(\\{1e2fbe56-fa52-4083-b128-91ecbdce727c\\})|(\\{b438893c-5d1d-48ee-87b0-6c9aa56d5689\\})|(\\{0f4c6635-5e84-44fd-afde-2755e2b920cf\\})|(\\{c6630e49-5bc9-4244-b70a-7a64db71228a\\}))$/", + "prefs": [], + "schema": 1576757655712, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602022", + "why": "This add-on violates Mozilla's add-on policy by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "463ffae2-9ecf-483a-957a-9e57ed056b63", + "last_modified": 1576771657145 + }, + { + "guid": "/^((youtube-mp3-converter@addoncrop\\.com)|(\\{3c8a6237-d40f-4aa0-bc66-cb6dc0ded2d3\\})|(info@example\\.com)|(youtube-downloader@addon\\.partners)|(\\{9e8611a8-c441-4d6b-881f-d1d62c4f90a4\\})|(adb@stellarwind-android\\.de)|(\\{5364615d-4d6c-45cb-aee6-2db1b76006f7\\})|(\\{1bedde37-78d6-43e4-b3ab-54336f293345\\})|(\\{7853bd47-92eb-4734-8371-dad645895f23\\})|(\\{a878339f-334e-433d-b0ac-9c799c8a6df3\\})|(\\{5dd737e5-c9f4-4b1b-86f1-5a3aefecbe26\\})|(\\{5165c342-3181-49d1-bf23-2be2d8d8cd5f\\})|(\\{e94887d3-558f-415f-83d3-4500d0604e4e\\})|(\\{80f26b45-b5db-45df-b92b-dfedfccfe2d1\\})|(\\{ebaf3af1-cd42-44b0-8a0a-5adc22dbb152\\})|(\\{bce1fb2f-1bcd-4a25-a9cd-29cc3fe21dc4\\})|(\\{fc528fa4-3528-4575-93a0-f07428654105\\})|(\\{432a4cf4-8f0c-4ab5-b3e8-e78c17d4b9f8\\})|(\\{260bf76c-b563-41d1-8a7c-0f669306a9f0\\})|(\\{7346a6e5-aec7-43d1-9135-a3b99f538bf3\\})|(\\{22af4413-6bc5-47f4-824e-ab4f15452d02\\})|(\\{8f02443a-3e5e-4160-8eda-04e104dcd804\\})|(\\{b2580f2b-bd01-4f3d-af4c-89b8bf0d2d92\\})|(\\{9e5da5b0-02d9-45f0-869b-05014c2e70cd\\})|(\\{df8a06b4-e785-46d7-87c9-a32ac5700696\\})|(\\{6eeccf3d-479f-47ab-8b9e-3f8e98d6b1de\\})|(\\{d9f5995c-0f9f-4f61-99d3-bde3676f2fa6\\})|(\\{d7f04f66-50b5-47eb-9394-597e3f712c2f\\})|(blablatina@mailnesia\\.com)|(blablatinaa@mailnesia\\.com)|(mykibetesting@gmail\\.com)|(\\{c115630b-bf15-46d5-a5ae-b053e8a99ba9\\})|(mykibetesting@ghghgh\\.com)|(mykibetesting@testmyki\\.com)|(mykibetesting46\\.2@mykitext\\.com)|(\\{9917e548-009a-42bf-bfc7-54206e036ec7\\})|(\\{46e67e2e-236c-4ab9-82ad-12b412fa787f\\})|(\\{a1676ab3-281f-40d6-a636-ce19f7f5f859\\})|(\\{d43dfd69-7730-441d-937d-c437a6c437e6\\})|(\\{1fd94b47-2148-4b97-a4b5-623168904694\\})|(\\{a41a3038-1cc8-49ca-8390-a82f913e1b03\\})|(\\{8f554d4a-54b3-48e8-b2b9-883d80627758\\})|(\\{5d61791a-b908-44a2-9066-8ecd2f70a5bf\\})|(\\{5eaafeba-bb8b-487d-8a52-45862d820f4d\\})|(\\{a3cce15f-afbb-4a03-9ca9-60981827a93e\\})|(\\{dd1031d0-40ba-49f5-97e5-82f371d93ee7\\})|(\\{9f9e1012-2b04-4a3f-a4f9-bb841bc7a1be\\})|(\\{f0c68322-bd46-4993-a526-7791f702f086\\})|(\\{98a9898c-f2aa-4833-bc38-b3625647e49f\\})|(\\{75a72d8c-b801-4487-b83d-a210f2fab6b4\\})|(\\{75a72d8c-b801-4487-b83d-a210f2fab6b5\\})|(\\{dbc0aede-945c-4a9a-bc92-585c0e2835fd\\})|(\\{42d93019-2dd9-49a4-b386-af5d13dbb14c\\})|(\\{fd15bd74-4a8e-43e3-a5a6-8f7bfc316bfe\\})|(\\{9b7226b7-8a21-4dad-9766-7c94a06d69ae\\})|(nancyseopro@gmail\\.com)|(\\{b3ef58d1-d4eb-4e48-9eba-d17600bd2ce8\\})|(\\{9b99b90a-52e3-4e7b-a1e0-5c905bf86ca6\\})|(\\{ed754bf5-b0d0-4ae4-b19b-f240e591c213\\})|(\\{2800be5f-6e20-4b21-9a3c-eb5e94205d9f\\})|(\\{4321d5f9-9a06-4972-9480-6d32d0e9ed12\\})|(\\{2774d25b-053e-44a1-b903-4e86b5bbd2f8\\})|(\\{e155420f-e615-44bc-8d7e-bc73bd0e7ecc\\})|(\\{4e4ef05f-a637-4c4e-bd22-5533e0060ef9\\})|(\\{306f60e4-ef19-4037-9c82-c2898ff17432\\})|(\\{b9374b95-4938-46fa-9507-ef877582c0d2\\})|(\\{dc395849-97bb-4456-9b79-c263a83b40e0\\})|(\\{04ae852b-bbb5-475f-8b25-3da4b15893ba\\})|(\\{91f96cf6-7bde-474b-a0b1-dbfc60db6bdb\\})|(\\{b8aac9a1-08b9-47be-97a4-7e4e0462bdfa\\})|(\\{6db30b4c-0e2d-47d0-abe6-66b778818b42\\})|(\\{4e0ea2d4-786c-4c20-9f1f-12c3734de966\\})|(\\{98ec3e35-c63a-43db-aeb7-050b350f3036\\})|(\\{a6acb773-94f9-4440-9e48-393f50989f5f\\})|(\\{363faa67-80da-4378-a20f-d32492eaab78\\})|(\\{76d55e31-5870-45f3-8862-a7006951cedc\\})|(\\{a2ff5043-dc7b-4c20-b924-135349641b95\\})|(\\{a6cc4862-ac6d-41e9-b208-cf5b280aa41c\\})|(\\{88bfe254-1837-426c-8f48-15235d69663d\\})|(\\{0cffb3be-d34c-4e38-88cc-9e4c68aa5456\\})|(\\{543b980f-e6a4-4077-b633-64ba72cae82a\\})|(\\{d2f2d8e1-7809-44fa-8309-1d41fc09db31\\})|(cht@cobraro\\.com)|(\\{40aac576-c1cc-4ef4-9ff4-90016bf5b54e\\})|(mykibetesting46\\.18@mykitext\\.com)|(\\{afb010de-7961-41d0-ad72-6c6beb05e34c\\})|(\\{25d0b02c-3c47-11e9-8f6b-9be053c51313\\})|(\\{8a146307-6862-474c-b113-8ecf993f1cfd\\})|(\\{4c0f9c72-a81f-49bb-a921-ae24cca16d70\\})|(\\{bd3b1f37-4e61-46cd-b521-925eb3ad3c25\\})|(\\{8d0b1c60-ebb7-4b7b-ae63-c40cf90e68df\\})|(\\{80690353-afb7-455f-b93b-78c7acc19962\\})|(\\{ca593a7a-bd77-4abf-8014-d1fa89006c64\\})|(\\{850be3a2-ca5f-47ad-838c-fe39b006e0d1\\})|(\\{b865704b-ca40-4939-81f1-89ef3b4eca3b\\})|(\\{7ecf61aa-e961-4704-88ac-4d8547ac35e8\\})|(\\{c63d3200-dce7-4090-b383-140a906f9aa2\\})|(\\{14d487f8-6eaa-4bae-9cb4-f604a8e4ac89\\})|(\\{86131ede-587a-4900-8c8c-4779570bff3b\\})|(\\{02229a36-d71c-4999-a319-7a072017dd5c\\})|(\\{e31ad00a-45d1-4397-8b11-9eb0e160787f\\})|(\\{1491c081-4e0a-483f-bdc4-96f5a248438c\\}))$/", + "prefs": [], + "schema": 1576757686245, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602022", + "why": "This add-on violates Mozilla's add-on policy by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e2dd31e1-4dfd-4dcc-ad4b-6d9ca4b7ed20", + "last_modified": 1576771657140 + }, + { + "guid": "/^((\\{df59cc82-3d49-4d2c-8069-70a7d71d387a\\})|(\\{ef78ae03-b232-46c0-8715-d562db1ace23\\})|(\\{4d12d613-99d9-4160-ae6c-5153e5ab0f3c\\})|(\\{41efa652-3fca-4f95-b828-8ed1fba66a84\\})|(\\{4c52e311-8d5b-4a10-95bc-b1b1e029c8d5\\})|(\\{cd627ef1-1059-4ed9-9217-1971806a9ed3\\})|(\\{c01e7fb9-2a24-4cf0-8562-896d53cd5a59\\})|(\\{83d91178-0a1e-4270-a901-b8e83aec3d0b\\})|(blabla@reallyimportantthing\\.com)|(\\{be2309ea-6113-4a99-bd3b-3b88c95cd41d\\})|(\\{02a5f8bb-9ffb-43c3-86d6-072d7316ff6d\\})|(\\{7e874309-fbf3-463b-8690-6ead9efccea0\\})|(\\{7420132f-055f-4f37-a962-3bf9ce42d798\\})|(\\{22c0c5ea-c554-498d-8a66-4c5b4be2f5e8\\})|(\\{ff95833e-5a0a-43cc-910a-e2859bc99ade\\})|(\\{92495a3e-062b-40e7-91e3-6b415c694c57\\})|(mykibetesting47\\.1@mykitext\\.com)|(\\{96cde09b-2d70-41ee-acf6-548728846d48\\})|(mykibetesting47\\.1000@mykitext\\.com)|(\\{98f556db-837c-489e-9c45-a06a6997492c\\})|(\\{bb19d814-d035-4f5d-b291-9b26e4a7fd37\\})|(\\{56a1e8d2-312d-4222-aca5-eee58e0f1234\\})|(\\{9cc56d18-c8fb-436a-861a-128cf2fc580c\\})|(\\{27eebcd1-7ef4-4de3-9d5e-db9e2348c990\\})|(\\{71d3ba6e-e461-44fa-8f27-02c6dbd9b653\\})|(connect@seovpn\\.net)|(\\{859bf655-0266-4f21-9c52-3bd156896259\\})|(\\{adc5c6d8-ac87-4b5f-b885-1adfd00265a7\\})|(\\{d826e099-f797-401f-b363-c03f29a8f9b7\\})|(\\{5621e792-bba7-4e66-a63b-56a9e145c089\\})|(\\{c3e4070e-7845-41fd-8996-8817bbc28790\\})|(\\{eb256992-3498-4262-9c7e-ac82f93aeaca\\})|(\\{da9d1606-dfde-429c-bb6a-63ebe5f8412c\\})|(\\{41e29257-1089-42c8-9d59-add5a0003ffa\\})|(seovpnv5@seovpn\\.net)|(direct-apk-s@eladkarako\\.com)|(\\{376063b4-f6b8-43e9-928f-437ad0ec5333\\})|(\\{b78bf75a-3cea-436b-b081-c9857f295618\\})|(\\{9b8ec882-b5dc-4df2-b9eb-097ba4dcb4b0\\})|(\\{8c0f6ff5-8ea2-4da0-be7b-e588baa02a03\\})|(\\{88295bde-3fc2-4bf9-8090-872d223f2f92\\})|(\\{67582ce8-8c5e-4207-9694-5b0ecc77887b\\})|(\\{db443bc5-4f91-4c82-a14a-298575ff6aba\\})|(\\{d7709ac2-bcde-4d15-a1a7-c459fe592d8e\\})|(\\{40fbe322-ca79-4472-9ac9-0d493f2623da\\})|(\\{77ca86a6-2042-4519-9cd5-4f5fd036d0eb\\})|(youtube-video-downloader@addoncrop\\.com)|(youtube-video-downloader-ff@addoncrop\\.com)|(\\{ed48a832-c064-4d79-bf2e-3c5b21242b3c\\})|(\\{1ce93624-96e6-449d-b038-3380db21cc48\\})|(\\{8957639f-8577-4df9-a1a7-f6d5a9fc9fcd\\})|(\\{c07d8367-6c2d-4a7d-a9ab-207b3104359f\\})|(\\{c4d3f949-60fd-45d5-9cc0-2501319bb60c\\})|(\\{b23bc17b-9fba-4a49-9afe-a1e9cd49c73b\\})|(\\{9f814c7c-0161-4cd4-9d19-50e203f5160c\\})|(\\{0a04ea15-85b4-40ac-a1d1-a6fb5967fb3b\\})|(\\{7bd5a0e9-e8c0-4e64-bda5-7b5310629777\\})|(unlazy@eladkarako\\.com)|(unsecure@eladkarako\\.com)|(xda@eladkarako\\.com)|(direct-filehippo@eladkarako\\.com)|(real-url@eladkarako\\.com)|(\\{63fcde93-baf6-40b5-892c-29cc34365b57\\})|(\\{2913327e-bd0b-4962-adce-2f44407fb521\\})|(\\{88f2e8f9-a7e1-4d68-901d-e5a9c805f4fa\\})|(\\{fd4fcb6a-cad2-43ab-b1c5-efd4d2102de8\\})|(youtube-mp3-converter-ff@addoncrop\\.com)|(\\{0a90b652-0de6-421a-bfee-1d2c5c3ee8fe\\})|(dropeextool@gmail\\.com)|(add_archive@ext\\.xpi)|(\\{47aa471f-3cf1-4a22-9c99-ab5baf326a60\\})|(\\{ac01f6a8-7c30-450b-9979-98d0c7795e11\\})|(\\{2c09761e-2c89-4537-b335-2bd30a7136c2\\})|(\\{c55ddee5-baab-4c90-9948-ca47033a82ce\\})|(\\{5cdb4845-56a8-45df-936b-de8279f2587d\\})|(\\{ea68dd52-2470-4d7c-b563-9a4c6d0e7eab\\})|(\\{01d33686-cab1-4f3b-b2ad-83bc1c35c60a\\})|(\\{fcb61a95-e861-4cf1-801b-f91c75c4714a\\})|(mg3a2mzdsug6fou4k8yk@mg3a2mzdsug6fou4k8yk\\.com)|(wh70nimqtlpwt951i12a@wh70nimqtlpwt951i12a\\.com)|(\\{b64109f0-5d2a-4c86-a38c-886589cb2703\\})|(legacy@context_search\\.com)|(\\{7b49d8bd-1e29-4887-9165-a272f553a154\\})|(\\{a49d6fa5-1967-43ed-b8bd-586f206326cd\\})|(\\{8b14f730-34fa-41e7-9590-d44df583a57f\\})|(\\{01dda21f-3b94-4297-b302-d92535b2e880\\})|(\\{6a33fb2c-3d3a-401f-be54-342ef4d57d1f\\})|(\\{887a4a9a-63ed-4af8-b82b-b68e9c23d39b\\})|(\\{6cae6de5-ff67-4682-a72b-39a1747fc682\\})|(\\{c5b5ac10-8592-4ee9-ad7b-003bb9706f87\\})|(\\{2fb3dc77-de0c-4e34-be3b-13a92bffe898\\})|(\\{82a92837-91de-4282-95d2-bda6d4ac682f\\})|(\\{dae5d3cf-42ba-40ad-85d9-482e25d4682c\\})|(\\{2328a4ad-9d5c-47fa-b580-c7d488bd7a7a\\})|(\\{deb5ba3b-3cf9-4333-99e3-ae2651a5297d\\})|(gespindola@unisuam\\.edu\\.br)|(\\{4518dbc6-fc51-4292-8d85-6eba66959dcb\\})|(\\{7864e12a-8628-415e-8ae8-5ac25edaa420\\})|(\\{b56e7db0-5504-4dd2-8c21-7fb645adae38\\})|(\\{c78ac5a8-0005-4cfb-8a01-5d116a225377\\})|(\\{468301ed-b30f-44f9-85aa-d5ff4c840008\\})|(\\{ae45b0a5-a02e-41df-8a2e-f745d7a7ede3\\})|(\\{f42b032c-3017-44d5-99a1-129524d02494\\}))$/", + "prefs": [], + "schema": 1576757713908, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602022", + "why": "This add-on violates Mozilla's add-on policy by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0f3e4d6d-9b1c-4fe1-940a-2871943bac88", + "last_modified": 1576771657136 + }, + { + "guid": "/^((\\{7e81d973-d606-4f4b-a61f-9c37612c3947\\})|(\\{651d950b-9db7-445f-8263-2dd18265ed3b\\})|(\\{d408b96a-203d-462a-9577-84afb08aa951\\})|(\\{e1fc8acf-65d6-48a2-89d9-410a32bd86da\\})|(\\{1e9302af-a3e1-4c7c-8416-f814dd796cb9\\})|(\\{c4aba9b0-df06-4fc5-8e64-37f99df5e9e9\\})|(\\{24399e9e-50ef-47c3-a728-ebf3af91a0c6\\})|(\\{bdf866a5-ebdd-4e7d-b624-49b53be98a4b\\})|(\\{9ab09f29-f294-4bdd-a58a-c3abea54f032\\})|(\\{b678ee0d-0e1f-433d-8ce8-93e1b51b8834\\})|(\\{6ed0906f-7926-4fc2-ab60-1577631641b2\\})|(\\{0bed1847-54e3-40ac-9cee-0ffd6e4acf1f\\})|(\\{09041dc2-0caa-4955-bf87-de874a0c6b7b\\})|(\\{035ff918-be13-4906-bba3-25153c30b8c4\\})|(\\{e81cbba6-39d5-4f33-a6be-3b99545b54b1\\})|(\\{67d8820b-3078-4835-ab7a-11befb521d33\\})|(\\{c5ad3a58-824c-4db4-8f2c-33b91565417f\\})|(\\{6e289bc1-ab94-44be-adf7-deb1a85cf809\\})|(\\{f6a3d54b-bf6f-4878-881e-a521065412c4\\})|(latest-extension@surviv\\.io)|(\\{3ee8d899-be15-45e4-8c09-a1c10febfb4d\\})|(\\{7ccf5eca-d275-4967-939a-7b9b91d9e0f2\\})|(\\{cd8542c3-3c8e-4fa1-8463-4c5ea7aa25ce\\})|(\\{2994abd2-02a5-4c66-9fd2-38ad8d911324\\})|(\\{10a69518-499c-4339-a733-8ac27166a400\\})|(apzsqkoq0oxjal69l9zr@apzsqkoq0oxjal69l9zr\\.com)|(mbanzzhnky54hoe6jq62@mbanzzhnky54hoe6jq62\\.com)|(\\{ce926acf-d6ab-48b0-8e5d-82257de14011\\})|(\\{c42394d0-1c76-4e5e-8a94-f7c4a2de2ada\\})|(7eylx2qcpshoiehzcl2y@7eylx2qcpshoiehzcl2y\\.com))$/", + "prefs": [], + "schema": 1576757777185, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602022", + "why": "This add-on violates Mozilla's add-on policy by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8f82683f-a1c2-4f61-bf9b-8f279d35f746", + "last_modified": 1576771657132 + }, + { + "guid": "/^((Astro_Connect_BHZNbNOGmL@www\\.astro-connect\\.com)|(Astro_Connect_HRMdSrojBR@www\\.astro-connect\\.com)|(Astro_Connect_lAOPFWMnqc@www\\.astro-connect\\.com)|(Astro_Connect_vMYMzqvERv@www\\.astro-connect\\.com)|(Astro_Daily_Online_1090c71e5da3cd9ab2d6af4985b43f5c@www\\.dailyastrotab\\.com)|(Astro_Daily_Online_clone_b82a3c2703d37e513b428067a34655d8@www\\.dailyastrotab\\.com)|(Astro_Daily_Online_clone_hqNzpeWZrm@www\\.astrodailyonline\\.com)|(Astro_Daily_Online_GzIzTViMnE@www\\.astrodailyonline\\.com)|(AstroDailyOnline\\.com_a7f59cbf31536249d41b97278115b17a@www\\.astrodailyonline\\.com)|(AstroDailyOnline\\.com_VqZjYRsphH@www\\.astrodailyonline\\.com)|(Bible_Quotes_97910bef6e96a7d0569f5fb44b2c607c@www\\.bible-quote\\.co)|(Bible_Quotes_clone_9699f1b771e162467f078c076eb9b695@www\\.bible-quote\\.co)|(Bible_Quotes_clone_wYibuualSr@www\\.bible-quote\\.co)|(Bible_Quotes_PHMyuAzCPa@www\\.bible-quote\\.co)|(blackfridaystoresco@www\\.blackfridaystores\\.co)|(Browse_Manuals_3ff31012f87f9a4c01e1ef723f48eff1@www\\.browsemanuals\\.co)|(Browse_Manuals_ca51c107572ab179fc1e8d4b652b19e1@www\\.browsemanuals\\.co)|(Browse_Manuals_clone_2dbeb0fd48d84ff3a5d3e3a7b5fc3756@www\\.browsemanuals\\.co)|(Browse_Manuals_clone_biPZDTWfln@www\\.browsemanuals\\.co)|(Browse_Manuals_clone_ksNBKBSmge@www\\.browsemanuals\\.co)|(Browse_Manuals_clone_lcMJgmivkD@www\\.browsemanuals\\.co)|(Browse_Manuals_clone_UaWIZDqLBJ@www\\.browsemanuals\\.co)|(Calculate_Fast_uWtHrQKgWv@www\\.nowcalculatefast\\.com)|(cheapflightfaresco@www\\.cheapflightfares\\.co)|(Check_Astro_Today_wkptBasvVf@www\\.checkastrotoday\\.com)|(Check_Astro_Today_YgMcUMJFVL@www\\.checkastrotoday\\.com)|(Check_Astrology_GGZGReDTnd@www\\.checkastrology\\.co)|(Check_Directions_RmiiKOUYTf@www\\.checkdirection\\.com)|(Check_Mails_clone_tdpeulgItk@www\\.check-mail\\.co)|(Check_Mails_iLVZDNXtnY@www\\.checkmailsnow\\.link)|(Check_Mails_ITnrNsNpcj@www\\.checkmailsnow\\.today)|(Check_Mails_Now_clone_oOzxmNgfQn@www\\.checkmailsnow\\.net)|(Check_Mails_Now_clone_vVWZzkqahk@www\\.checkmailsnow\\.net)|(Check_Mails_Now_ePyetjMzel@www\\.checkmailsnow\\.link)|(Check_Mails_Now_IfFzRcCuJa@www\\.checkmailsnow\\.online)|(Check_Mails_Now_OqRkgMLHGo@www\\.checkmailsnow\\.live)|(Check_Mails_Now_pHoBMgvLtK@www\\.checkmailsnow\\.net)|(Check_Mails_Now_QMsZiFhjjH@www\\.checkmailsnow\\.online)|(Check_Mails_Now_qOSRXKCsHK@www\\.checkmailsnow\\.net)|(Check_Mails_Now_vnjdpXGSWk@www\\.checkmailsnow\\.net)|(Check_Mails_vNOoNISpGM@www\\.check-mail\\.co)|(Check_Maps_3ec1ffc98971e67fa854369f9e3486d0@www\\.check-maps\\.co)|(Check_Maps_BezZcNfeab@www\\.check-maps\\.link)|(Check_Maps_bKaiUcBTpN@www\\.check-maps\\.net)|(Check_Maps_clone_RUjCUUeRwF@www\\.checkmaps\\.co)|(Check_Maps_KzXAqTbRZi@www\\.check-maps\\.co)|(Check_Maps_ogQkeJhGuf@www\\.checkmaps\\.net)|(Check_Maps_SwGvtYCSWM@www\\.check-maps\\.org)|(Check_Maps_uYfjFOesyI@www\\.check-maps\\.org)|(Check_My_Mails_iAClnrnvyj@www\\.checkmymails\\.online)|(Check_My_Mails_TYlXbVPRXn@www\\.checkmymails\\.info)|(Check_My_Mails_XvyJVrZWQX@www\\.checkmymails\\.info)|(Check_My_Mails_ZAVfjZCgnv@www\\.checkmymails\\.co)|(Check_My_Speed_Now_OsGaqlODan@www\\.checkmyspeednow\\.com)|(Check_My_Speed_PQiWezZbqs@www\\.checkmyspeed\\.co)|(Check_My_Speed_qvklQhzugJ@www\\.checkmyspeed\\.co)|(Check_Net_Speed_aAaAYSEawi@www\\.checknetspeed\\.online)|(Check_Net_Speed_EKnEHmaiYm@www\\.checknet-speed\\.today)|(Check_Net_Speed_IcfzRgdvIx@www\\.checknet-speed\\.today)|(Check_Net_Speed_rqJpoOAXqE@www\\.checknet-speed\\.today)|(Check_Net_Speed_SoVZYFExRY@www\\.checknetspeed\\.today)|(Check_Net_Speed_sXCrVEDGiO@www\\.checknet-speed\\.today)|(Check_Net_Speed_VQGJrJvlIm@www\\.checknet-speed\\.today)|(Check_Net_Speed_wCexanRKNz@www\\.checknetspeed\\.link)|(Check_Net_Speed_yTZgqCpFPi@www\\.checknet-speed\\.today)|(Check_Net_Speed_zCCNoLFHOF@www\\.checknetspeed\\.online)|(Check_News_clone_qPYbwwKpgF@www\\.checknews\\.co)|(Check_News_dGPwUXsQpF@www\\.checknews\\.co)|(Check_Speed_Test_KohJtFpgLB@www\\.checkspeedtest\\.co)|(Check_Speed_Test_XcYMLbMdlh@www\\.checkspeedtest\\.co)|(Check_Weather_clone_FJgKgEEkzT@www\\.checkweather\\.co)|(Check_Weather_clone_TwXCcjNBdE@www\\.checkweather\\.co)|(Check_Weather_Daily_65ec7723f6fbbe7786d60bd4e2643b1c@www\\.checkweatherdaily\\.com)|(Check_Weather_Daily_YxdeQsIzIP@www\\.checkweatherdaily\\.com))$/", + "prefs": [], + "schema": 1576757846784, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ff5d3b75-f26c-47cd-85df-32c193a8d002", + "last_modified": 1576771657128 + }, + { + "guid": "/^((Check_Weather_Details_26e944ef07a6d60d7e6e109df96aadc6@www\\.checkweatherdetails\\.co)|(Check_Weather_NkuDUpBOfy@www\\.checkweathertoday\\.com)|(Check_Weather_Today_1b92f68f44010297e79e167abdbbad90@www\\.checkweathertoday\\.com)|(Check_Weather_Today_2b5b41fcbe35242dced42efd82346b62@www\\.checkweathertoday\\.com)|(Check_Weather_Today_37f90d6081f0f87dde0eca340268dd06@www\\.checkweathertoday\\.com)|(Check_Weather_Today_421f2bbc2e93713f42200c4cf4184b2f@www\\.checkweathertoday\\.com)|(Check_Weather_Today_647dda58dab18a066b5edcd1bf82f244@www\\.checkweathertoday\\.com)|(Check_Weather_Today_8d106128d380304fbf7cf760284ed189@www\\.checkweathertoday\\.com)|(Check_Weather_Today_a6606d84c174672bdc97a40ae26a7329@www\\.checkweathertoday\\.com)|(Check_Weather_Today_a7ec7b0d21c0b719949374759fa27f9e@www\\.checkweathertoday\\.com)|(Check_Weather_Today_AGSeXIfuyC@www\\.checkweathertoday\\.com)|(Check_Weather_Today_c72d09cea0b53ffdd1a496e376b22696@www\\.checkweathertoday\\.com)|(Check_Weather_Today_cbc523bdd189c28af1505f104443a1cf@www\\.checkweathertoday\\.com)|(Check_Weather_Today_e448585b8ce80f90b04ad34b57c00d6e@www\\.checkweathertoday\\.com)|(Check_Weather_Today_ed0f6099cd2a8e05500657b43f897afc@www\\.checkweathertoday\\.com)|(Check_Weather_Today_EppkRAhhSW@www\\.checkweathertoday\\.com)|(Check_Weather_Today_f06201395c0dc5e1aa9fd68e2059dc18@www\\.checkweathertoday\\.com)|(Check_Weather_Today_fd6b11d8d8cad6bb654cf46a0e100cbe@www\\.checkweathertoday\\.com)|(Check_Weather_Today_FdVgvyMTJo@www\\.checkweathertoday\\.com)|(Check_Weather_Today_leAPNcsALh@www\\.checkweathertoday\\.com)|(Check_Weather_Today_LFHBoYhhIu@www\\.checkweathertoday\\.com)|(Check_Weather_Today_MJoPqcUdAA@www\\.checkweathertoday\\.com)|(Check_Weather_Today_neiEtvLsDn@www\\.checkweathertoday\\.com)|(Check_Weather_Today_NvAaeiHaWs@www\\.checkweathertoday\\.com)|(Check_Weather_Today_OGjNKxAeXE@www\\.checkweathertoday\\.com)|(Check_Weather_Today_uABqfeATcz@www\\.checkweathertoday\\.com)|(Check_Weather_Today_WKqJGOJnJg@www\\.checkweathertoday\\.com)|(Check_Weather_Today_xCOMPlhuXu@www\\.checkweathertoday\\.com)|(Check_Weather_Today_XmOwlHQNSl@www\\.checkweathertoday\\.com)|(Check_Weather_wktudKPpvh@www\\.checkweather\\.co)|(Check_Your_Mail_5697289aed4cf7b184d93e5c53117e07@www\\.check-yourmail\\.co)|(Check_Your_Mail_BZSHAsKTzJ@www\\.check-yourmail\\.co)|(CheckMailPro\\.net_CheZLENbVx@checkmailpro\\.net)|(checkmaps@www\\.check-maps\\.co)|(CheckNetSpeed\\.co_tkZoSRvsPg@www\\.checknetspeed\\.co)|(checknetspeedco@www\\.checknetspeed\\.co)|(Convert_Doc_Online_ptdllbDbTd@www\\.convertdoconline\\.com)|(Convert_File_JHjKeEFAkZ@www\\.convert-file\\.net)|(Convert_File_ocNpurmujk@www\\.myfileconverter\\.org)|(Convert_File_Online_JfGXnualjV@www\\.convertfileonline\\.net)|(Convert_Files_Now_8390db80d295eac08ca91cc1687ebf6d@www\\.convertfilesnow\\.co)|(Convert_Files_Now_afb77fba2ab7b055f10896e538ebd2f6@www\\.convertfilesnow\\.online)|(Convert_Files_Now_fzusJnJyIU@www\\.convertfilesnow\\.co)|(Convert_Files_Now_YEHBPCVZfb@www\\.convertfilesnow\\.online)|(Convert_Files_Online_cf2d8484b67f12d038789a618bae5465@www\\.convertfilesonline\\.co)|(Convert_Files_Online_RptGXHLRjE@www\\.convertfilesonline\\.co)|(Convert_My_Doc_771b3d45ecf0ab980ab0dfdcb9dc1c16@www\\.convertmydoconline\\.com)|(Convert_My_Doc_ofLnqzQpvZ@www\\.convertmydoconline\\.com)|(Convert_My_Document_ynCvryXblL@www\\.convertmypdf\\.online)|(Convert_My_File_0e081da9309e2ee8cbe6e5e1ca3373d1@www\\.convertmyfile\\.co)|(Convert_My_File_2451d924787cf4b696596f09fde42c2f@www\\.convertmyfiles\\.net)|(Convert_My_File_35bbde2206c3bb498f5033610ab82af6@disable\\.convertthefiles\\.online)|(Convert_My_File_68884222b56f8d972093a20f7d5fcf94@www\\.convertmyfile\\.co)|(Convert_My_File_e4a4b99e91c46c64d75d2795ac8e8d58@www\\.convertthefiles\\.online)|(Convert_My_File_GHurKBBhRg@www\\.convertmyfile\\.co)|(Convert_My_File_HlOKyXsynN@www\\.convertmyfile\\.co)|(Convert_My_File_sdaXbdSqCR@www\\.convertmyfiles\\.net)|(Convert_My_File_SiQYEMQoEO@www\\.convertthefiles\\.online)|(Convert_My_File_SJjGzfwuEU@www\\.convertthefiles\\.online)|(Convert_My_File_SzhPQAPUZF@www\\.convertmyfile\\.co)|(Convert_My_File_zOqNatevtM@www\\.convertmyfiles\\.net)|(Convert_My_Files_41f090cd741517aacdcc2c939b5ba94e@disable\\.convertmyfiles\\.net)|(Convert_My_Files_AcwjJWzydY@www\\.convertmyfiles\\.co))$/", + "prefs": [], + "schema": 1576757889686, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "92d2dcba-9f62-4b16-8472-843bc53efef5", + "last_modified": 1576771657124 + }, + { + "guid": "/^((Convert_My_Files_clone_AtaaUykZUt@www\\.convertthefiles\\.co)|(Convert_My_Files_clone_iipeyamXvF@www\\.convert-myfiles\\.link)|(Convert_My_Files_GcaMuSvchr@www\\.convertmyfiles\\.co)|(Convert_My_Files_ITTiqkJdem@www\\.convertmyfiles\\.link)|(Convert_My_Files_mOqWpDqFfS@www\\.convertmy-files\\.link)|(Convert_My_Files_ObjwXXGRmQ@www\\.convertmyfiles\\.net)|(Convert_My_Files_OdEReOXPeW@www\\.convertmyfiles\\.link)|(Convert_My_Files_WdZEfGNUyl@www\\.convertmyfiles\\.link)|(Convert_My_Files_ZSVwsGAvMB@www\\.convert-myfiles\\.link)|(Convert_The_File_ajAXskmxxC@www\\.convertthefile\\.co)|(Convert_The_File_decdQgcxcJ@www\\.convertthefile\\.co)|(Convert_The_File_lfLaRLbLCX@www\\.convertthefile\\.online)|(Convert_The_File_pVtEvdXWNp@www\\.convertthefile\\.co)|(Convert_The_File_YeFOCclGsc@www\\.convertthefile\\.co)|(Convert_The_File_yfsuYhYdaU@www\\.convertthefile\\.net)|(Convert_The_PDF_clone_52933220d40bf58153ba067b1c9c2a0c@www\\.convertthepdf\\.com)|(Convert_The_PDF_clone_929829a88637d12dbf08fd4f782a93b8@www\\.convertthepdf\\.com)|(Convert_The_PDF_clone_WZNDVMgflh@www\\.convertthepdf\\.com)|(Convert_The_PDF_clone_xmrWNxzVTQ@www\\.convertthepdf\\.com)|(Convert_The_PDF_e8cd3270f4b0eec8bd7ec39dfe2b411c@www\\.convertthepdf\\.com)|(Convert_Your_Files_546ea793afdb11f0c3cb3866dcb02379@www\\.convertyourfiles\\.co)|(convertfilestopdfcom@www\\.convertfilestopdf\\.com)|(convertmypdfco@www\\.convertmypdf\\.co)|(convertmypdfonline@www\\.convertmypdf\\.online)|(convertthepdfco@www\\.convertthepdf\\.co)|(convertthepdfcom@www\\.convertthepdf\\.com)|(Cook_With_Me_oYyEsUWTmq@www\\.cookwithme\\.co)|(Coupon_Club_App_20ef9a3e126b1c339a97a60672528026@www\\.couponclubapp\\.co)|(Coupon_Club_App_295813467b3bbbd03ed2574a3a379041@www\\.couponclubapp\\.co)|(Coupon_Club_App_clone_dMsjQwVamC@www\\.couponclubapp\\.co)|(Coupon_Club_App_clone_pahxCWSfWG@www\\.couponclubapp\\.co)|(Coupon_Club_App_clone_tLIovhcRov@www\\.couponclubapp\\.co)|(Coupon_Club_App_clone_UKQIyoDcSk@www\\.couponclubapp\\.co)|(Coupon_Club_App_clone_wgpiIAHAQx@www\\.couponclubapp\\.co)|(Coupon_Club_App_imfeKPFBRv@www\\.couponclubapp\\.co)|(Coupon_Club_App_vqwFoUyfpL@www\\.couponclubapp\\.co)|(Coupon_Daily_DGaEVQJCVN@www\\.coupondaily\\.today)|(Coupon_Dealer_rRkzTRGEcA@www\\.coupondealer\\.co)|(Coupon_Finder_Hub_8f0b12e405b7568785bcc785d6be30d2@www\\.couponfinderhub\\.com)|(Coupon_Saver_Plus_ffb1e922e2994e938b8f9aeff598c90d@www\\.couponsaverplus\\.co)|(Coupon_Saver_Plus_GTTzegkxqL@www\\.couponsaverplus\\.co)|(Coupon_Store_MlfSWWimyu@www\\.mycouponstore\\.co)|(Coupon_Store_Search_clone_AbsQujloBm@www\\.thecouponstore\\.co)|(Coupons_Flash_clone_TCfVLxCpbB@www\\.couponsflash\\.co)|(Coupons_Flash_fYMZkaoiDb@www\\.couponsflash\\.co)|(Coupons_Flash_WRjXiQAOwJ@www\\.couponsflash\\.co)|(Coupons_Magic_AuVOPUiczY@www\\.couponsmagic\\.co)|(Coupons_Magic_MaDWggNxen@www\\.couponsmagic\\.co)|(Coupons_Magic_ZebFYzkzoN@www\\.couponsmagic\\.co)|(Coupons_Tab_9a56fd9df10d162f000ef7dac2689e8e@www\\.couponstab\\.co)|(Coupons_Tab_sFSnaAFdDP@www\\.couponstab\\.co)|(Coupons_Test_uyywuEAPjT@www\\.couponstab\\.co)|(couponstore@www\\.coupon-store\\.co)|(CutePuppyWallapers\\.com_nicLwcUsfw@www\\.cutepuppywallapers\\.com)|(Daily_Astrology_fPoifgshwY@www\\.daily-astrology\\.online)|(Daily_Astrology_WPGvllFIRz@www\\.my-dailyastrology\\.net)|(Daily_Coupon_Finder_XefkJELubc@www\\.dailycouponfinder\\.co)|(Daily_Coupon_Store_cdowGuTgIr@dailycouponstore\\.co)|(Daily_Coupons_Tab_2c96becddbca0072a93db91266fbfefa@www\\.recipeboardplus\\.com)|(Daily_Coupons_Tab_HRcyzwBTWg@www\\.dailycouponstab\\.com)|(Daily_Deals_Club_yIvuSLfnXr@www\\.dailydealsclub\\.co)|(Daily_Easy_Recipe_OlMvwdldaK@www\\.dailyeasyrecipe\\.com)|(Daily_Easy_Search_95c6b38eb0f04a21212c678a43291d12@www\\.dailyeasysearch\\.com)|(Daily_Easy_Search_MVXtvNQzSj@dailyeasysearch\\.com)|(Daily_File_Converter_23db4a874fc066754c187eef894ccbe7@www\\.dailyfileconverter\\.com)|(Daily_File_Converter_25b0c49fa35ba08d5b33edd7b48f2c92@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_9d4c9a6f7a5c796c84a594619c1e6993@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_b4b0ea08aa61314c75baffd0246b9bcf@www\\.dailyfileconverter\\.com)|(Daily_File_Converter_clone_RDUYhKAKFi@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_clone_yuTWwUVfoL@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_cVmGYeldAg@www\\.dailyfileconverter\\.co))$/", + "prefs": [], + "schema": 1576757965088, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6944f445-4df8-4331-93da-1b32fc130849", + "last_modified": 1576771657120 + }, + { + "guid": "/^((Daily_File_Converter_ooPlASGwFM@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_QMxMSpDXIk@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_UufmFacjfR@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_vaLEkEAYJg@www\\.dailyfileconverter\\.co)|(Daily_File_Converter_VhHvpeSnpU@www\\.dailyfileconverter\\.com)|(Daily_Game_Search_EaLACFoFKS@www\\.daily-gamesearch\\.today)|(Daily_Game_Search_muvQxhbZNu@www\\.dailygamesearch\\.today)|(Daily_Game_Search_VSZsYHVLcK@www\\.dailygame-search\\.today)|(Daily_Game_Search_XpyakjSRnr@www\\.dailygamesearch\\.co)|(Daily_Games_40e88a4d6ecfd9f9af84e1e6c7d76ffe@www\\.gamesdaily\\.online)|(Daily_Games_53113db02f9dd623b086f331ade6d2a6@www\\.game-quest\\.co)|(Daily_Games_a3083ae69e261df3b7d43f5282b250d0@www\\.daily-games\\.co)|(Daily_Games_adebc8294b5d311f8d3b5abd659e09f6@disable\\.daily-games\\.co)|(Daily_Games_b0819ccb9f64203af68bb05e00f36c2a@www\\.dailygame\\.online)|(Daily_Games_BhJRTCwnnH@www\\.game-quest\\.co)|(Daily_Games_BYlaCwrbCa@www\\.dailygame\\.online)|(Daily_Games_c342718e3fb89df8a20b77f635ea0e0f@www\\.dailygame\\.online)|(Daily_Games_caf0a82b928c12c5d67d2e0d142acbcc@www\\.daily-games\\.online)|(Daily_Games_clone_MHxxZxsjgz@www\\.gamesdaily\\.co)|(Daily_Games_d859afcc6f811f0938e06c8b75ac15fc@www\\.gamesdaily\\.online)|(Daily_Games_e6da5a8f129fc23530fc715fd2f7b993@www\\.daily-games\\.today)|(Daily_Games_e858f8f836f17ab5e1c3b269639bc434@www\\.daily-games\\.co)|(Daily_Games_FHAIlDwdZF@www\\.gamesdaily\\.co)|(Daily_Games_IgqcabXnnH@www\\.dailygame\\.online)|(Daily_Games_NOgqDldYjE@www\\.daily-games\\.co)|(Daily_Games_qEgzrEJBZi@www\\.daily-games\\.co)|(Daily_Games_qFMKBgNfOc@www\\.gamesdaily\\.online)|(Daily_Games_vfGCbwKDjq@www\\.daily-games\\.online)|(Daily_Games_VoOHRAgAxg@www\\.daily-games\\.today)|(Daily_Games_vwRTUFvSbP@www\\.gamesdaily\\.online)|(Daily_Games_YoUYAafVuL@www\\.daily-games\\.co)|(Daily_Horoscope_Finder_f4f7f24d489880fa4779c5a4aff5278d@www\\.dailyhoroscopefinder\\.com)|(Daily_Local_Weather_RTvDyyEuxy@www\\.dailylocalweather\\.net)|(Daily_Mail_Tab_AMkImujKsG@www\\.dailymailtab\\.com)|(Daily_Mail_Tab_clone_BWhcKvnbJG@www\\.dailymailtab\\.com)|(Daily_Mail_Tab_clone_jkCRhwZjtG@www\\.dailymailtab\\.com)|(Daily_Mail_Tab_xMqjhKMBPX@www\\.dailymailtab\\.com)|(Daily_Net_Speed_gcVjCfMtcE@www\\.dailynetspeed\\.com)|(Daily_News_Reports_707e3c94519dc4f338e25415097fc409@disable\\.dailynewsreports\\.co)|(Daily_News_Reports_bc4cc8745c5c9113add6bbcc7a4e891d@www\\.dailynewsreports\\.co)|(Daily_News_Reports_MgfPHeboNM@www\\.dailynewsreports\\.co)|(Daily_News_Reports_RhfPUMZyKi@www\\.dailynewsreports\\.co)|(Daily_Package_Tracker_qJQSJblDIk@www\\.dailypackagetracker\\.com)|(Daily_PDF_Converter_cAlFsFtCmX@www\\.dailypdfconverter\\.com)|(Daily_Radio_Hub_4b6408c6028360c91e55070e8a8b6c07@www\\.dailyradiohub\\.com)|(Daily_Radio_Hub_CEgdHfrHfk@www\\.dailyradiohub\\.com)|(Daily_Recipe_Finder_LpHSZIWYdz@www\\.dailyrecipefinder\\.com)|(Daily_Recipe_Ideas_clone_xqrPqfTBif@www\\.dailyrecipeideas\\.co)|(Daily_Recipe_Ideas_dyhmXRtGGY@www\\.dailyrecipeideas\\.co)|(Daily_Recipe_Now_IXRimkpfYA@www\\.dailyrecipenow\\.com)|(Daily_Recipe_Search_wHQTXcltvc@www\\.dailyrecipesearch\\.co)|(Daily_Search_3e2f26bf46061e1fb6386bb2bc9fb3b1@www\\.dailysearch\\.co)|(Daily_Search_clone_RAfKSlJEDD@www\\.dailysearch\\.co)|(Daily_Search_Plus_b60c5575e4d5158e20036cb104c00bba@www\\.dailysearchplus\\.com)|(Daily_Search_Plus_f693ecbe1af3da784c31114acc402ec4@www\\.dailysearchplus\\.com)|(Daily_Search_Plus_oldtAgRAUL@www\\.dailysearchplus\\.com)|(Daily_Search_Plus_xCxtAmrZoI@www\\.dailysearchplus\\.com)|(Daily_Search_Web_f70aa46e9caf1e58a04aad7917c8d0aa@www\\.dailysearchweb\\.com)|(Daily_Search_Web_PvsRmlCiKL@www\\.dailysearchweb\\.com)|(Daily_Speed_Check_475174e16d0d170b447b7002797efade@www\\.dailyspeedcheck\\.com)|(Daily_Speed_Check_uqFEhmPbDE@www\\.dailyspeedcheck\\.com)|(Daily_Speed_Checker_1c6d34e9410cb2652228c18bed9eaf67@www\\.dailyspeedchecker\\.com)|(Daily_Speed_Checker_wcntWZhyvr@www\\.dailyspeedchecker\\.com)|(Daily_Transit_Guide_a4b1896ed15535c97cf0c19cb3960680@www\\.dailytransitguide\\.com)|(Daily_Transit_Guide_rOdLEeRGxw@www\\.dailytransitguide\\.com)|(Daily_Weather_Finder_94b59c3b4bf2e42f392be55572d25ac2@www\\.dailyweatherfinder\\.com)|(Daily_Weather_Finder_QhhPAMLPai@www\\.dailyweatherfinder\\.com)|(Daily_Weather_Forecast_cgMvSNkeBN@www\\.dailyweatherforecast\\.net))$/", + "prefs": [], + "schema": 1576757992072, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4acfb151-a1d2-4468-9794-5816fadef1b7", + "last_modified": 1576771657116 + }, + { + "guid": "/^((Daily_Weather_Forecast_clone_CSwGEuwNKr@www\\.dailyweatherforecast\\.co)|(Daily_Weather_Forecast_f7824fa0dfff1fdbe9f36ad33ecfa4b4@www\\.dailyweatherforecast\\.net)|(Daily_Weather_Forecast_kclkQdSlIO@www\\.dailyweatherforecast\\.co)|(DailyCoupons\\.store_zWkmEkoPgk@www\\.dailycoupons\\.store)|(DailyJobSearch\\.co_EKJtPSSMhB@www\\.dailyjobsearch\\.co)|(DailyJobsSearch\\.net_a2ba2cb41169ec0e7e1e381ae8cec363@www\\.dailyjobssearch\\.net)|(dailyjobssearch@www\\.dailyjobssearch\\.co)|(dailyjobssearchnet@www\\.dailyjobssearch\\.net)|(DailyLocalWeather\\.net_clone_jVyHpUvXAl@www\\.dailylocalweather\\.net)|(DailyLocalWeather\\.net_vzkDkoQoVN@www\\.dailylocalweather\\.net)|(DailyNetSpeedtest\\.co_HWGHkbkKGm@dailynetspeedtest\\.com)|(dailynewsupdatesinnet@www\\.dailynewsupdates\\.in\\.net)|(dailynewsupdatesonline@www\\.dailynewsupdates\\.online)|(DailyRecipeOnline\\.net_bVwwSbDKSk@www\\.dailyrecipeonline\\.net)|(DailyRecipeSearch\\.co_zXrOSYpFeH@www\\.dailyrecipesearch\\.co)|(DailyRecipeSearch\\.info_VJInpLJmig@www\\.dailyrecipesearch\\.info)|(Dictionary_Pro_App_HbjmnxLIZN@www\\.dictionaryproapp\\.com)|(Dictionary_Pro_PquztTZbLu@www\\.dictionarypro\\.co)|(Dictionary_Xpress_QeRWOLUqwj@www\\.mywordscribent\\.com)|(Directions_Finder_190cb6038fd06edbc48ea5423cc43227@www\\.finddirections\\.co)|(Directions_Finder_1e30de3303446cde1d03ee219a13f572@www\\.finddirection\\.online)|(Directions_Finder_868daddf032f443da0515e9c976159c9@www\\.finddirections\\.co)|(Directions_Finder_86fa782f7bd705c2e644aa07f6218661@www\\.finddirections\\.co)|(Directions_Finder_AcrlxGosjE@www\\.finddirections\\.co)|(Directions_Finder_BZDovhcQyV@www\\.findroutes\\.co)|(Directions_Finder_c005f28d896373d557e03e80f4b57d37@www\\.findroutes\\.co)|(Directions_Finder_e183b060f3f35940a9291cd2ed4791e3@www\\.finddirections\\.co)|(Directions_Finder_lHmoDZAZNo@www\\.finddirections\\.co)|(Directions_Finder_LKGiGUhRDf@www\\.finddirections\\.co)|(Directions_Finder_Now_NMzpPonhyb@www\\.directionsfindernow\\.com)|(Directions_Finder_QTCjmhKxhD@www\\.finddirection\\.online)|(Directions_Finder_V1_0c00deff96164188875cce59cb27ebb1@www\\.finddirections\\.co)|(Directions_Finder_V1_4668ec661e1b94a2474a1c1675158eb2@www\\.finddirections\\.co)|(Directions_Finder_V1_clone_BPDzngiFeD@www\\.finddirections\\.co)|(Directions_Finder_V1_clone_BPDzngiFeDic@www\\.finddirections\\.co)|(Directions_Finder_V1_clone_lkSfnVsTUN@www\\.finddirections\\.co)|(Directions_Finder_V1_clone_mPiHTaJZJi@www\\.finddirections\\.co)|(Directions_Finder_V1_de58f7b93a8d173bc071b3730806cf8c@www\\.finddirections\\.co)|(Directions_Finder_V1_ffc19350a43c8bf4db65930f95663016@www\\.finddirections\\.co)|(Directions_Finder_V1_xivwbBBnLX@www\\.finddirections\\.co)|(Directions_Found_1dc1261209c6647a8288498e180d5eef_2@www\\.directionsfound\\.com)|(Directions_Found_1dc1261209c6647a8288498e180d5eef_23@www\\.directionsfound\\.com)|(Directions_Found_1dc1261209c6647a8288498e180d5eef@www\\.directionsfound\\.com)|(Directions_Found_clone_hbtpEmPzTJ@www\\.directionsfound\\.com)|(Directions_Found_clone_hdjMdQYTvN@www\\.directionsfoundnt\\.com)|(Directions_Found_GLuiPTQCww@www\\.directionsfound\\.com)|(Directions_Found_mVBuOLkFzz@www\\.directionsfoundnt\\.com)|(Directions_Found_XmorLjjHtu@www\\.directionsfound\\.com)|(Directions_Quest_clone_qQtdSaDtmD@www\\.directionsquest\\.co)|(Directions_Quest_DuwqkuyNfU@www\\.directionsquest\\.co)|(Directions_Quest_FwNcHagZgz@www\\.directionsquest\\.co)|(Directions_Quest_XQDqehhuWR@www\\.directionsquest\\.co)|(Doc_Converter_0a0ede466240dc417f4f6d8594acd9a5@www\\.convertthepdf\\.co)|(Doc_Converter_Hub_sTIWsdHRFE@www\\.docconverterhub\\.com)|(Document_Converter_CZmoJDQssK@www\\.convertpdfnow\\.co)|(Driving_Directions_App_mUxSdxsgni@www\\.drivingdirectionsappn\\.org)|(Driving_Maps_Online_clone_jtpQCXsDzM@www\\.drivingmapsonline\\.com)|(Driving_Maps_Online_evmZdmROJS@www\\.drivingmapsonline\\.com)|(Easy_Directions_Now_a9718312f4e22a699b573a1cf508ccc9@www\\.easydirectionsnow\\.com)|(Easy_Directions_Now_TBmyGrTUZj@www\\.easydirectionsnow\\.com)|(Easy_Doc_Converter_393a2418db4856b1ec2d790e9b30ffc2@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_5a92e4df39328183b24ce0103f3dea19@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_79478769a5477116f6efd505f3b0cf15@www\\.easydocconverter\\.com)|(Easy_Doc_Converter_a1be8989ddea6e7543bea0b843c6a2ba@www\\.easydocconverter\\.com))$/", + "prefs": [], + "schema": 1576758021535, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "125bc0c5-76dc-4426-b682-1c321843d645", + "last_modified": 1576771657110 + }, + { + "guid": "/^((Everyday_Astro_YjPsaMBBWj@www\\.everydayastro\\.online)|(Everyday_File_Converter_ed35274eecd2df2822fa50636b6d9c2c@www\\.everydayfileconverter\\.com)|(Everyday_File_Converter_NldTeZANPx@everydayfileconverter\\.com)|(Everyday_Horoscope_App_zsZEtFjPki@www\\.everydayhoroscopeapp\\.com)|(Everyday_News_Tab_eebc4520552865e4fa1472681eeb5a3d@www\\.everydaynewstab\\.com)|(Everyday_News_Tab_zARNxTxEiu@www\\.everydaynewstab\\.com)|(Everyday_Radio_1f1b997a2d674af1b684dd5457ae5356@www\\.myeverydayradio\\.com)|(Everyday_Radio_mTDVrswpBm@www\\.myeverydayradio\\.com)|(Everyday_Recipe_Guide_566183664fdda4ba4e650fa5115c6cfe@www\\.recipeboardplus\\.com)|(Everyday_Recipe_Guide_tiorNZIoRi@www\\.everydayrecipeguide\\.com)|(Everyday_Weather_Forecast_2a934b9a097e1f619a7066c2530ef60f@www\\.everydayweatherforecast\\.com)|(Everyday_Weather_Forecast_sPyCkYArmT@www\\.everydayweatherforecast\\.com)|(EverydayAstro\\.net_kTOmUMbUtk@www\\.everydayastro\\.net)|(ext_11637@www\\.map-buddy\\.net)|(extensionid@www\\.dailynewsupdates\\.in\\.net)|(extensionid@www\\.moviequest\\.co)|(Fast_Mail_Tab_BfwMzfJAdm@www\\.fastmailtab\\.com)|(Fast_Mail_Tab_clone_LPzCObgNLo@www\\.fastmailtab\\.com)|(Fast_Map_Finder_ae4f5334a91035cf082f4c9e0983fc15@www\\.fastmapfinder\\.com)|(Fast_Map_Finder_YxRYGsJekN@www\\.fastmapfinder\\.com)|(Federal_Forms_clone_NgrjNsxcUN@www\\.easyformsfinder\\.com)|(Federal_Forms_EBqDdPvCFF@www\\.easyformsfinder\\.com)|(File_Convert_Plus_clone_juAtPozquc@www\\.fileconvertplus\\.com)|(File_Convert_Plus_JfRqzQtMeK@www\\.fileconvertplus\\.com)|(File_Convert_Pro_hOblzzFvJL@www\\.fileconvertpro\\.co)|(File_Converter_37ced8ee9475a347afab0d20660cef63@www\\.pdf-convertn\\.co)|(File_Converter_clone_aWSjJQAWwa@www\\.pdf-convertn\\.co)|(File_Converter_Hub_wnuDyqJFOe@www\\.fileconverterhub\\.com)|(File_Converter_Live_hWXrCtYfna@www\\.fileconverterlive\\.com)|(File_Converter_Tab_63009c33f9d202492393146d23df95d0@www\\.fileconvertertab\\.com)|(File_Converter_Tab_bEfXMQovol@info\\.fileconvertertab\\.com)|(File_Converter_Tab_bHaEkegUAR@www\\.fileconvertertab\\.com)|(File_Converter_Tab_clone_8d6f907bab9ab53a83b5d8a4505433f4@www\\.fileconvertertab\\.com)|(File_Converter_Tab_clone_cCZhwgyHDr@www\\.fileconvertertab\\.com)|(File_Converter_Tab_clone_gNPUWukwdG@www\\.fileconvertertab\\.com)|(File_Converter_Tab_clone_pweczfyyvv@www\\.fileconvertertab\\.com)|(File_Converter_Tab_clone_qygWFXkHri@www\\.fileconvertertab\\.com)|(File_Converter_Tab_clone_XpUyfDDJsm@info\\.fileconvertertab\\.com)|(File_Converter_Tab_dd756ad1b37f999650834591ffd21464@www\\.fileconvertertab\\.com)|(File_Converter_Tab_deb5d97364d6e9ba4d04089f90afa043@www\\.fileconvertertab\\.com)|(File_Converter_Tab_V2_8fe7b393ec2d293e54abf1aada3bcb94@www\\.fileconvertertab\\.com)|(File_Converter_Tab_V2_clone_DrmwsZhTkw@www\\.fileconvertertab\\.com)|(File_Converter_Tab_XHmAqSTCbP@www\\.fileconvertertab\\.com)|(File_Converter_xdbvCaJxrb@www\\.myfileconverter\\.co)|(FileConvertPro_clone_gDJuPpeLjD@www\\.fileconvertpro\\.co)|(FileConvertPro_mLscTidBER@www\\.fileconvertpro\\.co)|(Find_Coupons_Daily_dxJFJCQcpS@www\\.finddaily-coupons\\.today)|(Find_Coupons_Daily_lIKYSwDOhr@www\\.findcouponsdaily\\.co)|(Find_Coupons_Daily_RjrQWQzreq@www\\.findcoupons-daily\\.today)|(Find_Coupons_Daily_UhahUhrtxP@www\\.find-couponsdaily\\.today)|(Find_Coupons_Daily_wUpPOFfkzc@www\\.findcouponsdaily\\.today)|(Find_Coupons_OWfYsAQras@www\\.findcoupons\\.live)|(Find_Daily_Coupons_6ea44d6d01a700e213070c692343e03d@www\\.finddailycoupons\\.com)|(Find_Daily_Coupons_71b6faf57612a3de273a55a228ec1703@www\\.finddailycoupons\\.com)|(Find_Daily_Coupons_a4c3252d270bd03507cfe77d8397e8b6@www\\.finddailycoupons\\.com)|(Find_Daily_Coupons_cf887ce0c30cf483939356fd9dc4753a@www\\.finddailycoupons\\.com)|(Find_Daily_Coupons_clone_sCMBPIejep@www\\.finddailycoupons\\.com)|(Find_Daily_Coupons_dBVmCxAeYJ@www\\.finddailycoupons\\.com)|(Find_Daily_Coupons_gSKACLrhGQ@www\\.finddailycoupons\\.com)|(Find_Daily_Coupons_sVYmYFumfX@www\\.finddailycoupons\\.com)|(Find_Daily_Games_gZnVxpnfzi@www\\.finddailygames\\.com)|(Find_Daily_Games_JHwXAakSOO@www\\.finddailygames\\.com)|(Find_Daily_Games_osIGANaBXk@www\\.finddailygames\\.com)|(Find_Daily_Games_rcRvgxUzAu@www\\.finddailygames\\.net)|(Find_Directions_euMftZJtWV@www\\.finddirections\\.co)|(Find_Easy_Directions_2252eb9a9264ef1685d45b8fe13f6a43@www\\.findeasydirections\\.com))$/", + "prefs": [], + "schema": 1576758100930, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a69a7be4-4f76-4c00-aa17-d1bbe66fd2db", + "last_modified": 1576771657102 + }, + { + "guid": "/^((Find_Easy_Directions_541d5ace0c28764eb8e88d98d0e28740@www\\.findeasydirections\\.com)|(Find_Easy_Directions_90d67b798ba3070d90d32d74ff70d1c8@www\\.findeasydirections\\.com)|(Find_Easy_Directions_gbailzSvPb@www\\.findeasydirections\\.com)|(Find_Easy_Directions_YiGOrfTNFT@www\\.findeasydirections\\.com)|(Find_Easy_Directions_zcYSFoeMgF@www\\.findeasydirections\\.com)|(Find_Jobs_Daily_clone_ZryioBAtLo@www\\.findjobsdaily\\.co)|(Find_Jobs_Daily_FpBesVkFoN@www\\.findjobsdaily\\.co)|(Find_Jobs_Daily_TxUobqrfTu@www\\.findjobsdaily\\.co)|(Find_Jobs_Daily_ZLJpRXDhXK@www\\.findjobsdaily\\.co)|(Find_Recipes_Online_kSjCdoBAFX@www\\.findrecipesonline\\.co)|(Find_Templates_Quick_LyRzafuNXE@www\\.findtemplatesquick\\.com)|(FindJobsDaily\\.net_ilHEYezmJM@www\\.findjobsdaily\\.net)|(FlirtyWallPapers\\.online_IEZjVbwbSl@www\\.flirtywallpapers\\.online)|(Forbes_Search_aOPKgBmHyw@forbes\\.dailynewsupdates\\.online)|(Free_Directions_Finder_KEDDaoHBQl@www\\.freedirectionsfinder\\.com)|(Free_Manuals_KkQAhLuDkf@www\\.browsemanuals\\.co)|(Game_Buddy_632c7a501a256d43af845a6c03d36c65@www\\.game-buddy\\.co)|(Game_Buddy_ef910a8045b370e3df14a73fb05c0ee1@www\\.game-buddy\\.co)|(Game_Buddy_emPnaqsxzR@www\\.game-buddy\\.co)|(Game_Buddy_ISrmuaRAHN@www\\.game-buddy\\.co)|(Game_Quest_3c4a7895681b0a9d0860d5397036f146@www\\.game-quest\\.co)|(Game_Quest_bd98408b6a98a0b19825f9ff5023f0e7@www\\.game-quest\\.co)|(Game_Quest_bTqWKJtOPG@www\\.games-quest\\.co)|(Game_Quest_clone_40442f8034b58a60951b7a3cdf5fe97a@www\\.game-quest\\.co)|(Game_Quest_clone_rajfrvipoU@www\\.game-quest\\.co)|(Game_Quest_GHFSWLkdAO@game-quest\\.com)|(Game_Quest_hMqLrbOzmC@www\\.game-quest\\.co)|(Game_Quest_NDifumcXqQ@www\\.games-quest\\.org)|(Game_Quest_nilKwpfZen@www\\.game-quest\\.co)|(Game_Quest_rXONqXzsZj@www\\.games-quest\\.org)|(Game_Search_BbtNWEfPKy@www\\.daily-gamesearch\\.today)|(Game_Search_eOUjptBQzk@www\\.gamessearch\\.live)|(Game_Search_eymrhMPxLu@www\\.dailygame-search\\.today)|(Game_Search_kZDJlntNQs@www\\.gamesearch\\.link)|(Game_Search_MdsPFfXaXl@www\\.game-search\\.link)|(Game_Search_mgkCKPquHh@www\\.dailygamesearch\\.co)|(Game_Search_mvoOwLJOeY@www\\.games-search\\.link)|(Game_Search_nKMbWrakmm@www\\.dailygamesearch\\.today)|(Game_Search_PDKlEPjhDw@www\\.games-search\\.link)|(Game_Tab_BoRsmjioSA@www\\.gametab\\.co)|(Game-Quest_WhVgUeMwZI@www\\.game-quest\\.co)|(GameQuest\\.website_IwplUMSUhd@www\\.gamequest\\.website)|(GameQuest\\.website_ZGhTUTKRhi@www\\.gamequest\\.website)|(Games_Daily_clone_UAnrFPWGxF@www\\.games-daily\\.net)|(Games_Daily_EVSeZYmPYK@www\\.games-daily\\.today)|(Games_Daily_nRQyywoJPm@www\\.games-daily\\.net)|(Games_Daily_SPRlOvPjlj_clone@www\\.games-daily\\.co)|(Games_Daily_SPRlOvPjlj@www\\.games-daily\\.co)|(Games_Daily_WnvPyaqCdX@www\\.games-daily\\.today)|(Games_Finder_Online_98e08661ac2ba2826d702b9de87c3fbc@www\\.gamesfinderonline\\.com)|(Games_Finder_Online_YCDTopLIiv@www\\.gamesfinderonline\\.com)|(Games_Quest_antJJCJcBk@www\\.games-quest\\.website)|(Games_Quest_clone_aTlPJPmJEm@www\\.games-quest\\.today)|(Games_Quest_OxdmpwtACa@www\\.games-quest\\.today)|(Games_Quest_qmlUmTihvS@www\\.gamesquest\\.today)|(Games_Quest_vnzuqLPUQQ@www\\.gamesquest\\.today)|(Games_Quest_WEpNvcmerl@www\\.games-quest\\.today)|(Games_Quest_wKWBKbLjpl@www\\.games-quest\\.today)|(Games_Quest_Zafeeizthl@www\\.games-quest\\.today)|(Games_Quest_ZVRwioHOnh@www\\.games-quest\\.online)|(Games_Search_FPDqSSnSlY@www\\.game-search\\.today)|(Games_Search_HAfeXALvGY@www\\.gamesquest\\.online)|(Games_Search_LSrSMudgNL@www\\.gamessearch\\.co)|(Games_Search_mDdAwBVasx@www\\.gamesearch\\.today)|(Games_Search_RgMSPqlluj@www\\.gamessearch\\.xyz)|(Games_Search_sfZySehUmo@www\\.gamessearch\\.online)|(Games_Search_shZkWBHwut@www\\.gamessearch\\.online)|(Games_Search_USoWCKOlGq@www\\.finddailygames\\.com)|(Games_Search_uXqDhnPDBw@www\\.gamesearch\\.today)|(Games_Search_vCVYFXcOnV@www\\.gamessearch\\.org)|(Games-Daily\\.net_NainTzjpve@www\\.games-daily\\.net)|(Get_Coupons_awAkTqqCxd@www\\.getcoupons\\.live)|(Get_Coupons_RGWEhnHIbr@www\\.getcoupons\\.live)|(Get_Directions_07d014a7ed5bba404eb7f2b1058ad2cb@www\\.getdirectionsmapsn\\.com)|(Get_Directions_CBakCNUUbo@www\\.map-finder\\.link)|(Get_Directions_clone_kxIvsTsDgE@www\\.getdirectionsmapsn\\.com)|(Get_Directions_HlEHEMFRkj@www\\.getdirectionsmapsn\\.com))$/", + "prefs": [], + "schema": 1576758133784, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f6c1160a-fa98-4541-bb03-cd6d02b825e3", + "last_modified": 1576771657097 + }, + { + "guid": "/^((Get_Directions_Quick_89a733fca5e8b5436c95fcec40ca5573@www\\.getdirectionsquickn\\.online)|(Get_Easy_Directions_b3f03018f3d77ab1327550851c2a2f94@www\\.geteasydirections\\.com)|(Get_Easy_Directions_FeEnVDLKMP@www\\.geteasydirections\\.com)|(Get_Easy_Maps_a92ae443f4d44fdbcf507f73934c5bff@www\\.geteasymaps\\.co)|(Get_Easy_Maps_clone_RsVkJokaAq@www\\.geteasymaps\\.co)|(Get_Easy_Maps_clone_TgEXtwRoQx@www\\.geteasymaps\\.co)|(Get_Easy_Maps_e66ef79b1701806812d764c8b2e87673@www\\.geteasymaps\\.co)|(Get_Easy_Maps_eUfwfdzBJN@www\\.geteasymaps\\.co)|(Get_Easy_Maps_f9341eebd729ecc7e5389769a7ac8c27@www\\.geteasymaps\\.com)|(Get_Easy_Maps_NoApMGqlhQ@www\\.geteasymaps\\.com)|(Get_Easy_Maps_PiUUlEtxCz@www\\.geteasymaps\\.co)|(Get_Easy_Maps_uopDIWsrOB@www\\.geteasymaps\\.co)|(Get_Easy_Maps_VZVoDVzTZP@www\\.geteasymaps\\.co)|(Get_Easy_Search_5a1bd32b3bb3d079d253b6fe048c6d4e@www\\.geteasysearch\\.com)|(Get_Easy_Search_b1272ab7a446984d2e1ddbf0b94016d0@www\\.geteasysearch\\.com)|(Get_Easy_Search_zjvfbWwXrA@www\\.geteasysearch\\.com)|(Get_Forms_Today_HmKpwNqqgw@www\\.getformstoday\\.com)|(Get_Local_Forecast_KSKPskMcZy@www\\.getlocalforecastn\\.com)|(Get_Map_Finder_clone_AVJpxOfDmZ@www\\.getmapfinder\\.link)|(Get_Map_Finder_clone_BGAfKOwTad@www\\.getmapfinder\\.link)|(Get_Map_Finder_clone_hwWbfqrHDh@www\\.getmapfinder\\.link)|(Get_Map_Finder_clone_YUnreTXyJo@www\\.getmapfinder\\.link)|(Get_Map_Finder_RDbuumWRlP@www\\.getmapfinder\\.link)|(Get_Map_Finder_vJqBDvfVEi@www\\.getmapfinder\\.link)|(Get_Map_Finder_zORQiNFbOh@www\\.getmapfinder\\.co)|(Get_Online_Converter_AbNmflhxCb@www\\.getonlineconverter\\.com)|(Get_Online_Converter_clone_255e60767a18e291b0b8b3cabd6f382c@www\\.getonlineconverter\\.com)|(Get_Online_Converter_clone_oCShEwyADp@www\\.getonlineconverter\\.com)|(Get_Online_Maps_clone_hGHOrvKQpE@www\\.getonlinemapsnow\\.com)|(Get_Package_Tracker_5a92260cd5155c5f26b16207532e9c5e@www\\.getpackagetracker\\.com)|(Get_Package_Tracker_clone_CztbvYsSLz@www\\.getpackagetracker\\.com)|(Get_Speed_Checker_CdqYfmDnVO@www\\.getspeedchecker\\.com)|(Get_Speed_Test_Ace_efd65663a0f689684ed5b60337c519ac@www\\.getspeedtestace\\.com)|(Get_Speed_Test_Ace_uNUzxBsIBO@www\\.getspeedtestace\\.com)|(GetOnlineConverter_62dc3ed61ba55b471bc5952d64f22e34@www\\.getonlineconverter\\.com)|(GetOnlineConverter_clone_hmwBWihZaf@www\\.getonlineconverter\\.com)|(Global_Weather_2abbd1bfe1f92474dc556a6abd23c97e@www\\.global-weathern\\.online)|(Global_Weather_qPjJvKHHjn@www\\.global-weathern\\.online)|(Go_Package_Tracker_CDyUCsNhYI@www\\.gopackagetracker\\.com)|(Go_Search_Easy_3b073071b9f9727f2a3c22961bc6aae7@www\\.gosearcheasy\\.com)|(Go_Search_Easy_fCSPxmpWqN@www\\.gosearcheasy\\.com)|(Go_Search_Easy_MTOfbhqqMt@www\\.gosearcheasy\\.com)|(Go_Video_Converter_clone_AtpxxXDpvq@www\\.govideoconverter\\.com)|(Go_Video_Converter_clone_b78d64ef3f6d32d836cb9cb45f64f3d4@www\\.govideoconverter\\.com)|(Go_Video_Converter_clone_ENqGVUhAYo@www\\.govideoconverter\\.com)|(Go_Video_Converter_cXhEAmNOBA@www\\.govideoconverter\\.com)|(Go_Video_Converter_daef20a38c38415e2819357035444fa2@www\\.govideoconverter\\.com)|(Go_Video_Converter_YLdugTFiBs@www\\.govideoconverter\\.com)|(holidaygiftingco@www\\.holidaygifting\\.co)|(Holy_Bible_daily_22c0372905eb878bf90eb726cf1c4cd7@www\\.holybibledaily\\.com)|(Holy_Bible_Daily_36d9839f134dba7393dcd1ce19e35114@www\\.holybibledaily\\.com)|(Holy_Bible_daily_clone_BwGilqVxow@www\\.holybibledaily\\.com)|(Holy_Bible_Daily_clone_TdHKbGqxyp@www\\.holybibledaily\\.com)|(Holy_Bible_daily_WRzbIpWLfR@www\\.holybibledaily\\.com)|(Horoscope_Finder_CQAkIhPhib@www\\.horoscopefinder\\.co)|(Hunt_New_Jobs_MQlcPgVQSY@www\\.huntnewjobs\\.com)|(Hunt_New_Jobs_TeypdaDdsy@www\\.huntnewjobs\\.com)|(Inbox_Tab_1549897502@www\\.inboxtab\\.com)|(Inbox_Tab_LTnzVZlfKv@www\\.inboxtab\\.com)|(Inbox_Tab_rOksOtIddG@www\\.inboxtab\\.com)|(Instant_Doc_Converter_1264fb8b31af7c6c3f56c4c33b825dab@www\\.instantdocconverter\\.com\\.removed)|(Instant_Doc_Converter_clone_LWgHiMFzyN@www\\.instantdocconverter\\.com)|(Instant_Doc_Converter_clone_OeEmGVRjdr@www\\.instantdocconverter\\.com)|(Instant_Doc_Converter_fe41778b25f65137ed606033eea89902@www\\.instantdocconverter\\.com)|(Instant_Doc_Converter_hMMLIeTKQH@www\\.instantdocconverter\\.com)|(Instant_Doc_Converter_UJTpBaxVPV@www\\.instantdocconverter\\.com)|(Instant_Doc_Converter_vZPKbExySk@www\\.instantdocconverter\\.com))$/", + "prefs": [], + "schema": 1576758169317, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9acbbf4c-ef5c-46d4-bc1d-f520b8ced595", + "last_modified": 1576771657093 + }, + { + "guid": "/^((Instant_Email_Login_3fff03c5bbd668d2f7093d5491f66518@www\\.instantemaillogin\\.com)|(Instant_Email_Login_WRGGywlQTy@www\\.instantemaillogin\\.com)|(Instant_Email_Plus_KAZiCvZFWB@www\\.instantemailplus\\.com)|(Instant_Inbox_clone_QKZmbMSVkA@www\\.instantinbox\\.co)|(Instant_Inbox_clone_QmVdpyuWwT@www\\.instantinbox\\.co)|(Instant_Inbox_clone_VlVOmivYUN@www\\.instantinbox\\.co)|(Instant_Inbox_clone_XLjUGcrTgI@www\\.instantinbox\\.co)|(Instant_Inbox_clone_YxmUgAsOrZ@www\\.instantinbox\\.co)|(Instant_Inbox_pzclglfDGq@www\\.instantinbox\\.co)|(Instant_Inbox_qnRYsobGem@www\\.instantinbox\\.co)|(Instant_Maps_clone_clone_zqdkjyMgdQ@www\\.checkmaps\\.co)|(Instant_Maps_ZefayBJOcD_test@www\\.checkmaps\\.co)|(Instant_Maps_ZefayBJOcD@www\\.checkmaps\\.co)|(Instant_Net_Speed_7291e707ced30428e6ed71611c3071ed@www\\.instantnetspeed\\.com)|(Instant_Net_Speed_iEjKvJreAi@www\\.instantnetspeed\\.com)|(Internet_Speed_Test_cf6c9b84dd607bc049898130dd8aa403@www\\.fastinternetspeedtestn\\.com)|(Internet_Speed_Test_RCgVciwcSD@www\\.fastinternetspeedtestn\\.com)|(Job_Search_Online_53e3be5488ce961c12d4341c2e3c830d@www\\.jobssearchonline\\.co)|(Job_Search_Online_clone_20bb80efe94b528ef6fa06730d560cbb@www\\.jobssearchonline\\.co)|(Job_Search_Online_clone_fUDgRmLxrl@www\\.jobssearchonline\\.co)|(Job_Search_Online_clone_jxgyVIROvt@www\\.jobssearchonline\\.co)|(Job_Search_Online_qNOPIVBSge@www\\.jobssearchonline\\.co)|(Job-Portal_dPNxZEZiQF@www\\.job-portal\\.co)|(Jobs_Now_jkAqvfwqNy@jobsnow\\.com)|(Just_Search_Easy_PJxNIOGbZS@www\\.justsearcheasy\\.com)|(justlovepetsonline@www\\.justlovepets\\.online)|(KC_Recipe_Finder_f8541026d8d56f16ba1db6664ac76ed8@www\\.testextension\\.online)|(LifeLock_Safe_Search_LDKQZxofGQ@searchsafe\\.lifelock\\.com)|(Live_Email_4cacc77ba79f2e88d808404b1487e27d@www\\.liveemail\\.co)|(Live_Email_clone_avmFyKjGHH_ach@www\\.liveemail\\.co)|(Live_Email_clone_avmFyKjGHH@www\\.liveemail\\.co)|(Live_Inbox_clone_HSqMlnurhx@www\\.liveinbox\\.co)|(Live_Inbox_ITUElbGJyq@www\\.liveinbox\\.co)|(Live_News_90155c3a670c030b75d27b2b060bab76@www\\.get-news\\.co)|(Live_News_ab96451cfc686d1903b9bd5bbefc58df@www\\.get-news\\.co)|(Live_News_Daily_coRwlwVUtL@www\\.livenewsdaily\\.net)|(Live_News_RoTkoRyzKW@www\\.get-news\\.co)|(Live_News_sQHxsfPWHn@www\\.get-news\\.co)|(Live_News_UPeYgtCNWH@www\\.get-news\\.co)|(liveemail@www\\.liveemail\\.co)|(LiveNewsDaily\\.co_Qrkavccepk@www\\.livenewsdaily\\.co)|(Local_Weather_Now_clone_CGdqWGvDuk@www\\.localweathernow\\.co)|(Local_Weather_Now_clone_PsfbncWCpz@www\\.localweathernow\\.co)|(Local_Weather_Today_af62b67de6656cd77ec1232ce28b0847@www\\.localweathertoday\\.co)|(Local_Weather_Today_b6bdb9471dfdbbe166ebba101bf87987@www\\.localweathertoday\\.co)|(Local_Weather_Today_clone_25d0f678c3de41078bfebfedb36f3e5e@www\\.localweathertoday\\.co)|(Local_Weather_Today_clone_ozBsMkxlfM@www\\.localweathertoday\\.co)|(Local_Weather_Today_clone_QFZOuAbpBy@www\\.localweathertoday\\.co)|(Local_Weather_Today_MoAsKEYZyC@www\\.localweathertoday\\.co)|(localweathertodayco@www\\.localweathertoday\\.co)|(localweathertodaynet@www\\.localweathertoday\\.net)|(Locate_Packages_JIjwUKeces@www\\.locatepackages\\.com)|(loginpro_bTqvLMpNXn@www\\.easy-maillogin\\.link)|(loginpro@www\\.loginemailpro\\.com)|(manifestdata_Check_clone_AraOARfaeL@hjm)|(manifestdata_Check_HFkEdcMQtX@hjm)|(manifestname@www\\.wallpaperonlinepro\\.com)|(Map_Buddy_svMNjHoVFQ@www\\.map-buddy\\.net)|(Map_Directions_Pro_hFzkEkDBXw@www\\.mapdirectionspro\\.co)|(Map_Directions_Pro_JAlSTUeoZW@www\\.mapdirectionspro\\.co)|(Map_Directions_Pro_vnuUbjddlF@www\\.mapdirectionspro\\.co)|(Map_Finder_GaEdNdyQoi@www\\.mapfinder\\.live)|(Map_Finder_Online_clone_HAzjiKHVFr@www\\.mapfinderonline\\.com)|(Map_Finder_Online_clone_vxRStAVXsj@www\\.mapfinderonline\\.com)|(Map_Finder_Online_DWFqZBeElh@www\\.mapfinderonline\\.com)|(Map_Finder_RIBbQLtJhY@www\\.map-finder\\.link)|(Map_Finder_ubFbNkPLbu@www\\.map-finder\\.co)|(Map_My_Journey_clone_dbZghybkTf@www\\.mapmyjourney\\.co)|(Map_My_Journey_gWimMkbjcf@www\\.mapmyjourney\\.co)|(Map_My_Journey_hwaFtGeeRF@www\\.mapmyjourney\\.co)|(Map_My_Journey_lOKajIjWRX@www\\.mapmyjourney\\.co)|(Map_My_Travel_3d7340d4339df15d23c823030d0b3dd4@www\\.mapmytravel1\\.co)|(Map_My_Travel_4c1060a52cb966087a862f50c66cff76@www\\.mapmytravel1\\.co)|(Map_My_Travel_66a1df798c97eecbcbc5ce97f5bb58ce@www\\.mapmytravel\\.co))$/", + "prefs": [], + "schema": 1576758213475, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f0cac174-d15f-427f-b310-9409e69a2ca6", + "last_modified": 1576771657088 + }, + { + "guid": "/^((Map_My_Travel_77440f39b0fc3ac74a9e894ff2e26617@www\\.mapmytravel\\.co)|(Map_My_Travel_ac398c1a8442efc53c141095c6e386f1@www\\.mapmytravel1\\.co)|(Map_My_Travel_b47128735efb1ae5232811bf674a5598@www\\.mapmytravel1\\.co)|(Map_My_Travel_c520fe83319115df915325944097f2d7@www\\.mapmytravel1\\.co)|(Map_My_Travel_clone_906668a067200f92f67c8a1d4934ca53@www\\.mapmytravel\\.co)|(Map_My_Travel_clone_agEGcivVyr@www\\.mapmytravel1\\.co)|(Map_My_Travel_clone_AojrINyqec@www\\.mapmytravel\\.co)|(Map_My_Travel_clone_CaXjHVvzvR@www\\.mapmytravel1\\.co)|(Map_My_Travel_clone_CmwkGssriz@www\\.mapmytravel1\\.co)|(Map_My_Travel_clone_KUUHhqDlkF@www\\.mapmytravel1\\.co)|(Map_My_Travel_clone_xjdUmTvEZE@www\\.mapmytravel\\.co)|(Map_My_Travel_clone_ZJilbRDhlx@www\\.mapmytravel1\\.co)|(Map_My_Travel_ea415ecadf91d96c596c61d748dfab3a@www\\.mapmytravel\\.co)|(Map_My_Travel_FGYxkNvlYu@www\\.mapmytravel1\\.com)|(Map_My_Travel_GnhWycuRWw@www\\.mapmytravel\\.co)|(Map_My_Travel_KoRBtjTOaQ@www\\.mapmytravel\\.co)|(Map_My_Travel_V2_1136cc214788f6e2c4fbd748c875f289@www\\.mapmytravel1\\.co)|(Map_Your_Way_dba6e11787a9a4c0ff802d30e2d84655@www\\.mapyourway\\.online)|(Map_Your_Way_RRrntXMhmV@www\\.mapyourway\\.online)|(Map-Buddy\\.net_YDYeEFqMMB@www\\.map-buddy\\.net)|(MapAssistant\\.net_IxpgMrxoal@www\\.mapassistant\\.net)|(Maps_Finder_clone_lXfNOQsASa@www\\.getmapfinder\\.com)|(Maps_Finder_clone_OHRJTSZWsp@www\\.getmapfinder\\.com)|(Maps_Finder_clone_qjbEfuxypy@www\\.getmapfinder\\.com)|(Maps_Finder_clone_sVixvNGfvq@www\\.getmapfinder\\.com)|(Maps_Finder_clone_XiwisRQHED@www\\.mapsfinder\\.co)|(Maps_Finder_cxamiyXEhG@www\\.maps-finder\\.co)|(Maps_Finder_ikDztWsXOS@www\\.mapsfinder\\.co)|(Maps_Finder_ISFXVmdcMZ@www\\.getmapfinder\\.com)|(Maps_Finder_NEJbQoRTxu@www\\.mymapfinder\\.co)|(Maps_Finder_OAXSzUFBAz@www\\.getmapfinder\\.com)|(Maps_Finder_Online_cb9ff9fa82db81028ec8b5a9c738c3d0@www\\.mymapsfinderonline\\.com)|(Maps_Finder_Online_fa0d46585631975dfefb9653fbdc5ae5@www\\.mapsfinderonline\\.com)|(Maps_Finder_Online_VBUnFyZArH@www\\.mapsfinderonline\\.com)|(Maps_Finder_OyeiUXSYyC@www\\.mymap-buddy\\.com)|(Maps_Finder_yiomudinei@www\\.mapsfinder\\.co)|(Maps_Now_1a47fbf8546a43d949f229efca9a2f34@www\\.mapsnow\\.co)|(Maps_Now_23f9f3cd0b483c22be85bb2568a2df4f@www\\.mapsnow\\.co)|(Maps_Now_85bb8ec9ca1c2baab2dc307af81964c2@www\\.mapsnow\\.co)|(Maps_Now_cbd62915225f7f059c5d15daa3a59cad@www\\.mapsnow\\.co)|(Maps_Now_clone_daDccVKFiR@www\\.mapsnow\\.co)|(Maps_Now_clone_eaIVjwJWMX@www\\.mapsnow\\.co)|(Maps_Now_clone_HmcRPOcjzE@www\\.mapsnow\\.co)|(Maps_Now_clone_QnqJIECgrq@www\\.mapsnow\\.co)|(Maps_Now_clone_VNlrSBkTlM@www\\.mapsnow\\.co)|(Maps_Now_clone_yDfyuWvMMp@www\\.mapsnow\\.co)|(Maps_Now_clone_YDpyMtVaLW@www\\.mapsnow\\.co)|(Maps_Now_clone_ZyNlPAcxdc@www\\.mapsnow\\.co)|(Maps_Now_tmnjrLnmFO@www\\.mapsnow\\.co)|(Maps_Now_uwqucqWEGq@www\\.mapsnow\\.co)|(Maps_Now_XSqhhyPFiH@www\\.mapsnow\\.co)|(Maps_Today_UoKEAlMYFh@www\\.mapmyjourneynt\\.co)|(mapsfinder@www\\.mapsfinder\\.co)|(Metric_Converter_Pro_437e424a1d8a287713fd991b4babb241@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_48e7e1cd7ea7a150ce498c7f4c1a8525@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_c1043f9d35c4300be27a136d2956b8bd@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_clone_b151705ba46c35bf8530f6fcccad39ff@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_clone_HoBPpUTrbw@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_clone_KaiswSnKUV@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_clone_SzGHQnHNkN@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_clone_WchvzpVULZ@www\\.metricconverterpro\\.com)|(Metric_Converter_Pro_rEvcfmsuJW@www\\.metricconverterpro\\.com)|(Movie_Delight_LVlUUJCTfd@www\\.movie-delight\\.com)|(Movie_Delight_xWRWlvWNGj@www\\.movie-delight\\.com)|(Movie_Hunt_GzAuVRhAph@www\\.moviequest\\.online)|(Movie_Hunt_ioLSsIUbVX@www\\.themoviesearch\\.org)|(Movie_Hunt_jbuqRPUUVt@www\\.moviehunt\\.today)|(Movie_Hunt_jHPrIKUDZi@www\\.moviehunt\\.today)|(Movie_Hunt_Online_jJicoNDurf@www\\.moviehunt\\.online)|(Movie_Maniac_2462069208dfe0b3d754ec0d11bf7b40@www\\.themoviemaniac\\.co)|(Movie_Maniac_a84cd1cd277e4886cbd535cafddb9248@www\\.themoviemaniac\\.co)|(Movie_Maniac_clone_4b2e8fc1efa469664fbfe3b1cfd600ce@www\\.themoviemaniac\\.co)|(Movie_Maniac_clone_bbe030355b07de5cd10dee52a60db3e3@www\\.themoviemaniac\\.co))$/", + "prefs": [], + "schema": 1576758242842, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b009e1d2-ed9c-42b3-b795-b63d15b15deb", + "last_modified": 1576771657084 + }, + { + "guid": "/^((Movie_Maniac_clone_FhKByuyzfD@www\\.themoviemaniac\\.co)|(Movie_Maniac_clone_OzkDkQoxcV@www\\.themoviemaniac\\.co)|(Movie_Maniac_clone_pNBsVugeLv@www\\.themoviemaniac\\.co)|(Movie_Maniac_qNRzbWPyVh@www\\.themoviemaniac\\.co)|(Movie_Quest_DgloGEcqoQ@www\\.movie-quest\\.online)|(Movie_Quest_DHlTecEnbH@www\\.moviequest\\.online)|(Movie_Quest_lSUIfjeATU@www\\.movie-quest\\.org)|(Movie_Quest_ogkgHKpiCu@www\\.movie-quest\\.org)|(Movie_Quest_oIbDMPkLHN@www\\.the-moviequest\\.today)|(Movie_Quest_OYXdMYNGlQ@www\\.moviequest\\.online)|(Movie_Quest_WwjFqvwace@www\\.moviequest\\.online)|(Movie_Search_4634ea60272d58007cb594bddf4c2d14@www\\.moviesearchtoday\\.com)|(Movie_Search_5adb28a06e6a557580f892b00df54fe9@www\\.moviesearchtoday\\.com)|(Movie_Search_5e2468caef8b002ec4af47d33347d946@www\\.moviesearchtoday\\.com)|(Movie_Search_6a2835fbcfa21f071a72cf4b1d768ee1@www\\.moviesearchtoday\\.com)|(Movie_Search_82956306d33016e1612f6b93238abb8c@www\\.movie-searchtoday\\.com)|(Movie_Search_amo_hosting_clone_cdvWgyZchs@staging\\.themovie-portal\\.com)|(Movie_Search_clone_BPUjteCIKH@www\\.moviesearchtoday\\.com)|(Movie_Search_clone_hOjSGlUSJc@www\\.themovie-portal\\.com)|(Movie_Search_clone_tUhUWKFWEy@staging\\.themovie-portal\\.com)|(Movie_Search_clone_yNPQCvYjiE@www\\.search-movie\\.today)|(Movie_Search_fce3642a62f1d43d671b9cc1edae2248@www\\.moviesearchtoday\\.com)|(Movie_Search_GnuaPoCHyn@www\\.moviesearchtoday\\.com)|(Movie_Search_HQkuRtYSBi@www\\.movie-search\\.today)|(Movie_Search_JsPzrSgfUw@www\\.moviesearchtoday\\.com)|(Movie_Search_ldxwfXRmwB@www\\.movie-search\\.live)|(Movie_Search_LyOzXmENOz@www\\.movie-search\\.today)|(Movie_Search_OoYYDEappx@www\\.moviesearch\\.today)|(Movie_Search_qQXnYBWtZL@www\\.movie-searchtoday\\.com)|(Movie_Search_qSoQoWSxLl@www\\.movie-hunt\\.co)|(Movie_Search_ryfmFVsOyx@www\\.movie-search\\.today)|(Movie_Search_usVRdBSJFC@www\\.moviesearchtoday\\.com)|(Movie_Search_VbiTYRjDIi@www\\.themoviesearch\\.co)|(Movie_Search_wEfUXCokzy@www\\.moviesearch\\.today)|(Movie_Search_wGdgcYcPQb@www\\.moviesearchtoday\\.com)|(Movie-Hub\\.info_OmWUKRcKuS@www\\.movie-hub\\.info)|(Movie-Quest_KJpvmWXXVL@www\\.movie-quest\\.co)|(Movie-Quest\\.info_eUjlCfSYnv@www\\.movie-quest\\.info)|(Movie-Quest\\.info_kdhRhuajuC@www\\.movie-quest\\.info)|(movie-quest\\.today_ftrItBjuLQ@www\\.movie-quest\\.today)|(MovieQuest_jyRMkNmSGC@www\\.moviequest\\.co)|(moviequest\\.today_JlDnGylmbG@www\\.moviequest\\.today)|(moviequest@www\\.moviequest\\.co)|(MusicQuest_IuWBQBhiuw@www\\.music-quest\\.co)|(mvquest@www\\.moviequest\\.co)|(My_Astro_Tab_8e89559e65a469daf6766bb75d4376ab@www\\.myastrotab\\.com)|(My_Astro_Tab_KaAEnbwPUx@www\\.myastrotab\\.com)|(My_Coupon_Store_ylKTlAUXbo@www\\.mycouponstore\\.co)|(My_Daily_Astrology_MqfKduyqcg@www\\.my-dailyastrology\\.net)|(My_Daily_Utilities_4ebdffa571404d80516d8d0f530af9a6@www\\.mydailyutilities\\.co)|(My_Daily_Utilities_clone_4d4f3e4318db94478d9302c598ef468c@www\\.mydailyutilities\\.co)|(My_Daily_Utilities_clone_TQYRFwiprp@www\\.mydailyutilities\\.co)|(My_Daily_Utilities_clone_wsrcjEDqNR@www\\.mydailyutilities\\.co)|(My_Daily_Utilities_iwpgTQIUFx@www\\.mydailyutilities\\.co)|(My_Directions_Finder_c406a6ac6a2b0638fcb489f1b776b903@www\\.mydirectionsfinder\\.net)|(My_Directions_Finder_clone_alDWaOiCNo@www\\.mydirectionsfinder\\.com)|(My_Directions_Finder_clone_iIYkMlrcwN@www\\.mydirectionsfinder\\.net)|(My_Directions_Finder_clone_mBlfabAHtF@www\\.mydirectionsfinder\\.com)|(My_Directions_Finder_clone_oIFvReYNwJ@www\\.mydirectionsfinder\\.net)|(My_Directions_Finder_clone_TUeunTaGWJ@www\\.mydirectionsfinder\\.net)|(My_Directions_Finder_ejopyTsDPc@www\\.mydirectionsfinder\\.com)|(My_Directions_Finder_RIUfITHqsm@www\\.mydirectionsfinder\\.com)|(My_Doc_Converter_a70620b85cb93c122c397d5a0d6d6c17@www\\.mydocconverter\\.net)|(My_Doc_Converter_hTuhuUWAbu@www\\.mydocconverter\\.net)|(My_File_Converter_InJpDFniec@www\\.myfileconverter\\.net)|(My_File_Converter_MOLroWzmvk@www\\.myfileconverter\\.net)|(My_File_Converter_uJJIGUseRO@www\\.myfileconverter\\.org)|(My_Mail_Center_clone_AOBAQIlpph@www\\.mymailcenter\\.co)|(My_Mail_Center_clone_JJSlJDujLf@www\\.mymailcenter\\.co)|(My_Mail_Center_StbPlXiGzU@www\\.mymailcenter\\.co)|(My_Map_Buddy_AHxkFzHUSq@www\\.mymap-buddy\\.com)|(My_Map_Buddy_aJhRltItzB@www\\.mymap-buddy\\.co)|(My_Map_Buddy_dzzNGYdgEy@www\\.mymapbuddy\\.link)|(My_Map_Buddy_lCpBZhZcug@www\\.mymap-buddy\\.co))$/", + "prefs": [], + "schema": 1576758270991, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c264de14-d096-45d7-afd6-4e03f3f0874e", + "last_modified": 1576771657080 + }, + { + "guid": "/^((My_Map_Buddy_LlytpwAxzJ@www\\.my-mapbuddy\\.link)|(My_Map_Buddy_omjWWfWnZV@www\\.checkmaps\\.live)|(My_Map_Buddy_QniENdnZwH@www\\.mymapbuddy\\.co)|(My_Map_Buddy_qwjuFaLDiS@www\\.my-mapbuddy\\.link)|(My_Map_Finder_216cea355b90081b2fa2b35228afa8cd@www\\.mymapfinder\\.net)|(My_Map_Finder_FQhLxNAhOb@www\\.mymapfinder\\.net)|(My_Map_Finder_HIAdZUsdqP@www\\.easymapsfinder\\.com)|(My_Map_Key_bfebaaffd5e89eb58d8eede5550b7657@www\\.mymapkey\\.com)|(My_Map_Key_PNKwkyDcSv@www\\.mymapkey\\.com)|(My_Map_Quest_amtyvbVOvw@www\\.mymapquest\\.co)|(My_Map_Tab_e4256b0cd83dd5dc7ffd2d6fa89bb3ca@www\\.mymaptab\\.co)|(My_Map_Tab_fQPfsnZVRi@www\\.mymaptab\\.co)|(My_Maps_Daily_2216a0fb9fa410e585f75583c84bb1fc@www\\.mymapsdaily\\.com)|(My_Maps_Daily_HTtwoSgSmO@www\\.mymapsdaily\\.com)|(My_Maps_Finder_mlqpWnYOjs@www\\.mymapsfinderonline\\.com)|(My_Maps_Now_wouVnzxahG@www\\.mymapsnow\\.co)|(My_Metric_Converter_AZzUIIKwJY@www\\.mymetricconverter\\.com)|(My_Net_Speed_clone_LEPCOrGXJt@www\\.my-netspeed\\.co)|(My_Net_Speed_dIUrWuilrC@www\\.my-netspeed\\.co)|(My_Net_Speed_hQVahLtcSK@www\\.mynet-speed\\.com)|(My_Net_Speed_keJWBLUIYD@www\\.mynet-speed\\.com)|(My_Net_Speed_XhDnHJmvAF_1@www\\.mynetspeed\\.co)|(My_Net_Speed_XhDnHJmvAF@www\\.mynetspeed\\.co)|(My_Net_Speed_YGdxRAMaly@www\\.mynetspeed\\.link)|(My_Quick_Directions_384f670ee31df7278a1df86a202697c2@www\\.myquickdirections\\.com)|(My_Quick_Directions_IoipCjCqHK@www\\.myquickdirections\\.com)|(My_Quick_Search_HHmoLNFNZK@www\\.myquicksearch\\.co)|(My_Radio_Plus_clone_xijbeLEKut@www\\.myradioplus\\.co)|(My_Radio_Plus_clone_yLODfWIUzo@www\\.myradioplus\\.co)|(My_Radio_Plus_huJGcbKJdz@www\\.myradioplus\\.co)|(My_Recipe_Digest_806bc9c3d627874ac6f6c953e1174d41@www\\.myrecipedigest\\.com)|(My_Recipe_Digest_TQhDINXjet@www\\.myrecipedigest\\.com)|(My_Recipe_Guide_4bc8a248146602106b25349ec50b7395@www\\.myrecipeguideonline\\.com)|(My_Recipe_Guide_ZlrLqLHAKb@myrecipeguideonline\\.com)|(My_Route_Planner_6bb055b2be5912b90cec3a2788636830@www\\.myrouteplanner\\.co)|(My_Route_Planner_ccdd62d844c1769d5bf4db308b3bc9c9@www\\.myrouteplanner\\.co)|(My_Route_Planner_clone_AFCMlCneMs@www\\.myrouteplanner\\.co)|(My_Search_Plus_91de35f6bc26e7f423a701dc4a7d9e6b@www\\.mysearchplus\\.co)|(My_Search_Plus_clone_7d6359f4c55d1f5c5a9af0304e7dcf34@www\\.mysearchplus\\.co)|(My_Search_Plus_clone_ejVWzjJsWF@www\\.mysearchplus\\.co)|(My_Search_Plus_clone_fBHEaWXiDZ@_123456www\\.mysearchplus\\.co)|(My_Search_Plus_clone_fBHEaWXiDZ@www\\.mysearchplus\\.co)|(My_Search_Plus_clone_zHyeSWPlyQ@www\\.mysearchplus\\.co)|(My_Search_Plus_zasNrdaCMb@www\\.mysearchplus\\.co)|(My_Search_Wizard_7f2979e35d2bd7dd61433bbca4e930bf@www\\.mysearchwizard\\.co)|(My_Search_Wizard_887bc211e6c12bcd4c59204553e75ed5@www\\.mysearchwizard\\.co)|(My_Search_Wizard_clone_CuEYdreCCG@www\\.mysearchwizard\\.co)|(My_Search_Wizard_clone_MfzUjTpTSj@www\\.mysearchwizard\\.co)|(My_Search_Wizard_clone_PhkuhYznxq@www\\.mysearchwizard\\.co)|(My_Smart_Search\\.co_uUlxGOYDUP@www\\.mysmartsearch\\.co)|(My_Smart-Search\\.co_moHloXmlvd@www\\.mysmart-search\\.co)|(My_Smart-Search\\.co_nVQpezjwFN@www\\.mysmart-search\\.co)|(My_Weather_Services_MTCOGCZgEo@myweatherservicesn\\.org)|(My_Weather_Tab_cdb3a5fdbcc268762efb7f1b9088a935@www\\.myweathertab\\.co)|(My_Word_Scribe_6ac6fa9173610517a627c5298a38891a@www\\.mywordscribe\\.com)|(My_Word_Scribe_clone_RZEMcCVoIU@www\\.mywordscribe\\.com)|(My_Word_Scribe_clone_TAZVHOvofp@www\\.mywordscribe\\.com)|(My_Word_Scribe_GlqExrIfvA@www\\.mywordscribe\\.com)|(My_Word_Scribe_wRguUlbptJ@www\\.mywordscribe\\.com)|(MyAstroFinder_clone_meuCieSJGq@www\\.myastrofinder\\.co)|(MyAstroFinder_QqSSwnxQpy@www\\.myastrofinder\\.co)|(MyCareer-Search\\.co_IcpqItVxAi@www\\.mycareer-search\\.co)|(MyCoupon-Finder\\.co_DatSgecXGq@www\\.mycoupon-finder\\.co)|(MyCouponFinder\\.co_ZMMzlIEann@www\\.mycouponfinder\\.co)|(mydailyastrologyco@www\\.mydailyastrology\\.co)|(mydailyastrologyonline@www\\.mydailyastrology\\.online)|(mydailynewsonline@www\\.mydailynews\\.online)|(MyMapBuddy\\.net_oVKQZzxNzk@www\\.mymapbuddy\\.net)|(MyMusicSearch\\.co_zsqyyUdIfE@www\\.mymusicsearch\\.co)|(mynetspeedco@www\\.mynetspeed\\.co)|(MySearchTab\\.co_QXqKBErWIz@www\\.mysearchtab\\.co)|(Net_Speed_bfa1870197a5380d1f00708341444fe4@www\\.testnetspeed\\.co)|(Net_Speed_f61989f67aeb3c5ed6180f49b2160ea1@www\\.net-speed\\.co)|(Net_Speed_imtTaleZMk@www\\.net-speed\\.co))$/", + "prefs": [], + "schema": 1576758309063, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a3e1bff4-2e6a-4a7f-a1e3-f7eff451655b", + "last_modified": 1576771657076 + }, + { + "guid": "/^((Net_Speed_Ninja_rJSmpzPQRq@www\\.netspeedninja\\.co)|(Net_Speed_nYAhXCpaOF@www\\.testnetspeed\\.co)|(Net_Speed_Plus_MnPDDtnfZh@www\\.checkmyspeednownt\\.com)|(Net_Speed_Test_b419d25436f7b7cb3458407498bae21f@www\\.netspeedtesthub\\.com)|(Net_Speed_Test_Hub_OOlSMbcwOe@netspeedtesthub\\.com)|(Net_Speedtest_8e202a4f66675f188c265a8ef11e47fa@www\\.netspeedtest\\.co)|(Net_Speedtest_clone_ikAadlLvLs@www\\.netspeedtest\\.co)|(Net_Speedtest_ysCFtRLxZn@www\\.netspeedtest\\.co)|(NetSpeedCalculator\\.net_mzMnaouenc@www\\.netspeedcalculator\\.net)|(netspeedproco@www\\.netspeedpro\\.co)|(New_Jobs_Quest_8f6247b00ffc5b1bb2f3f30fc73a9de5@www\\.newjobsquest\\.co)|(News__Precinct_75018cea70cf57e0ab66d312ef3a769d@www\\.newsprecinct\\.com)|(News__Precinct_sggnBoSvRu@www\\.newsprecinct\\.com)|(News_Daily_UBtJmysrDA@www\\.newstrackrnt\\.co)|(News_Headlines_bKwUmAeFcG@www\\.newsheadlinespro\\.com)|(News_Headlines_f70874554c3c22b5fec6f98e845d9b4d@www\\.newsheadlinespro\\.com)|(News_Headlines_SfZxlPzEgN@www\\.newsheadlinespro\\.com)|(News_Precinct_05544a082b3c01a286c66f7cf4251700@www\\.newsprecinct\\.com)|(News_Precinct_clone_5cd166a363627a7f453b1b69769f436d@www\\.newsprecinct\\.com)|(News_Precinct_clone_NeRHwQGodq@www\\.newsprecinct\\.com)|(News_Precinct_clone_PTdBCNrWPD@www\\.newsprecinct\\.com)|(News_Precinct_dLqSikAjQC@www\\.newsprecinct\\.com)|(News_Precinct_gzVnHZnzOy@www\\.newsprecinct\\.com)|(NewStackr_uNFYKFwAWP@www\\.NewsTackR\\.com)|(NewsTrackr_clone_qKtwLAHfjm@www\\.newstrackr\\.co)|(NewsTrackr_clone_vJczLpOPSO@www\\.newstrackr\\.co)|(NewsTrackr_e1032761f4eb1beb126fdface070fb37@www\\.newstrackr\\.co)|(NewsTrackr_iDEHLRYWqj@www\\.newstrackr\\.co)|(NewsTrackr_ZtEQVEonqX@www\\.newstrackr\\.co)|(newsupdatesinnet@www\\.newsupdates\\.in\\.net)|(no_EzdEKVkeZk@www\\.test10\\.com)|(Online_Coupon_Finder_njhVhBdgcg@onlinecoupon-finder\\.co)|(Online_Coupon_Finder_QuNkIQQUVz@www\\.onlinecouponfinder\\.net)|(Online_Coupon_Finder_SZuYIubYMN@www\\.onlinecouponfinder\\.net)|(Online_Doc_Converter_7d7d0b74adad02f39590d705822a70e2@www\\.onlinedocconverter\\.com)|(Online_Doc_Converter_qpIkrHZegO@www\\.onlinedocconverter\\.com)|(Online_Document_Converter_clone_xZguXlOjJE@www\\.convertmypdf\\.co)|(Online_Package_Tracker_TNAcobvKgm@onlinepackagetracker\\.co)|(Online_PDF_Converter_aJsvvVEBaO@www\\.myonlinepdfconverter\\.net)|(Online_PDF_Converter_kxVNqagGZG@myonlinepdfconverter\\.com)|(Online_Recipe_RURMecAQzt@www\\.onlinerecipe\\.co)|(Online_Recipe_VNtLUGYjpI@www\\.onlinerecipe\\.co)|(Online_Speed_Radar_clone_YlZLvnSGjM@www\\.onlinespeedradar\\.com)|(Online_Web_Search_clone_WjoqJJklvS@www\\.onlinewebsearch\\.co)|(Online_Web_Search_d1b9b3e703905b67931858f6f5aecdca@www\\.onlinewebsearch\\.co)|(OnlineRecipeSearch\\.info_pEdBAUaqCR@www\\.onlinerecipesearch\\.info)|(Package_Trace_JOGQGobPMj@www\\.trackpackagequicknt\\.com)|(Package_Tracker_02a37a57871ec5b57a0555403157dcf8@www\\.package-tracker\\.co)|(Package_Tracker_07a2a700968a7da88eb77b9c46279b1c@www\\.dailypackagetracker\\.com)|(Package_Tracker_3ce363c30f45ee0016862ac499be7ff5@www\\.packagetrackeronline\\.com)|(Package_Tracker_698579ac9c231e14ca69e1894f353396@www\\.dailypackagetracker\\.com\\.disabled)|(Package_Tracker_75a19df973eae7cf6e19c396c3973451@www\\.package-tracker\\.co)|(Package_Tracker_clone_FNZBehhrfr@www\\.package-tracker\\.co)|(Package_Tracker_EOXBmHOBkZ@www\\.package-tracker\\.co)|(Package_Tracker_Express_clone_cbOTQhTNgs@www\\.packagetrackerexpress\\.com)|(Package_Tracker_Express_ZxaTHkYQxF@www\\.packagetrackerexpress\\.com)|(Package_Tracker_fd87086da3f36037204f4b0bf23f1ea1@www\\.package-tracker\\.co)|(Package_Tracker_HaMksfdhUi@www\\.dailypackagetracker\\.com)|(Package_Tracker_Hub_clone_cfdJxpAmIT@www\\.packagetrackerhub\\.com)|(Package_Tracker_Hub_EreNcpVUoK@www\\.packagetrackerhub\\.com)|(Package_Tracker_kaiHEFsqvp@www\\.ThePackageTrack\\.com)|(Package_Tracker_KwwfXjTWsn@www\\.package-tracker\\.co)|(Package_Tracker_OHnRIWuvmH@www\\.packagetrackeronline\\.com)|(Package_Tracker_Online_b49f3fc69d90679955379dab26b2a6fa@www\\.packagetrackeronline\\.com)|(Package_Tracker_Online_cleZBgkKxj@www\\.packagetrackeronline\\.com)|(Package_Tracker_Online_EmvHhroAmG@www\\.packagetrackeronline\\.com)|(Package_Tracker_QYmRTqNSxY@www\\.getpackagetracker\\.com)|(Package_Tracker_Tab_50fa581c36cad6995f117782f5105636@www\\.packagetrackertab\\.com))$/", + "prefs": [], + "schema": 1576758357512, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4bc321ad-e23b-4ed3-86bf-a1a3a38fbf75", + "last_modified": 1576771657072 + }, + { + "guid": "/^((Package_Tracker_yJbpZEQECr@www\\.package-tracker\\.co)|(packagefollowercom@www\\.packagefollower\\.com)|(PDF_Converter_AQpxoneXlZ@www\\.pdf-convertn\\.co)|(PDF_Converter_Plus_ZgLTNbAZUb@www\\.pdfconverterplus\\.com)|(PDF_Pro_Converter_eTqIXytHWq@www\\.pdfproconverter\\.com)|(Pets_Wallpapers_SKWDasMHYY@petswallpapers\\.net)|(phototab@www\\.wallpaperonlinepro\\.com)|(Premier_Search_clone_CcAEnLGkmK@www\\.premiersearch\\.co)|(Premier_Search_clone_wfigyWtsdL@www\\.premiersearch\\.co)|(Privacy-Search\\.biz_cXHpGjbGct@www\\.privacy-search\\.biz)|(Privacy-Search\\.club_fNwUjmGvkM@www\\.privacy-search\\.club)|(Privacy-Search\\.company_gzqnuNM@www\\.privacy-search\\.company)|(Privacy-Search\\.in\\.net_UdIeydXEbw@www\\.privacy-search\\.in\\.net)|(Privacy-Search\\.info_BzTWanpKDM@www\\.privacy-search\\.info)|(Privacy-Search\\.link_DATRCktqwk@www\\.privacy-search\\.link)|(Privacy-Search\\.one_NPLmbtQaBI@www\\.privacy-search\\.one\\.disabled\\.com)|(Privacy-Search\\.online_PwQLiKaWGq@www\\.privacy-search\\.online)|(Privacy-Search\\.site_zYtZQgkEtf@www\\.privacy-search\\.site)|(Privacy-Search\\.space_AjqewVnBVW@www\\.privacy-search\\.space)|(Privacy-Search\\.store_vNAqbjRGGn@www\\.privacy-search\\.store)|(Privacy-Search\\.today_NRVdCCeOIa@www\\.privacy-search\\.today)|(Privacy-Search\\.website_JwizWmtjPu@www\\.privacy-search\\.website)|(Privacy-Search\\.xyz_clone_juEABfQNOl@www\\.privacy-search\\.xyz)|(Privacy-Search\\.xyz_elSDqUATPt@www\\.privacy-search\\.xyz\\.disabled\\.com)|(Privacy-Search\\.xyz_oIkblTLPSN@www\\.privacy-search\\.xyz)|(PrivacySearch\\.biz_bAwIbiJVAP@www\\.privacysearch\\.biz)|(PrivacySearch\\.club_MRWkilEqOZ@www\\.privacysearch\\.club)|(PrivacySearch\\.company_hOwSWgQKdn@www\\.privacysearch\\.company\\.disabled\\.com)|(PrivacySearch\\.company_NAKTVsEmEC@www\\.privacysearch\\.company)|(PrivacySearch\\.in\\.net_NxaHlhdTlz@www\\.privacysearch\\.in\\.net)|(PrivacySearch\\.link_EOhtykDnGr@www\\.privacysearch\\.link)|(PrivacySearch\\.me_QSSIQcOMtH@www\\.privacysearch\\.me)|(PrivacySearch\\.one_WUIHjcIjXh@www\\.privacysearch\\.one)|(PrivacySearch\\.one_zOkggTAokc@www\\.privacy-search\\.one)|(PrivacySearch\\.online_CYiUatShgl@www\\.privacysearch\\.online)|(PrivacySearch\\.space_YagHPcSikB@www\\.privacysearch\\.space)|(PrivacySearch\\.store_pJBFyyaDRx@www\\.privacysearch\\.store)|(PrivacySearch\\.website_cGZdtKrFao@www\\.privacysearch\\.website)|(PrivacySearch\\.xyz_ZEUMZLHcqv@www\\.privacysearch\\.xyz)|(privacysearchco_xXdgBRl@info\\.privacysearch\\.co)|(privacysearchco@info\\.privacysearch\\.co)|(PrivateSearchOnline\\.com_IhKdmYiVeK@info\\.privatesearchonline\\.com)|(Pro_Games_Online_HVwluXulrt@www\\.progamesonline\\.co)|(Puppies_World_trFanlsdqu@www\\.puppiesworld\\.info)|(Puppy_Wallpapers_ZOWpJDLXFj@www\\.puppywallpapers\\.net)|(Quick_Career_Search_WDeLsGajVb@www\\.quickcareersearch\\.co)|(Quick_Daily_Mail_SKeVfXlhFp@www\\.quickdailymail\\.com)|(Quick_Email_App_EFITirXRRs@www\\.quickemailapp\\.com)|(Quick_Email_Checker_7796dc8c3c9cde3502870b6054bb34db@www\\.quickmailchecker\\.com)|(Quick_Email_Checker_BQerPmohdb@www\\.quickmailchecker\\.com)|(Quick_Map_Tab_98eb6af566aadaea6dae58aa6d841a70@www\\.quickmaptab\\.com)|(Quick_Map_Tab_IJTJmbzNOx@www\\.quickmaptab\\.com)|(Quick_Maps_clone_hIZNOOMdWQ@www\\.getquickmaps\\.com)|(Quick_Maps_clone_xRcOZiPvYl@www\\.getquickmaps\\.com)|(Quick_Maps_Online_7bcf0c280d92934d11d0b449980a1937@www\\.quickmapsonline\\.com)|(Quick_Maps_Online_QKDpNHmDpD@www\\.quickmapsonline\\.com)|(Quick_Maps_pjWqGYHPRE@www\\.getquickmaps\\.com)|(Quick_Online_Directions_0364feaec027aaab80a920e79ac1800c@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_clone_8cfe33dcf14406257fca53f436aad1ff@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_clone_bFKYEkYGUO@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_clone_JcSWuSfSrw@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_clone_qswqvmCxux@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_d922039a0497633824e490148707ca71@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_eddee69bb3984d22264d57b63e6f0115@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_OFYSSUWMOH@www\\.quickonlinedirections\\.com)|(Quick_Online_Directions_Test_gbFqFQwwaZ@www\\.quickonlinedirections\\.com)|(Quick_Recipe_Search_QumKWdGMdB@www\\.quickrecipesearch\\.today)|(Quick_Search_Tab_Cbzrnmuqub@www\\.quicksearchtab\\.co))$/", + "prefs": [], + "schema": 1576758405945, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "402be073-072f-44d7-a557-7c98914dc0da", + "last_modified": 1576771657067 + }, + { + "guid": "/^((Quick_Speed_Checker_09b7f7d1acd76f23f1d887e60ddcc474@www\\.quickspeedchecker\\.com)|(Quick_Speed_Checker_clone_3db2cd8f8f3e192f57ca83da50451660@www\\.quickspeedchecker\\.com)|(Quick_Speed_Checker_clone_bhRDSCySFq@www\\.quickspeedchecker\\.com)|(Quick_Speed_Checker_sqZXXzzpsT@www\\.quickspeedchecker\\.com)|(Quick_Weather_Forecast_IHOnRApsey@www\\.quickweatherforecast\\.com)|(Quick_Weather_Updates_6277aeb2d83428ca469904f0886e0938@www\\.quickweatherupdates\\.com)|(Quick_Weather_Updates_yieimwNJEx@www\\.quickweatherupdates\\.com)|(QuickNews\\.co_vZLHaiMlVV@www\\.quicknews\\.co)|(QuickRecipeSearch\\.co_hqhPDvuKrF@www\\.quickrecipesearch\\.co)|(qwertyuiopasdf@www\\.xyz\\.com)|(Radio_Hub_Online_YfJreNCpww@www\\.radiohubonline\\.com)|(rakshitchoudhary8@gmail\\.com)|(rakshitchoudhary8@gmail\\.om)|(Recipe_Board_Plus_7484a472b0b409e4895321bdd72e1945@www\\.recipeboardplus\\.com)|(Recipe_Board_Plus_f92fd9f713f932491e785a977401bb7c@www\\.recipeboardplus\\.com)|(Recipe_Board_Plus_gpeHSqIYiT@www\\.recipeboardplus\\.com)|(Recipe_Board_Plus_hYpxDyhZpP@www\\.myrecipeguideonline\\.com)|(Recipe_Board_Plus_pfyXDonFTr@www\\.recipeboardplus\\.com)|(Recipe_Book_Search_d62d1a871fc0f1d38ecf6b7653ae688b@www\\.recipebooksearch\\.com)|(Recipe_Book_Search_MRJMOtSRZt@www\\.recipebooksearch\\.com)|(Recipe_Book_Tab_3deea22a93f8e99342510c9cb16ccdf4@www\\.recipebooktab\\.com)|(Recipe_Guide_Plus_41c1f7d66a164e59a4721902d4a0a6c5@www\\.recipeguideplus\\.com)|(Recipe_Guide_Plus_dHGfHpqpFD@www\\.recipeguideplus\\.com)|(Recipe_Online_gwFoqximpy@www\\.recipeonline\\.net\\.test)|(Recipe_Search_Guide_e1d73ceb736fe3f0cf7b0da6063a92b3@www\\.recipesearchguide\\.com)|(Recipe_Search_Guide_mbFGBVqpia@www\\.recipesearchguide\\.com)|(Recipe_Search_Plus_5bc244154d5ed1b0fa3e2ff8e788ddbd@www\\.recipesearchplus\\.com)|(Recipe_Search_Plus_FxKIeZgMHD@www\\.recipesearchplus\\.com)|(Recipes_by_Alot_klEAJdojHM@www\\.mydailyrecipes\\.co)|(sahil@searchprivacy\\.co)|(Satellite_Maps_Now_aPVhlrXyLu@www\\.satellitemapsnow\\.com)|(Say_Rosary_7fbf455ac8a8d9319ec8e9c888bbc8f0@www\\.sayrosary\\.com)|(Say_Rosary_IoTPeJHVYN@www\\.sayrosary\\.com)|(Search_Anonymous_3914bca7a10a50db91f284a844eeec2f@www\\.searchanonymous\\.co)|(Search_Anonymous_kHrtzTbIUW@www\\.searchanonymous\\.co)|(Search_Atlas_8c11f710a756258b85fce73f36080a00@www\\.searchatlas\\.co)|(Search_Atlas_a65471976592d8afb32ab1d355329f4f@www\\.searchatlas\\.co)|(Search_Atlas_fgPWjhrMII@www\\.searchatlas\\.co)|(Search_Atlas_JvMkNyzBew@www\\.searchatlas\\.co)|(Search_Beacon_App_eqehzqftmN@www\\.searchbeaconapp\\.com)|(Search_Center_clone_jwMueYZfAC@www\\.searchcenter\\.co)|(Search_Central_c4359af19cd8d0828733d75832e3bb87@www\\.searchcentral\\.co)|(Search_Direct_Pro_qkzlUJiGov@www\\.searchdirectpro\\.com)|(Search_Easy_008d08b3ff05afbcae6df5b92296a6d0@www\\.search-easy\\.co)|(Search_Easy_Go_577387e8b765f721486040e639398c71@www\\.searcheasygo\\.com)|(Search_Easy_Go_80d5131cc8b26c5c4af3f4aa83886f7f@www\\.searcheasygo\\.com)|(Search_Easy_Go_AvYrOIFWDF@www\\.searcheasygo\\.com)|(Search_Easy_Hub_2f45453af53121c146d7bf5c3dd914c3@www\\.searcheasyhub\\.com)|(Search_Easy_kDZUkxUbSP@www\\.search-easy\\.co)|(Search_Easy_Plus_3f7d63b42346d0eb02176aa32f33b9a4@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_8965c3185ad17bdd1c759078bcde92f0@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_8d7d969cf1d24004447fa0066e4dfd02@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_8f70d67adfbdd9aa623aa2057efffc8b@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_9e6663ee2c4803fe1e7bf20b90f26e96@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_aMOxRncWIJ@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_cdb1d9b83ae0801f912e8b73cac0836f@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_dedOWbqqXd@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_DHIlDaCkwl@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_dWFEmfADcn@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_eRdOVuRKeX@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_HlNUDYUqua@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_njXAdPCyfZ@www\\.searcheasyplus\\.com)|(Search_Easy_Plus_rrXYlzwRaJ@www\\.searcheasyplus\\.com)|(Search_Easy_Pro_20670965804c5d16d21370f657dcdca0@www\\.searcheasypro\\.com)|(Search_Easy_Pro_73aa350eceb1758d88bd3edb17b26a95@www\\.searcheasypro\\.com)|(Search_Easy_Pro_VDbJUdDuEi@www\\.searcheasypro\\.com)|(Search_Easy_Web_ad43024621fcd724bf50596e41f6852f@www\\.searcheasyweb\\.com))$/", + "prefs": [], + "schema": 1576758467216, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b0054d00-739f-43e5-a6a4-22a56fd09844", + "last_modified": 1576771657063 + }, + { + "guid": "/^((Search_Easy_Web_tvqEdWRwUI@www\\.searcheasyweb\\.com)|(Search_Express_CgqiRHXtYw@www\\.yoursearchexpress\\.com)|(Search_For_Maps_gLRdugmyXV@www\\.search-maps\\.net)|(Search_Plus_090a3f242292c36b80d33fd7e48aeb84@www\\.search-plus\\.co)|(Search_Plus_App_2d6fef8895df8a9a67c9bd3b15438abf@www\\.searchplusapp\\.com)|(Search_Plus_App_zZmjIdjudH@www\\.searchplusapp\\.com)|(Search_Plus_lPNIzNDSuX@www\\.search-plus\\.co)|(Search_Plus_Now_04ba3f2b13d728af94433e016d46239d@www\\.searchplusnow\\.com)|(Search_Plus_Now_312928a70ba3909e221b0524753bf126@www\\.weatherforecasttracker\\.com)|(Search_Plus_Now_7a64f4abfa7dba3075cabf9896c8fa19@www\\.searchplusnow\\.com)|(Search_Plus_Now_81c0506ff8d40fb8a7daa44222962215@www\\.searchplusnow\\.com)|(Search_Plus_Now_clone_sgREjdpaZx@www\\.searchplusnow\\.com)|(Search_Plus_Now_clone_SMpbwlnYxa@www\\.searchplusnow\\.com)|(Search_Plus_Now_fuKTpdNeOn@www\\.mapmytravel\\.co)|(Search_Plus_Now_RUxZwdNBtU@www\\.searchplusnow\\.com)|(Search_Plus_Now_YQIFAskSvi@www\\.weatherforecasttracker\\.com)|(Search_Plus_Pro_248f4aa071807a8d8f35bf57aef7b217@www\\.mapmytravel\\.co)|(Search_Plus_Pro_c5202d6e9fff0fb53512d681772f0244@www\\.searchpluspro\\.com)|(Search_Plus_Pro_cBmYtdkkFb@www\\.searchpluspro\\.com)|(Search_Plus_Pro_clone_egAEtDTMkX@www\\.searchpluspro\\.com)|(Search_Plus_Pro_clone_KRXJEhDHND@www\\.searchpluspro\\.com)|(Search_Plus_Pro_clone_odzZVUScjD@www\\.searchpluspro\\.com)|(Search_Plus_Pro_clone_SpGuCbnjPE@www\\.searchpluspro\\.com)|(Search_Plus_Pro_d67fb2263b88d9d41bae10a6e2ae855b@www\\.searchpluspro\\.com)|(Search_Plus_Pro_f7d126d7141628c1b739c58997b082b2@www\\.searchpluspro\\.com)|(Search_Plus_Pro_f889c071fa2a90062f8eeb4e7fd56db6@disable\\.searchpluspro\\.com)|(Search_Plus_Pro_fab7d78fa6444068be71b39db91e15ff@www\\.searchpluspro\\.com)|(Search_Privacy_clone_ozOdXpYKTM@www\\.search-privacy\\.live)|(Search_Privacy_clone_UDbsERGbgS@www\\.privacysearch\\.news)|(Search_Privacy_dIeFDdYUHg@www\\.searchprivacy\\.xyz)|(Search_Privacy_dKaGZWBFIM@www\\.searchprivacy\\.today)|(Search_Privacy_jpagAkMKvy@www\\.search-privacy\\.today)|(Search_Privacy_llZnkTGcOa@www\\.searchprivacy\\.today)|(Search_Privacy_naGfXaprzJ@info\\.searchprivacy\\.co)|(Search_Privacy_PQZCFxQkZX@www\\.privacysearch\\.news)|(Search_Privacy_qUxDJagEVN@www\\.searchprivacy\\.live)|(Search_Privacy_uVBxmwGLSk@www\\.privacy-search\\.works)|(Search_Privacy_wZVkQUHrHw@www\\.privacysearch\\.live)|(Search_Privacy_ZpneZaXeZp@www\\.search-privacy\\.live)|(Search_Privacy_ZvcIJuofJd@www\\.privacy-search\\.org)|(Search_Recipe_Pro_c299176cc8e5b03509bc091d92718516@www\\.searchrecipepro\\.com)|(search_Recipe_Pro_hZSnuXpHlI@www\\.searchrecipepro\\.com)|(Search_Secure_5fe09050cfe3944c0782c53e6aead42f@www\\.searchsecure\\.co)|(Search_Secure_5fe09050cfe3944c0782c53e6aead42fhsadghs@www\\.searchsecure\\.co)|(Search_Secure_clone_haBhKCwJbD_34@www\\.searchsecurepro\\.co)|(Search_Secure_clone_haBhKCwJbD1@www\\.searchsecurepro\\.co)|(Search_Secure_PUhUZXSDqx@www\\.searchsecure\\.co)|(Search_Select_804ce00595966fa5ced4efb6e0919d6f@www\\.searchselect\\.co)|(Search_Select_FQCSJqYZdP@www\\.searchselect\\.co)|(Search_Voyager_91c3275d156ab7272e24590b4f7f49d6@www\\.searchvoyager\\.co)|(Search_Voyager_PgzBPOqQhh@www\\.searchvoyager\\.co)|(search-movie\\.net_cURRAPTBgN@www\\.search-movie\\.net)|(search-movie\\.online_PqiotXlNOM@www\\.search-movie\\.online)|(search-movie\\.today_jpmWHncsRI@www\\.search-movie\\.today)|(Search-Privacy\\.biz_ZPdXCSGFgO@www\\.search-privacy\\.biz)|(Search-Privacy\\.club_CtlbxhsuVt@info\\.search-privacy\\.club)|(Search-Privacy\\.desi_AdRXpDarGi@info\\.search-privacy\\.desi)|(Search-Privacy\\.in\\.net_WibIleNEHx@info\\.search-privacy\\.in\\.net)|(Search-Privacy\\.info_rsNLqucdiG@info\\.search-privacy\\.info)|(Search-Privacy\\.link_wDxGmBgqcl@www\\.search-privacy\\.link)|(Search-Privacy\\.me_AvAriQFEhU@www\\.search-privacy\\.me)|(Search-Privacy\\.net_nWVLWwvLtN@www\\.search-privacy\\.net)|(Search-Privacy\\.online_cYKHeBuuMA@www\\.search-privacy\\.online)|(Search-Privacy\\.org_HWNXCRCnGt@info\\.search-privacy\\.org)|(Search-Privacy\\.site_qacnidOLmc@www\\.search-privacy\\.site)|(Search-Privacy\\.space_FjfWuiGHqH@info\\.search-privacy\\.space)|(Search-Privacy\\.store_xyaGgMqtWA@www\\.search-privacy\\.store)|(Search-Privacy\\.website_smicMDEmxB@www\\.search-privacy\\.website)|(Search-Privacy\\.xyz_gBlCbCGBMh@info\\.search-privacy\\.xyz))$/", + "prefs": [], + "schema": 1576758508310, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4f75a73b-1541-4d50-b7c7-4d3524619899", + "last_modified": 1576771657059 + }, + { + "guid": "/^((Search-Tab\\.net_qpBMgxRRKX@www\\.search-tab\\.net)|(searchmovie\\.today_rRBhPdnLIB@www\\.searchmovie\\.today)|(SearchPrivacy\\.Biz_TQxQMmZjax@info\\.searchprivacy\\.biz)|(SearchPrivacy\\.club_sbgzeaMJld@info\\.searchprivacy\\.club)|(SearchPrivacy\\.company_lbwegvHahG@www\\.searchprivacy\\.company)|(SearchPrivacy\\.in\\.net_ozDwuJlbCG@info\\.searchprivacy\\.in\\.net)|(SearchPrivacy\\.Info_zDsfyaIpyx@info\\.searchprivacy\\.info)|(SearchPrivacy\\.link_MEIDSFxuIf@www\\.searchprivacy\\.link)|(SearchPrivacy\\.me_SHWxbNO@info\\.searchprivacy\\.me)|(SearchPrivacy\\.one_JRipaiPyZz@www\\.searchprivacy\\.one)|(SearchPrivacy\\.space_AJTsgDwydV@info\\.searchprivacy\\.space)|(SearchPrivacy\\.store_NqrJweiCoL@www\\.searchprivacy\\.store)|(SearchPrivacy\\.website_EYeCzNtENe@www\\.searchprivacy\\.website)|(searchprivacy@searchprivacy\\.co)|(searchprivacyco@info\\.search-privacy\\.co)|(searchprivacydemo@searchprivacy\\.co)|(SearchSafe_cmZnrUyIXL@www\\.searchsafe\\.site)|(SearchSafe\\.online_bTqWKNIadp@www\\.searchsafe\\.online)|(SearchSafe\\.website_NFNXBQxRPA@www\\.searchsafe\\.website)|(searchsafee_VRAYffGolz@staging\\.findmaps\\.co)|(SearchTab\\.co_bEVNcWEgQN@www\\.searchtab\\.co)|(SearchTab\\.online_gZFeDEkoQl@www\\.searchtab\\.online)|(Severe_Weather_Check_51c60f3de3995f14f71373a70593618c@www\\.severeweathercheck\\.com)|(Severe_Weather_Check_bcb8c4ad7063c40e0e5b2d0f64ba11a5@www\\.severeweathercheck\\.com)|(Severe_Weather_Check_clone_joZdMWExSs@www\\.severeweathercheck\\.com)|(Severe_Weather_Check_clone_VlvXXiHGHD@www\\.severeweathercheck\\.com)|(Severe_Weather_Check_clone_xBNchBtncq@www\\.severeweathercheck\\.com)|(Severe_Weather_Check_muRAWnsLSV@www\\.severeweathercheck\\.com)|(Single_Login_pHoBMgvLtK@single\\.login\\.com)|(Smart_-_Search_cQYvhxQOuG@www\\.EasyOnlineRecipe\\.net)|(SMART_-_SEARCH_hYubbcgBlB@www\\.netspeedcalculator\\.net)|(Smart_File_Converter_clone_BVgFKwVsaS@www\\.smartfileconverter\\.com)|(Smart_File_Converter_IasnkilxwD@www\\.smartfileconverter\\.com)|(Smart_Package_Tracker_06309abe3838d3a65fcbc43e2958851e@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_1e9ff4a4ee10df2fb289180d5bb635bb@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_705864a844f3d8d84a5aecba1f675809@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_9ef0b4fcd24ee214476e73afb6adbb2e@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_bfc6ce01ede9446711a752c034df59c4@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_clone_KVsdeebRuB@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_clone_OvvQZsPtpR@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_clone_SRguaqnzXn@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_clone_XRHzsWuwOn@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_clone_yPTsguhReR@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_clone_ZxioLKlnOD@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_db3d13974ca0682b514ad40e0f1fc682@www\\.smartpackagetracker\\.com)|(Smart_Package_Tracker_jqLcWhCacs@www\\.smartpackagetracker\\.com)|(Smart_Search_05fc1caf5755ccc74a092042913cbbda@www\\.finddirections\\.co)|(Smart_Search_10751eb8462a19515e0f9f1525333d1d@www\\.game-quest\\.co)|(Smart_Search_118237f18afcc2ce76539a9d1e015bc2@www\\.dailygame\\.online)|(Smart_Search_13d2b5842a0358c117dd87411a84db0d@www\\.convertfilesonline\\.co)|(Smart_Search_2160f8e9f48a63f3d9d7a019b9a5c53d@www\\.finddirections\\.co)|(Smart_Search_2834376aee173a89462511cf3ae21633@www\\.dailyrecipesearch\\.net)|(Smart_Search_287a4b04b4dca219a02375ca2f6a4985@www\\.finddailycoupons\\.com)|(Smart_Search_3f36608d872e4e46edd8b4e57902e6bd@www\\.dailyrecipesearch\\.net)|(Smart_Search_5cad449f0283b0b10b9cd78472de5c54@www\\.finddirections\\.co)|(Smart_Search_69eb8884d7d2e0aabdad154644e38401@www\\.convertmyfile\\.co)|(Smart_Search_6a0b26a582308b0dd9ae82634ccfe6f7@www\\.localweathertoday\\.net)|(Smart_Search_77710b6943d36049c1b72d9035a63379@www\\.localweathertoday\\.net)|(Smart_Search_7f90cdefd25d69c792d02cec7763b41d@www\\.finddirections\\.co)|(Smart_Search_814229d05c1e81d4ce1e73a3ca7c3eaa@www\\.smartsearchnow\\.co)|(Smart_Search_94fb06cfccd5d4112f555fb00a1b38ec@www\\.finddirections\\.co)|(Smart_Search_a1e27539ac89c70a170688ee375c5426@www\\.finddailycoupons\\.com)|(Smart_Search_a98fb26ee015d18ad91a68c7124e1f30@www\\.get-news\\.co)|(Smart_Search_abeJBrRXlb@www\\.smart-search\\.one))$/", + "prefs": [], + "schema": 1576758534450, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "24adcec0-6f62-41bd-9e13-97895838bf64", + "last_modified": 1576771657055 + }, + { + "guid": "/^((Smart_Search_aKLddQIYMY@www\\.gamesdaily\\.online)|(Smart_Search_aQBxkbfpqq@www\\.finddirections\\.co)|(Smart_Search_aStviFIoor@www\\.finddirections\\.co)|(Smart_Search_aTGazruEUE@www\\.search-smart\\.co)|(Smart_Search_ayroufrtQb@www\\.finddirections\\.co)|(Smart_Search_b08c081d9f06123fb6172679fdf4fcb6@www\\.finddirections\\.co)|(Smart_Search_b859bcf9fb977dc2076199076b2f9081@www\\.convertmyfile\\.co)|(Smart_Search_bbgycNKlXh@www\\.dailyjobssearch\\.net)|(Smart_Search_bdDgpqbSTU@www\\.smartsearchnow\\.co)|(Smart_Search_bnnlTJEfCZ@www\\.search-smart\\.live)|(Smart_Search_BWMiEUiCaH-my@www\\.findmaps\\.co)|(Smart_Search_BWMiEUiCaH@www\\.findmaps\\.co)|(Smart_Search_c14750a49aa2937ede499b641a6c4390@www\\.finddirections\\.co)|(Smart_Search_cb20a26ab1dc76cc9f793fb975be4b02@www\\.smartsearchnow\\.co)|(Smart_Search_cFSWRyjwef@www\\.smart-search\\.today)|(Smart_Search_chUMEQmsSU@www\\.finddirections\\.co)|(Smart_Search_CIWmtesCzJ@www\\.mycouponstore\\.co)|(Smart_Search_cKezTfCPJB@www\\.checkmaps\\.live)|(Smart_Search_cKnSYkYtgM@www\\.check-yourmail\\.co)|(Smart_Search_clone_bZjlXmDRQB@www\\.finddirections\\.co)|(Smart_Search_clone_duXXvsUEtZ@www\\.finddirections\\.co)|(Smart_Search_clone_EQDmJygWxO@www\\.searchsmart\\.online)|(Smart_Search_clone_ETXwzVYwpI@www\\.search-smart\\.work)|(Smart_Search_clone_HsJXCDiNNC@www\\.smartsearch\\.link)|(Smart_Search_clone_JxJqcLVLOv@www\\.finddirections\\.co)|(Smart_Search_clone_SoSaSQDKGZ@www\\.dailyrecipesearch\\.net)|(Smart_Search_CQbLIZDIOl@www\\.smartsearch\\.link)|(Smart_Search_CxIpAXIPVs@www\\.smart-search\\.co)|(Smart_Search_d0f38b4ff2f0e788c442249a857e4dde@www\\.finddirections\\.co)|(Smart_Search_d16ea913837e848252ad5d856d9c8b02@www\\.dailyjobssearch\\.net)|(Smart_Search_d8982f2dae25e7422cc1392ccf974d04@www\\.dailyrecipesearch\\.net)|(Smart_Search_da3cc67017bdcd720cbd7fc26f24d743@www\\.check-yourmail\\.co)|(Smart_Search_dcf5af05538a3c68edd4625c7f0d2dc4@www\\.finddailycoupons\\.com)|(Smart_Search_DLsAIStnQX@www\\.smartsearch\\.link)|(Smart_Search_dTBjWAGKlN@www\\.testnetspeed\\.co)|(Smart_Search_e34c59b523a7a1585da696e4576b101c@www\\.gamesdaily\\.online)|(Smart_Search_e6d57c56077122ca0bcef12a39e3f6a3@www\\.localweathertoday\\.net)|(Smart_Search_ea5e7bb722049dd11af40116816edee3@disable\\.convertmyfile\\.co)|(Smart_Search_EkAoOFUJwV@www\\.games-daily\\.co)|(Smart_Search_eMDQwPVVyj@www\\.search-smart\\.co)|(Smart_Search_f377b2bb34f42cbb572c36d9bfb3d9c3@www\\.finddirections\\.co)|(Smart_Search_fbd05acb7ab6331297e367f816ff0a20@www\\.testnetspeed\\.co)|(Smart_Search_FCphmgFrTz@www\\.finddailygames\\.co)|(Smart_Search_FMxybFLCfE@www\\.smart-search\\.today)|(Smart_Search_gGkCXJUDEH@www\\.game-quest\\.co)|(Smart_Search_HnEqPdzcek@www\\.search-smart\\.live)|(Smart_Search_IbBWPmuFLU@www\\.convertfilesonline\\.co)|(Smart_Search_JcpHMeulsq@www\\.dailyrecipesearch\\.net)|(Smart_Search_JJPKAMJWLv@www\\.finddailycoupons\\.com)|(Smart_Search_JTruqraRrz@www\\.localweathertoday\\.net)|(Smart_Search_JViAyYSxof@www\\.search-smart\\.today)|(Smart_Search_KjzSKpViVv@www\\.getmapfinder\\.com)|(Smart_Search_LBQKLuCjDu@www\\.search-smart\\.work)|(Smart_Search_LBUbgBMxrr@www\\.convertmyfile\\.co)|(Smart_Search_lKbvfFvWdq@www\\.finddirections\\.co)|(Smart_Search_LPxWJiUiWP@dailyrecipesearch\\.net)|(Smart_Search_lVYnIBRyYO@www\\.search-smart\\.today)|(Smart_Search_lzlfscclxO@www\\.smartsearch\\.news)|(Smart_Search_mmUSVHTgZN@www\\.searchsmart\\.website)|(Smart_Search_MYlsYAHChr@www\\.search-smart\\.one)|(Smart_Search_OYMxotguWV@www\\.thecoupon-store\\.co)|(Smart_Search_PncWXXrZyE@www\\.get-news\\.co)|(Smart_Search_PxUetfSxfK@www\\.finddirections\\.co)|(Smart_Search_qbdedBcsMV@www\\.mysmartsearch\\.online)|(Smart_Search_QFEtHYupEv@www\\.localweathertoday\\.net)|(Smart_Search_rmcVsyMvUV@www\\.finddailycoupons\\.com)|(Smart_Search_sfQjeIHqAg@www\\.onlinecouponfinder\\.net)|(Smart_Search_SLKIYTovfO@www\\.dailygame\\.online)|(Smart_Search_snYvsooWFZ@www\\.dailycoupons\\.store)|(Smart_Search_SRonsLzSpz@www\\.search-smart\\.link)|(Smart_Search_SUWIUWczzW@www\\.search-smart\\.club)|(Smart_Search_trFxvKvCFp@www\\.dailyrecipesearch\\.net)|(Smart_Search_tTrjAmWOoR@www\\.convertmyfile\\.co)|(Smart_Search_txfWPOCTvy@www\\.finddailycoupons\\.com)|(Smart_Search_UgGHEdLtFf@www\\.search-smart\\.one)|(Smart_Search_UXuBBPZiCD@www\\.search-smart\\.website)|(Smart_Search_VFsgkOxdvk@www\\.convertmyfile\\.co))$/", + "prefs": [], + "schema": 1576758569109, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "65197a67-44c8-440d-b22c-26d84b2d763e", + "last_modified": 1576771657051 + }, + { + "guid": "/^((Templates_Online_clone_UmejRGyzBe@www\\.gettemplatesonline\\.com)|(Templates_Online_clone_WiWqHXsGkm@www\\.gettemplatesonline\\.com)|(Templates_Online_xUPYerdIfL@www\\.gettemplatesonline\\.com)|(test_fhwkrCFoHO@test)|(test_searchprivacy@info\\.searchprivacy\\.info)|(TestFirefoxAddon_ahdBLDcQql@www\\.testfirefoxaddon\\.com)|(testsearchprivacy@info\\.searchprivacy\\.co)|(Text_From_Your_PC_9e43d427bd5cde52e50706cf2ce04da8@www\\.textfromyourpc\\.com)|(Text_From_Your_PC_cBapqdxFhR@www\\.textfromyourpc\\.com)|(The_Coupon_-_Store_ERhPcJTQll@thecoupon-store\\.co)|(The_Coupon_King_1209d556e43b7c529e282d8b8842da76@www\\.thecouponking\\.co)|(The_Coupon_King_lgTjuAddLR@www\\.thecouponking\\.co)|(The_Coupon_King_UwqzosFLgL@www\\.thecouponking\\.com)|(The_Coupon_Store_FuajgfDoOD@www\\.thecouponstore\\.co)|(The_Coupon_Store_lPVGxmIRUf@www\\.thecouponstore\\.co)|(The_Coupon_Store_Test_jkAVapBJTo@www\\.thecouponstore\\.co\\.test)|(The_Coupon_Trail_bdc89c2aeb9eb486945d9f9d80bdbb02@www\\.thecoupontrail\\.com)|(The_Coupon_Trail_MMnMCLSPWo@www\\.thecoupontrail\\.com)|(The_Coupon_Trial_IteRooFtcL@www\\.thecoupontrial\\.com)|(The_Coupons_Daily_efc9df81f19dc213cb2a20067afa4ccb@www\\.thecouponsdaily\\.com)|(The_Coupons_Daily_NAEBGGQbOZ@www\\.thecouponsdaily\\.com)|(The_Coupons_King_cb3ed44aa8013a310be07e3d3fcc4dc2@www\\.thecouponsking\\.co)|(The_Currency_Switch_987755b6cd2466fcb08f71503c38cbfa@www\\.thecurrencyswitch\\.co)|(The_Currency_Switch_c80da7ec7cd15c168fc419b3d69aa602@www\\.thecurrencyswitch\\.co)|(The_Currency_Switch_clone_e0f313620f09d4dfc50e3a362bac8a9a@www\\.thecurrencyswitch\\.co)|(The_Currency_Switch_clone_geyyIAUtgm@www\\.thecurrencyswitch\\.co)|(The_Currency_Switch_clone_JZMSasbVPc@www\\.thecurrencyswitch\\.co)|(The_Currency_Switch_QlvIbtxDLF@www\\.thecurrencyswitch\\.co)|(The_Movie_Quest_BPyzyFXOpw@www\\.themoviequest\\.today)|(The_Movie_Search_IgbQnCESAN@www\\.themoviesearch\\.co)|(The_Movie_Search_zFBfJgLeGc@www\\.themoviesearch\\.co)|(The_News_Prompter_e90ebcdb43eb03954526e4576d6c6b71@www\\.thenewsprompter\\.com)|(The_News_Prompter_mbjJOGhbsK@www\\.thenewsprompter\\.com)|(The_Package_Track_9e0db37d4fc02628a9996d9be9ee7ad6@www\\.thepackagetrack\\.com)|(The_Quiz_Tab_xWbNAmFREz@www\\.thequiztab\\.com)|(The_Search_Easy_44e7c2c276e66b66d8bfb874777d4a2e@www\\.thesearcheasy\\.com)|(The_Search_Easy_XObNZOoESy@www\\.thesearcheasy\\.com)|(The_Search_Plus_37976642e8a885a46631002f0051ff23@www\\.thesearch-plus\\.co)|(The_Search_Plus_UawsDpTchY@www\\.thesearch-plus\\.co)|(thecouponstore@www\\.thecoupon-store\\.co)|(TheMovie-Hub\\.net_YezkChdTjo@www\\.themovie-hub\\.net)|(TheMovie-Portal_tPUcBCgXvk_1@www\\.themovie-portal\\.com)|(TheMovie-Portal_tPUcBCgXvk@www\\.themovie-portal\\.com)|(TheMovie-Quest\\.com_wlPrmnAQDs@www\\.themovie-quest\\.com)|(TheMovieQuest\\.co_cORgvNNjpL@www\\.themoviequest\\.co)|(TheMovieQuest\\.co_fQQHMPPCrc@www\\.themoviequest\\.co)|(themoviesearch\\.today_NaQhVgVlwL@www\\.themoviesearch\\.today)|(This_is_my_extension_qZkKGOuZcR@www\\.exampleAMO\\.com)|(This_is_New_AMO_dyIGZrXnWN@www\\.kite\\.com)|(This_is_New_AMO_QVeIjjGbol@www\\.kite\\.com)|(Trace_Packages_f406b9f80935c80df1166017cdb4d6f1@www\\.tracepackages\\.com)|(Track_Daily_News_1eaf6dc15b67e558badb4d0cfe763913@www\\.trackdailynews\\.com)|(Track_Daily_News_6e84f45f079de0ffcc94b2d968db9bee@www\\.trackdailynews\\.com)|(Track_Daily_News_a339167d304bfa5f599a249dda7ce993@www\\.trackdailynews\\.com)|(Track_Daily_News_EIMlViVmYz@www\\.trackdailynews\\.com)|(Track_Daily_News_sCvLoGjBau@www\\.trackdailynews\\.com)|(Track_Flight_Pro_7b52baadaefaac0bc71eaa5d3ffdd62c@www\\.trackflightpro\\.com)|(Track_Flight_Pro_c7f7b664812211123e6c54706eb66c3e@www\\.trackflightpro\\.com)|(Track_Flight_Pro_clone_hNojYqaWwF@www\\.trackflightpro\\.com)|(Track_Flight_Pro_eZAWVNMAAT@www\\.trackflightpro\\.com)|(Track_Package_Pro_clone_xayswwXtnt@www\\.trackpackagepro\\.com)|(Track_Package_Pro_clone_xBtZVauyZo@www\\.trackpackagepro\\.com)|(Track_Package_Pro_KvcoWaXXaa@www\\.trackpackagepro\\.com)|(Track_Package_Quick_525020a307e84998fc7ad8a634296e1d@www\\.trackpackagequick\\.com)|(Track_Package_Quick_clone_SjXacYcSKg@www\\.trackpackagequick\\.com)|(Track_Package_Quick_clone_zTowhYQtBK@www\\.trackpackagequick\\.com)|(Track_Package_Quick_lFiXdhlnGK@www\\.trackpackagequick\\.com)|(trackthatpackagecom@www\\.trackthatpackage\\.com)|(trailertab@www\\.trailertab\\.co))$/", + "prefs": [], + "schema": 1576758629163, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5cd48455-378c-4e67-a37d-8c39d497b4af", + "last_modified": 1576771657042 + }, + { + "guid": "/^((Translate_cgjqtumrjO@app\\.translate\\.com)|(Travel_Deals_Center_clone_GJNQLATOqW@www\\.traveldealscenter\\.co)|(Travel_Deals_Center_rqctZxkUaz@www\\.traveldealscenter\\.co)|(Travel_Directions_clone_FfMthspRaM@www\\.traveldirections\\.co)|(Travel_Directions_clone_ourIqJKEoa@www\\.traveldirections\\.co)|(Travel_Directions_mvJHkLHMMo@www\\.traveldirections\\.co)|(Tv_Shows_Online_4331a381f3f1b8d9f9f0b56793be73be@www\\.tvshowsonline\\.co)|(Tv_Shows_Online_MpTzgCmWDM@www\\.tvshowsonline\\.co)|(TV_Streaming_Plus_ARhtOdhYyz@www\\.tvstreamingplus\\.co)|(tyghn_EbxtKCSoQf@ghn)|(Universal_Package_Tracker_vEmOcVlKPs@www\\.universalpackagetrackern\\.online)|(Weather_Alert_Pro_BodPvrEWvi@www\\.weatheralertpro\\.co)|(Weather_Alert_Pro_clone_sTGOfwOqRV@www\\.weatheralertpro\\.co)|(Weather_Alert_Pro_eAvhNglZrt@www\\.weatheralertpro\\.com)|(Weather_Center_App_06cc2a6dd45345b134670c50b38c8692@www\\.weathercenterapp\\.com)|(Weather_Center_App_edIZBMRKqw@www\\.weathercenterapp\\.com)|(Weather_Center_App_ef684143ed5bb6671446fa504a5feec8@www\\.weathercenterapp\\.com)|(Weather_Center_App_f860fbbefafc691d55516dbf9ff9de42@www\\.weathercenterapp\\.com)|(Weather_Center_App_FCsxbRlcmB@www\\.weathercenterapp\\.com)|(Weather_Center_App_rEbQRyobNS@www\\.weathercenterapp\\.com)|(Weather_Coach_aRmHcdLWxv@www\\.weather-coach\\.com)|(Weather_Coach_cnhVSyfNQb@www\\.weather-coach\\.com)|(Weather_Coach_SdaRyeWHza@www\\.weather-coach\\.com)|(Weather_Details_a74cceec1f4c6974ce48a55da22c4fc8@www\\.weather-details\\.co)|(Weather_Details_clone_WmbTEchnZv@www\\.weather-details\\.today)|(Weather_Details_DBDwBdsiUV@www\\.weather-details\\.online)|(Weather_Details_LZKpPYrFpw@www\\.weather-details\\.today)|(Weather_Details_UMhKKuOmLm@www\\.weather-details)|(Weather_Details_xrNgYMjMud@www\\.weather-details\\.today)|(Weather_Forecast_0c98741392250106567fdcdf92e662ed@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_22873441e62b17d83e57bee8dae28096@disable\\.checkweathernow\\.co)|(Weather_Forecast_26bbf7afaa8ca321d3eb1793eab261eb@www\\.checkweathernow\\.co)|(Weather_Forecast_34a39ecaa08283313fa326a45884233b@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_5432f72204437596f302a717de4410ab@www\\.localweathertoday\\.net)|(Weather_Forecast_5c6a315e6da8023a47a4e247bcd60544@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_5d13ac5f68d80a584ad913b02540fae5@www\\.checkweather\\.today)|(Weather_Forecast_647cf952e5329d490351c5b250e81e54@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_6f6484fab931bf3f3fcf75dfa65adc37@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_7462a90b29dc5f13bb4628cce7d2c6dc@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_a4fe2e9e47d66eebd6220e13d10434ef@www\\.checkweathernow\\.co)|(Weather_Forecast_BjIrtPRHri@www\\.weatherforecasttracker\\.com)|(Weather_Forecast_Buddy_hqZFZdAdSe@www\\.weatherforecastbuddy\\.com)|(Weather_Forecast_c27096e498d3541e341f2597fa7124b0@www\\.weatherforecasttracker\\.com)|(Weather_Forecast_c6509e49a9a27bdab9facfb442488a79@www\\.localweathertoday\\.net)|(Weather_Forecast_ca28589b2ca67136318e4bfdf954002d@www\\.checkweathertoday\\.net)|(Weather_Forecast_cbf70b1126d7690bf9180b2e6c43e8f6@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_ChxOzHZcLa@www\\.localweathertoday\\.net)|(Weather_Forecast_clone_60d8346940b46a116579ec1ea3906d5d@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_chFFSZCgvz@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_clone_94a823ffc8bedf3bd1e6ab4465826581@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_clone_clone_apzzlMbkyB@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_clone_clone_f2e4cad9766f8cde8ffbdd484efbdd2b@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_clone_yQjWeYXXXI@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_HErKKKDyFS@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_HOrAZgjBCz@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_clone_jMGHMuyVVi@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_KYlMwumWQY@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_kyUmeEeefY@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_clone_pDcEnQJDXJ@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_piPIkUDQPc@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_QoJcNyLaQt@www\\.weatherforecasttracker\\.com))$/", + "prefs": [], + "schema": 1576758680309, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a083664b-0301-45cd-a2ee-0c690be250b6", + "last_modified": 1576771657038 + }, + { + "guid": "/^((Weather_Forecast_clone_TSGzGLieya@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_clone_UBimgpjhsm@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_clone_XcnVeRvMca@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_clone_YQnByPToHy@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_clone_zkFObDWDhS@www\\.weatherforecasttracker1\\.com)|(Weather_Forecast_Daily_CTzpOZENfa@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_e3838c6031faf63c6209533e71b456c0@www\\.weatherforecastdaily\\.disabled\\.com)|(Weather_Forecast_EnkDNfGxAS@www\\.checkweathernow\\.co)|(Weather_Forecast_epwQJrNJwH@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_eWEHJWtQzM@www\\.checkweathertoday\\.net)|(Weather_Forecast_eWFHhLxmdi@www\\.checkweathernow\\.co)|(Weather_Forecast_f5d79bda8e7d666123f8cff0f49720ab@www\\.localweathertoday\\.net)|(Weather_Forecast_FbozBLKAnT@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_fc107df349d96479678eb88a9d2f58b3@www\\.weatherforecasttracker\\.com)|(Weather_Forecast_KlMFWMEnxo@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_KNdWdwrVBg@www\\.weather-forecast\\.link)|(Weather_Forecast_lESFprnxRC@www\\.checkweathernow\\.co)|(Weather_Forecast_nsdhLSCwEt@www\\.weather-forecast\\.news)|(Weather_Forecast_OMezCgBGlR@www\\.weather-forecast\\.link)|(Weather_Forecast_Plus_81c2ce246488019a54fed798354fbb85@www\\.weatherforecastplus\\.com)|(Weather_Forecast_Plus_WVxlZhsVCn@www\\.weatherforecastplus\\.com)|(Weather_Forecast_QqFyFxILHq@www\\.checkweather\\.today)|(Weather_Forecast_rUPfzmWQNa@www\\.localweathertoday\\.net)|(Weather_Forecast_Search_4f3974a0460bd8947f25b61879196ae6@www\\.weatherforecastsearch\\.com)|(Weather_Forecast_Search_SPAKKzWPua@www\\.weatherforecastsearch\\.com)|(Weather_Forecast_SjMIOdqezy@www\\.weatherforecastdaily\\.com)|(Weather_Forecast_Tab_d586ce62eebca7b5ccd054f645ab3278@www\\.weatherforecasttab\\.com)|(Weather_Forecast_Tab_LHutnjYcaD@www\\.weatherforecasttab\\.com)|(Weather_Forecast_UZnBodXSQY@www\\.localweathertoday\\.net)|(Weather_Forecast_V2_c8e3c1f489201dcee4f10ed13b88423b@www\\.weatherforecasttracker1\\.com)|(Weather_Info_AdPTHnolyR@www\\.weatherinfo\\.live)|(Weather_Info_clone_IylhdngkdZ@www\\.weatherinfo\\.live)|(Weather_Info_clone_LAyJxkrWYr@www\\.weatherinfo\\.live)|(Weather_Info_mYTEbKRsJk@www\\.weatherinfo\\.live)|(Weather_Info_rBUleVUgYg@www\\.weatherinfo\\.today)|(Weather_Info_ZurIhFqkfO@www\\.weatherinfo\\.live)|(Weather_Online_Now_clone_gXBxUraWkz@www\\.weatheronlinenow\\.com)|(Weather_Online_Now_clone_VEajkSqfLm@www\\.weatheronlinenow\\.com)|(Weather_Online_Now_LxMeLWImMw@www\\.weatheronlinenow\\.com)|(Weather_Report_69d519795ecb2edb106cf07b8e5fcbae@www\\.weatherreportlive\\.co)|(Weather_Report_JHDIwuETLZ@www\\.weather-report\\.live)|(Weather_Report_PdxgfapiBL@www\\.weatherreportlive\\.co)|(Weather_Report_WKPqFTsXUk@www\\.weather-report\\.live)|(Weather_Reporter_98959e99f2103eef2d02f3d351ba89b9@www\\.weatherreporter\\.co)|(Weather_Reports_BjGuRgCpLB@www\\.weather-reportsnow\\.com)|(Weather_Reports_c982e1b51cd8bc347ab328410329fbb6@www\\.weatherreports\\.live)|(Weather_Reports_clone_fRjlGYJSnk@www\\.weatherreports\\.live)|(Weather_Reports_clone_NMTTAUMqtY@www\\.dailyweatherreports\\.co)|(Weather_Reports_EQVqqGZdlM@www\\.weather-report\\.link)|(Weather_Reports_HlXmMSuFKX@www\\.weatherreports\\.live)|(Weather_Reports_KDxyqUBBwO@www\\.dailyweatherreports\\.co)|(Weather_Reports_UyzmbxlFVx@www\\.weather-reports\\.today)|(Weather_Reports_xaeygHwNhf@www\\.weatherreports\\.live)|(Weather_Reports_zDpccdTchc@www\\.weatherreports\\.today)|(Weather_Reports_zKYmoRPwNT@www\\.weatherreports\\.live)|(Weather_Tab_Pro_clone_aTBTPSAEUX@www\\.weathertabpro\\.com)|(Weather_Updates_TGBOowTNwY@www\\.weather-updates\\.co)|(Weather-Bee_1b435f4656c8c4f6df6b455cc302cf9b@www\\.weather-bee\\.co)|(Weather-Bee_cbff90268cbc8e1fd65336342cfcf056@www\\.weather-bee\\.co)|(Weather-Bee_deb8c4f31d28d8ef924d8604c528f43b@www\\.weather-bee\\.com)|(Weather-Bee_gFubKNACfC@www\\.weather-bee\\.co)|(Weather-Bee_HyIkcLzmEL@www\\.weather-bee\\.co)|(Weather-Bee_oUeUqEGckY@www\\.weather-bee\\.com)|(WeatherDetails_qGhRPwNzlD@www\\.weatherdetails\\.net)|(Web_Gamer_World_clone_bBqNIFWIiJ@www\\.webgamerworld\\.com)|(Web_Gamer_World_clone_vSvnetvjxh@www\\.webgamerworld\\.com)|(Web_Gamer_World_EnPQSoVMPy@www\\.webgamerworld\\.com))$/", + "prefs": [], + "schema": 1576758713481, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "86bffc98-7002-4c94-84b1-2036f19f1345", + "last_modified": 1576771657033 + }, + { + "guid": "/^((WheresMyPackage\\.co_04c5041d404a424ae04b3bdae470bf36@www\\.wheresmypackage\\.co)|(WheresMyPackage\\.co_nkiJdrfSTz@www\\.wheresmypackage\\.co)|(wheresmypackageco@www\\.wheresmypackage\\.co)|(Wonder_Wallpaper_7445d465b196ea5672cf5ddad5121e20@www\\.wonderwallpaper\\.co)|(Wonder_Wallpaper_86c968090b99d54af67a65a1c6010dc8@www\\.wonderwallpaper\\.co)|(Wonder_Wallpaper_clone_b3a8885c9af58cc53b837522fbeab5fa@www\\.wonderwallpaper\\.co)|(Wonder_Wallpaper_clone_BdEgqQaNqI@www\\.wonderwallpaper\\.co)|(Wonder_Wallpaper_clone_rujWfVzPPf@www\\.wonderwallpaper\\.co)|(Wonder_Wallpaper_XONDNNciYs@www\\.wonderwallpaper\\.co)|(www\\.search-privacy\\.in\\.net_frtQaDOkLW@www\\.search-privacy\\.in\\.net)|(www\\.search-privacy\\.info_hyVgVlpUDh@www\\.search-privacy\\.info)|(Your_Mail_Tab_06f9e0cacb38332e7d67d6f6eb36fbf4@www\\.yourmailtab\\.com)|(Your_Mail_Tab_27811aa670d957af7c67c9b40421db33@www\\.yourmailtab\\.com)|(Your_Mail_Tab_clone_FFlaTZQUzT@www\\.yourmailtab\\.com)|(Your_Mail_Tab_clone_kckMtSXzhP@www\\.yourmailtab\\.com)|(Your_Mail_Tab_clone_rZgtWWxFTE@www\\.yourmailtab\\.com)|(Your_Mail_Tab_clone_tplYnwtbOB@www\\.yourmailtab\\.com)|(Your_Mail_Tab_clone_UStpWwUYys@www\\.yourmailtab\\.com)|(Your_Mail_Tab_clone_wQEiyJhcTK@www\\.yourmailtab\\.com)|(Your_Mail_Tab_sHxtuglHxJ@www\\.yourmailtab\\.com)|(Your_Map_Tab_caf5c90424b052d0a732d773f984284d@www\\.yourmaptab\\.com)|(Your_Map_Tab_MGrwdnTuFV@www\\.yourmaptab\\.com)|(Your_Maps_Finder_1cb2c0753a9f2089208e662ff7887eff@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_283a77e3a6a0c1cb4d803b12b7b3f38d@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_299d66c546dc1bf61c2f1ac4a823d848@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_2b2f2c9fdc74da065784a5537d7cbe8a@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_493f8ad05595d053fc466c6d712a0f5b@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_5fb3ba7aabc0df0482f721f2b8226c49@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_8ec3602a25914b2b98753c78f3ff0792@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_a4e12e88158bb14fe3f3b23935d1005b@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_clone_KCngJjrrAW@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_CVslhcZliG@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_dfeYIGYOsX@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_gSkAwghnnM@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_HbjLLgCqVI@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_IAQSXqZaut@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_IjXrmViPjV@www\\.yourmapsfinder\\.com)|(Your_Maps_Finder_jfCclXiWUZ@www\\.yourmapsfinder\\.com)|(Your_Maps_Guide_gJsxGXyDUk@www\\.yourmapsguide\\.com)|(Your_Recipes_Guide_6e2b67bec168f977b275d2cb8e38cb99@www\\.yourrecipesguide\\.com)|(Your_Recipes_Guide_pjXpEcLtqt@www\\.yourrecipesguide\\.com)|(Yum_Recipe_finder_5de2cc2f278f3bfc1a3715a70c8661f0@www\\.yumrecipefinder\\.com)|(Yum_Recipe_finder_93a02159e31918ef11d89db653420167@www\\.yumrecipefinder\\.com)|(Yum_Recipe_finder_clone_AoObYTQNIW@www\\.yumrecipefinder\\.com)|(Yum_Recipe_finder_clone_c3c1e13d25490ca78859e8857319b432@www\\.yumrecipefinder\\.com)|(Yum_Recipe_finder_clone_nFKjdQhPjD@www\\.yumrecipefinder\\.com)|(Yum_Recipe_finder_clone_oIeIcINzWP@www\\.yumrecipefinder\\.com)|(Yum_Recipe_Finder_CqcblSJbqU@www\\.yumrecipefinder\\.com)|(Yum_Recipe_finder_eb8981fe181afd4e02a57fd9c4b9e8f2@www\\.yumrecipefinder\\.com)|(Yum_Recipe_finder_mgdDNujmFJ@www\\.yumrecipefinder\\.com))$/", + "prefs": [], + "schema": 1576758771928, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "bfa84d62-24de-474e-bc35-a69b1f46f331", + "last_modified": 1576771657029 + }, + { + "guid": "/^((@FirefoxUpdate)|(@googledashboard)|(@smash_mov)|(@smash_tv)|(@smashdashboard)|(@smashmovs)|(@smashtvs)|(\\{0be01832-7cce-4457-b8ad-73b743914085\\})|(\\{0e1c683e-9f34-45f1-b365-a283befb471a\\})|(\\{0c72a72d-6b2e-4a0e-8a31-16581176052d\\})|(\\{0ccfc208-8441-4c27-b1cb-799accb04908\\})|(\\{0ede8d39-26f2-49c4-8014-dfc484f54a65\\})|(\\{1fc1f8e6-3575-4a6f-a4d1-c4ca1c36bd2a\\})|(\\{3a1d6607-e6a8-4012-9506-f14cd157c171\\})|(\\{03b3ac4d-59a3-4cc6-aa4d-9b39dd8b3196\\})|(\\{3bb6e889-ac7a-46ca-8eed-45ba4fbe75b5\\})|(\\{3c841114-da8c-44ea-8303-78264edfe60b\\})|(\\{3f3bcb3e-dd73-4410-b102-60a87fcb8323\\})|(\\{3f951165-fd85-42ae-96ef-6ff589a1fe72\\})|(\\{04c86cb3-5f52-4083-9e9a-e322dd02181a\\})|(\\{4d8b44ef-9b8b-4d82-b668-a49648d2749d\\})|(\\{4d25d2b4-6ae7-4a66-abc0-c3fca4cdddf6\\})|(\\{5c9a2eca-2126-4a84-82c0-efbf3d989371\\})|(\\{6ecb9f49-90f0-43a1-8f8a-e809ea4f732b\\})|(\\{6fb8289d-c6c8-4fe5-9a92-7dc6cbf35349\\})|(\\{7fea697d-327c-4d20-80d5-813a6fb26d86\\})|(\\{08a3e913-0bbc-42ba-96d7-3fa16aceccbf\\})|(\\{8b04086b-94a5-4161-910b-59e3e31e4364\\})|(\\{08c28c16-9fb6-4b32-9868-db37c1668f94\\})|(\\{8cd69708-2f5e-4282-a94f-3feebc4bce35\\})|(\\{8dc21e24-3883-4d01-b486-ef1d1106fa3d\\})|(\\{8f8cc21a-2097-488f-a213-f5786a2ccbbf\\})|(\\{9c8b93f7-3bf8-4762-b221-40c912268f96\\})|(\\{9ce66491-ef06-4da6-b602-98c2451f6395\\})|(\\{1e1acc1c-8daa-4c2e-ad05-5ef01ae65f1e\\})|(\\{10b0f607-1efa-4762-82a0-e0d9bbae4e48\\})|(\\{24f338d7-b539-49f1-b276-c9edc367a32d\\})|(\\{40c9030f-7a2f-4a58-9d0a-edccd8063218\\})|(\\{41f97b71-c7c6-40b8-83b1-a4dbff76f73d\\})|(\\{42f3034a-0c4a-4f68-a8fd-8a2440e3f011\\})|(\\{52d456e5-245a-4319-b8d2-c14fbc9755f0\\})|(\\{57ea692b-f9fe-42df-bf5e-af6953fba05a\\})|(\\{060c61d8-b48f-465d-aa4b-23325ea757c3\\})|(\\{65c1967c-6a5c-44dd-9637-0d4d8b4c339b\\})|(\\{65d40b64-b52a-46d8-b146-580ff91889cb\\})|(\\{75b7af0d-b4ed-4320-95c8-7ffd8dd2cb7c\\})|(\\{77fe9731-b683-4599-9b06-a5dcea63d432\\})|(\\{84b20d0c-9c87-4340-b4f8-1912df2ae70d\\})|(\\{92b9e511-ac81-4d47-9b8f-f92dc872447e\\})|(\\{95afafef-b580-4f66-a0fe-7f3e74be7507\\})|(\\{116a0754-20eb-4fe5-bd35-575867a0b89e\\})|(\\{118bf5f6-98b1-4543-b133-42fdaf3cbade\\})|(\\{248eacc4-195f-43b2-956c-b9ad1ae67529\\})|(\\{328f931d-83c1-4876-953c-ddc9f63fe3b4\\})|(\\{447fa5d3-1c27-4502-9e13-84452d833b89\\})|(\\{476a1fa9-bce8-4cb4-beff-cb31980cc521\\})|(\\{507a5b13-a8a3-4653-a4a7-9a03099acf48\\})|(\\{531bf931-a8c6-407b-a48f-8a53f43cd461\\})|(\\{544c7f83-ef54-4d17-aa91-274fa27514ef\\})|(\\{546ea388-2839-4215-af49-d7289514a7b1\\})|(\\{635cb424-0cd5-4446-afaf-6265c4b711b5\\})|(\\{654b21c7-6a70-446c-b9ac-8cac9592f4a9\\})|(\\{0668b0a7-7578-4fb3-a4bd-39344222daa3\\})|(\\{944ed336-d750-48f1-b0b5-3c516bfb551c\\})|(\\{1882a9ce-c0e3-4476-8185-f387fe269852\\})|(\\{5571a054-225d-4b65-97f7-3511936b3429\\})|(\\{5921be85-cddd-4aff-9b83-0b317db03fa3\\})|(\\{7082ba5c-f55e-4cd8-88d6-8bc479d3749e\\})|(\\{7322a4cb-641c-4ca2-9d83-8701a639e17a\\})|(\\{90741f13-ab72-443f-a558-167721f64883\\})|(\\{198627a5-4a7b-4857-b074-3040bc8effb8\\})|(\\{5e5b9f44-2416-4669-8362-42a0b3f97868\\})|(\\{824985b9-df2a-401c-9168-749960596007\\})|(\\{4853541f-c9d7-42c5-880f-fd460dbb5d5f\\})|(\\{6e6ff0fd-4ae4-49ae-ac0c-e2527e12359b\\})|(\\{90e8aa72-a7eb-4337-81d4-538b0b09c653\\})|(\\{02e3137a-96a4-433d-bfb2-0aa1cd4aed08\\})|(\\{9e734c09-fcb1-4e3f-acab-04d03625301c\\})|(\\{a6ad792c-69a8-4608-90f0-ff7c958ce508\\})|(\\{a512297e-4d3a-468c-bd1a-f77bd093f925\\})|(\\{a71b10ae-b044-4bf0-877e-c8aa9ad47b42\\})|(\\{a33358ad-a3fa-4ca1-9a49-612d99539263\\})|(\\{a7775382-4399-49bf-9287-11dbdff8f85f\\})|(\\{afa64d19-ddba-4bd5-9d2a-c0ba4b912173\\})|(\\{b4ab1a1d-e137-4c59-94d5-4f509358a81d\\})|(\\{b4ec2f8e-57fd-4607-bf4f-bc159ca87b26\\})|(\\{b06bfc96-c042-4b34-944c-8eb67f35630a\\})|(\\{b9dcdfb0-3420-4616-a4cb-d41b5192ba0c\\})|(\\{b8467ec4-ff65-45f4-b7c5-f58763bf9c94\\})|(\\{b48e4a17-0655-4e8e-a5e2-3040a3d87e55\\})|(\\{b6166509-5fe0-4efd-906e-1e412ff07a04\\})|(\\{bd1f666e-d473-4d13-bc4d-10dde895717e\\})|(\\{be572ad4-5dd7-4b6b-8204-5d655efaf3b3\\})|(\\{bf2a3e58-2536-44d4-b87f-62633256cf65\\})|(\\{bfc5ac5f-80bd-43e5-9acb-f6d447e0d2ce\\})|(\\{bfe3f6c1-c5fe-44af-93b3-576812cb6f1b\\})|(\\{c0b8009b-57dc-45bc-9239-74721640881d\\})|(\\{c1cf1f13-b257-4271-b922-4c57c6b6e047\\})|(\\{c3d61029-c52f-45df-8ec5-a654b228cd48\\})|(\\{c39e7c0b-79d5-4137-bef0-57cdf85c920f\\})|(\\{ce043eac-df8a-48d0-a739-ef7ed9bdf2b5\\})|(\\{cf62e95a-8ded-4c74-b3ac-f5c037880027\\})|(\\{cff02c70-7f07-4592-986f-7748a2abd9e1\\}))$/", + "prefs": [], + "schema": 1576756770976, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1476369", + "why": "These add-ons contain unwanted features and try to prevent the user from uninstalling themselves.", + "name": "Smash/Upater (malware) and similar" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c7d7515d-563f-459f-821c-27d4cf825dbf", + "last_modified": 1576771657025 + }, + { + "guid": "/^((search-unlisted2@mozilla\\.com)|(search-unlisted3@mozilla\\.com)|(search-unlisted4@mozilla\\.com)|(search-unlisted5@mozilla\\.com)|(search-unlisted11@mozilla\\.com)|(search-unlisted12@mozilla\\.com)|(search-unlisted55@mozilla\\.com)|(search-unlisted111@mozilla\\.com)|(search-unlisted400@mozilla\\.com)|(search-unlisted40110@mozilla\\.com)|(search-unlisted17441000051@mozilla\\.com)|(search-unlisted174410000522777441@mozilla\\.com)|(search-unlisted@mozilla\\.com)|({0a054930-63d7-46f4-937a-de80eab21da4})|({0b24cf69-02b8-407d-83db-e7af04fc1f3e})|({0c4df994-4f4a-4646-ae5d-8936be8a4188})|({0d50d8aa-d1ed-4930-b0a0-f3340d2f510e})|({0eb4672d-58a6-4230-b74c-50ca3716c4b0})|({0f9e469e-4245-43f8-a7a8-7e730f80d284})|({0fc9fcc7-2f47-4fd1-a811-6bd4d611294b})|({4479446e-40f3-48af-ab85-7e3bb4468227})|({1a927d5b-42e7-4407-828a-fdc441d0daae})|({1a760841-50c3-4143-9f7e-3c8f04e8f9d1})|({1bd8ba17-b3ed-412e-88db-35bc4d8771d7})|({1c7d6d9e-325a-4260-8213-82d51277fc31})|({01c9a4a4-06dd-426b-9500-2ea6fe841b88})|({1cab8ccf-deff-4743-925d-a47cbd0a6b56})|({1cb0652a-4645-412d-b7e8-0b9e9a83242f})|({1d6634ca-dd37-4a31-aad1-321f05aa2bb3})|({1d9997b2-f61e-429a-8591-999a6d62becc})|({1ed2af70-9e89-42db-a9e8-17ae594003ac})|({01f409a5-d617-47be-a574-d54325fe05d1})|({2a8bec00-0ab0-4b4d-bd3d-4f59eada8fd8})|({2aeb1f92-6ddc-49f5-b7b3-3872d7e019a9})|({2bb68b03-b528-4133-9fc4-4980fbb4e449})|({2cac0be1-10a2-4a0d-b8c5-787837ea5955})|({2d3c5a5a-8e6f-4762-8aff-b24953fe1cc9})|({2ee125f1-5a32-4f8e-b135-6e2a5a51f598})|({2f53e091-4b16-4b60-9cae-69d0c55b2e78})|({3a65e87c-7ffc-408d-927e-ebf1784efd6d})|({3a26e767-b781-4e21-aaf8-ac813d9edc9f})|({3c3ef2a3-0440-4e77-9e3c-1ca8d48f895c})|({3dca6517-0d75-42d2-b966-20467f82dca1})|({3f4191fa-8f16-47d2-9414-36bfc9e0c2bf})|({3f49e12b-bb58-4797-982c-4364030d96d9})|({4aa2f47a-0bae-4a47-8a1b-1b93313a2938})|({04abafc7-7a65-401d-97f3-af2853854373})|({4ad16913-e5cb-4292-974c-d557ef5ec5bb})|({4b1050c6-9139-4126-9331-30a836e75db9})|({4b1777ec-6fe4-4572-9a29-5af206e003bf})|({4beacbbb-1691-40e7-8c1e-4853ce2e2dee})|({4c140bc5-c2ad-41c3-a407-749473530904})|({4cbef3f0-4205-4165-8871-2844f9737602})|({4dac7c77-e117-4cae-a9f0-6bd89e9e26ab})|({04ed02dc-0cb0-40c2-8bc8-6f20843024b8})|({4f6b6aaf-c5a1-4fac-8228-ead4d359dc6d})|({4f8a15fb-45c2-4d3b-afb1-c0c8813a4a5a})|({5af74f5a-652b-4b83-a2a9-f3d21c3c0010})|({5b0f6d3c-10fd-414c-a135-dffd26d7de0f})|({5b421f02-e55e-4b63-b90e-aa0cfea01f53})|({5b620343-cd69-49b8-a7ba-f9d499ee5d3d})|({5c5cf69b-ed92-4429-8d26-ff3bb6c37269})|({5cf77367-b141-4ba4-ac2a-5b2ca3728e81})|({5da81d3d-5db1-432a-affc-4a2fe9a70749})|({5eac1066-90c3-4ba0-b361-e6315dcd6828})|({5ec4c837-59b9-496d-96e2-ff3fa74ca01f})|({5efd8c7a-ff37-41ac-a55c-af4170453fdf})|({5f4e63e4-351f-4a21-a8e5-e50dc72b5566})|({6a934ff5-e41d-43a2-baf5-2d215a869674})|({06a71249-ef35-4f61-b2c8-85c3c6ee5617})|({6ad26473-5822-4142-8881-0c56a8ebc8c0})|({6cee30bc-a27c-43ea-ac72-302862db62b2})|({6ed852d5-a72e-4f26-863f-f660e79a2ebb})|({6eee2d17-f932-4a43-a254-9e2223be8f32})|({6f13489d-b274-45b6-80fa-e9daa140e1a4})|({6fa41039-572b-44a4-acd4-01fdaebf608d})|({7ae85eef-49cf-440d-8d13-2bebf32f14cf})|({7b3c1e86-2599-4e1a-ad98-767ae38286c8})|({7b23c0de-aa3d-447f-9435-1e8eba216f09})|({7b71d75e-51f5-4a71-9207-7acb58827420})|({7c6bf09e-5526-4bce-9548-7458ec56cded})|({7ca54c8d-d515-4f2a-a21f-3d32951491a6})|({7d932012-b4dd-42cc-8a78-b15ca82d0e61})|({7d5e24a1-7bef-4d09-a952-b9519ec00d20})|({7eabad73-919d-4890-b737-8d409c719547})|({7eaf96aa-d4e7-41b0-9f12-775c2ac7f7c0})|({7f8bc48d-1c7c-41a0-8534-54adc079338f})|({7f84c4d8-bdf5-4110-a10d-fa2a6e80ef6a})|({8a6bda75-4668-4489-8869-a6f9ccbfeb84})|({8a0699a0-09c3-4cf1-b38d-fec25441650c})|({8ab8c1a2-70d4-41a8-bf78-0d0df77ac47f})|({8b4cb418-027e-4213-927a-868b33a88b4f})|({8fcfe2b3-598e-4861-a5d4-0d77993f984b})|({9a941038-82fa-4ae4-ba98-f2eb2d195345})|({9b8a3057-8bf4-4a9e-b94b-867e4e71a50c})|({9b8df895-fcdd-452a-8c46-da5be345b5bc})|({09c8fa16-4eec-4f78-b19d-9b24b1b57e1e})|({09cbfddf-5e55-4676-920d-5a16cb9e4cb5})|({9cf8d28f-f546-4871-ac4d-5faff8b5bde3})|({9d592fd5-e655-461a-9b28-9eba85d4c97f})|({9fc6e583-78a5-4a2b-8569-4297bb8b3300})|({014d98ce-dab9-4c1d-8643-166e75d7cb4d})|({18c64b09-4ccb-4c21-ba6f-ebd4a1efa034})|({21d83d85-a636-4b18-955d-376a6b19bd19})|({22ecf14b-ead6-4684-a498-7b2b839a4c97}))$/", + "prefs": [], + "schema": 1576757328395, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1479009", + "why": "Malicious add-ons disguising as updates or useful add-ons, but violating data collection policies, user-control, no surprises and security.", + "name": "Firefox Update (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cae5d906-0b1d-4d1c-b83f-f9727b8c4a29", + "last_modified": 1576771657022 + }, + { + "guid": "/^(({0f9e469e-4245-43f8-a7a8-7e730f80d284})|({117ca2f3-df4c-4e17-a5c5-b49077e9c731})|({11db147a-a1cb-43dd-9c05-0d11683483e1})|({1ed2af70-9e89-42db-a9e8-17ae594003ac})|({24ed6bdc-3085-413b-a62e-dc5dd30272f4})|({2aa19a7a-2a43-4e0d-a3dc-abb33fa7e2b6})|({3d6fbbb3-6c80-47bb-af20-56fcaebcb9ca})|({42f4c194-8929-42b9-a9a3-afa56dd0913b})|({46740fa0-896d-4f2e-a240-9478865c47c2})|({4718da68-a373-4a03-a77b-0f49b8bb40ee})|({4d41e0b8-bf7e-45ab-bd90-c426b420e3ee})|({50957a38-c15d-42da-94f5-325bc74a554c})|({5650fc63-a7c5-4627-8d0a-99b20dcbd94b})|({5c5c38ec-08bf-493a-9352-6ccf25d60c08})|({67ecb446-9ccd-4193-a27f-7bd1521bd03c})|({71f01ffe-226d-4634-9b21-968f5ce9f8f5})|({72f31855-2412-4998-a6ff-978f89bba0c3})|({7b3c1e86-2599-4e1a-ad98-767ae38286c8})|({7c37463c-001e-4f58-9e88-aaab2a624551})|({7de64f18-8e6b-4c41-9b05-d8872b418026})|({82dcf841-c7e1-4764-bb47-caa28909e447})|({872f20ea-196e-4d11-8835-1cc4c877b1b8})|({8efee317-546f-418d-82d3-60cc5187acf5})|({93deeba1-0126-43f7-a94d-4eecfce53b33})|({9cc12446-16da-4200-b284-d5fc18670825})|({9cd27996-6068-4597-8e97-bb63f783a224})|({9fdcedc7-ffde-44c3-94f6-4196b1e0d9fc})|({a191563e-ac30-4c5a-af3d-85bb9e9f9286})|({a4cb0430-c92e-44c6-9427-6a6629c4c5f6})|({a87f1b9b-8817-4bff-80fd-db96020c56c8})|({ae29a313-c6a9-48be-918d-1e4c67ba642f})|({b2cea58a-845d-4394-9b02-8a31cfbb4873})|({b420e2be-df31-4bea-83f4-103fe0aa558c})|({b77afcab-0971-4c50-9486-f6f54845a273})|({b868c6f4-5841-4c14-86ee-d60bbfd1cec1})|({b99ae7b1-aabb-4674-ba8f-14ed32d04e76})|({b9bb8009-3716-4d0c-bcb4-35f9874e931e})|({c53c4cbc-04a7-4771-9e97-c08c85871e1e})|({ce0d1384-b99b-478e-850a-fa6dfbe5a2d4})|({cf8e8789-e75d-4823-939f-c49a9ae7fba2})|({d0f67c53-42b5-4650-b343-d9664c04c838})|({dfa77d38-f67b-4c41-80d5-96470d804d09})|({e20c916e-12ea-445b-b6f6-a42ec801b9f8})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({e7d03b09-24b3-4d99-8e1b-c510f5d13612})|({fa8141ba-fa56-414e-91c0-898135c74c9d})|({fc99b961-5878-46b4-b091-6d2f507bf44d})|(firedocs@mozilla\\.com)|(firetasks@mozilla\\.com)|(getta@mozilla\\.com)|(javideo@mozilla\\.com)|(javideo2@mozilla\\.com)|(javideos@mozilla\\.com)|(javideosz@mozilla\\.com)|(search_free@mozilla\\.com)|(search-unlisted@mozilla\\.com)|(search-unlisted101125511@mozilla\\.com)|(search-unlisted10155511@mozilla\\.com)|(search-unlisted1025525511@mozilla\\.com)|(search-unlisted1099120071@mozilla\\.com)|(search-unlisted1099125511@mozilla\\.com)|(search-unlisted109925511@mozilla\\.com)|(search-unlisted11@mozilla\\.com)|(search-unlisted111@mozilla\\.com)|(search-unlisted12@mozilla\\.com)|(search-unlisted14400770034@mozilla\\.com)|(search-unlisted144007741154@mozilla\\.com)|(search-unlisted144436110034@mozilla\\.com)|(search-unlisted14454@mozilla\\.com)|(search-unlisted1570124111@mozilla\\.com)|(search-unlisted1570254441111@mozilla\\.com)|(search-unlisted15721239034@mozilla\\.com)|(search-unlisted157441@mozilla\\.com)|(search-unlisted15757771@mozilla\\.com)|(search-unlisted1577122001@mozilla\\.com)|(search-unlisted15777441001@mozilla\\.com)|(search-unlisted15788120036001@mozilla\\.com)|(search-unlisted157881200361111@mozilla\\.com)|(search-unlisted1578899961111@mozilla\\.com)|(search-unlisted157999658@mozilla\\.com)|(search-unlisted158436561@mozilla\\.com)|(search-unlisted158440374111@mozilla\\.com)|(search-unlisted15874111@mozilla\\.com)|(search-unlisted1741395551@mozilla\\.com)|(search-unlisted17441000051@mozilla\\.com)|(search-unlisted174410000522777441@mozilla\\.com)|(search-unlisted1768fdgfdg@mozilla\\.com)|(search-unlisted180000411@mozilla\\.com)|(search-unlisted18000411@mozilla\\.com)|(search-unlisted1800411@mozilla\\.com)|(search-unlisted18011888@mozilla\\.com)|(search-unlisted1801668@mozilla\\.com)|(search-unlisted18033411@mozilla\\.com)|(search-unlisted180888@mozilla\\.com)|(search-unlisted181438@mozilla\\.com)|(search-unlisted18411@mozilla\\.com)|(search-unlisted18922544@mozilla\\.com)|(search-unlisted1955511@mozilla\\.com)|(search-unlisted2@mozilla\\.com)|(search-unlisted3@mozilla\\.com)|(search-unlisted4@mozilla\\.com)|(search-unlisted400@mozilla\\.com)|(search-unlisted40110@mozilla\\.com)|(search-unlisted5@mozilla\\.com)|(search-unlisted55@mozilla\\.com)|(search@mozilla\\.com)|(searchazsd@mozilla\\.com)|(smart246@mozilla\\.com)|(smarter1@mozilla\\.com)|(smarters1@mozilla\\.com)|(stream@mozilla\\.com)|(tahdith@mozilla\\.com))$/", + "prefs": [], + "schema": 1576611696962, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1480591", + "why": "These add-ons violate the no-surprises and user-control policy.", + "name": "Search engine hijacking malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cee5c2ab-1059-4b15-a78c-1203116552c4", + "last_modified": 1576771657018 + }, + { + "guid": "/^((application2@fr-metoun\\.com)|(application@br-annitop\\.com)|(application@br-atoleg\\.com)|(application@br-cholty\\.com)|(application@br-debozoiz\\.com)|(application@br-echite\\.com)|(application@br-estracep\\.com)|(application@br-exatrom\\.com)|(application@br-iginot\\.com)|(application@br-imastifi\\.com)|(application@br-isobiv\\.com)|(application@br-ludimaro\\.com)|(application@br-pintoula\\.com)|(application@br-proufta\\.com)|(application@br-qhirta\\.com)|(application@br-qibizar\\.com)|(application@br-qopletr\\.com)|(application@br-roblaprouf\\.com)|(application@br-rosalop\\.com)|(application@br-samalag\\.com)|(application@br-sopreni\\.com)|(application@br-stoumo\\.com)|(application@br-villonat\\.com)|(application@br-zoobre\\.com)|(application@de-barbuna\\.com)|(application@de-bicelou\\.com)|(application@de-blabuma\\.com)|(application@de-dalofir\\.com)|(application@de-elplic\\.com)|(application@de-erotah\\.com)|(application@de-ertuck\\.com)|(application@de-eurosty\\.com)|(application@de-ezigat\\.com)|(application@de-lorelam\\.com)|(application@de-losimt\\.com)|(application@de-luchil\\.com)|(application@de-miligap\\.com)|(application@de-open-dog\\.com)|(application@de-rydima\\.com)|(application@de-slapapi\\.com)|(application@de-soqano\\.com)|(application@de-treboola\\.com)|(application@de-vasurk\\.com)|(application@de-ygivas\\.com)|(application@es-biloufer\\.com)|(application@es-boulass\\.com)|(application@es-cemaseur\\.com)|(application@es-elixet\\.com)|(application@es-gestona\\.com)|(application@es-glicalol\\.com)|(application@es-griloup\\.com)|(application@es-iblep\\.com)|(application@es-iglere\\.com)|(application@es-jounyl\\.com)|(application@es-klepst\\.com)|(application@es-nofinaj\\.com)|(application@es-ofarnut\\.com)|(application@es-phistouquet\\.com)|(application@es-pronzal\\.com)|(application@es-roterf\\.com)|(application@es-taapas\\.com)|(application@es-tatoflex\\.com)|(application@fr-acomyl\\.com)|(application@fr-avortep\\.com)|(application@fr-blicac\\.com)|(application@fr-bloubil\\.com)|(application@fr-carazouco\\.com)|(application@fr-cichalou\\.com)|(application@fr-consimis\\.com)|(application@fr-cropam\\.com)|(application@fr-deplitg\\.com)|(application@fr-doadoto\\.com)|(application@fr-domeoco\\.com)|(application@fr-domlaji\\.com)|(application@fr-eferif\\.com)|(application@fr-eivlot\\.com)|(application@fr-eristrass\\.com)|(application@fr-ertike\\.com)|(application@fr-esiliq\\.com)|(application@fr-fedurol\\.com)|(application@fr-grilsta\\.com)|(application@fr-hyjouco\\.com)|(application@fr-intramys\\.com)|(application@fr-istrubil\\.com)|(application@fr-javelas\\.com)|(application@fr-jusftip\\.com)|(application@fr-lolaji\\.com)|(application@fr-macoulpa\\.com)|(application@fr-mareps\\.com)|(application@fr-metoun\\.com)|(application@fr-metyga\\.com)|(application@fr-mimaloy\\.com)|(application@fr-monstegou\\.com)|(application@fr-oplaff\\.com)|(application@fr-ortisul\\.com)|(application@fr-pastamicle\\.com)|(application@fr-petrlimado\\.com)|(application@fr-pinadolada\\.com)|(application@fr-raepdi\\.com)|(application@fr-soudamo\\.com)|(application@fr-stoumo\\.com)|(application@fr-stropemer\\.com)|(application@fr-tlapel\\.com)|(application@fr-tresdumil\\.com)|(application@fr-troglit\\.com)|(application@fr-troplip\\.com)|(application@fr-tropset\\.com)|(application@fr-vlouma)|(application@fr-yetras\\.com)|(application@fr-zorbil\\.com)|(application@fr-zoublet\\.com)|(application@it-bipoel\\.com)|(application@it-eneude\\.com)|(application@it-glucmu\\.com)|(application@it-greskof\\.com)|(application@it-gripoal\\.com)|(application@it-janomirg\\.com)|(application@it-lapretofe\\.com)|(application@it-oomatie\\.com)|(application@it-platoks\\.com)|(application@it-plopatic\\.com)|(application@it-riploi\\.com)|(application@it-sabuf\\.com)|(application@it-selbamo\\.com)|(application@it-sjilota\\.com)|(application@it-stoploco\\.com)|(application@it-teryom\\.com)|(application@it-tyhfepa\\.com)|(application@it-ujdilon\\.com)|(application@it-zunelrish\\.com)|(application@uk-ablapol\\.com)|(application@uk-blamap\\.com)|(application@uk-cepamoa\\.com)|(application@uk-cloakyz\\.com)|(application@uk-crisofil\\.com)|(application@uk-donasip\\.com)|(application@uk-fanibi\\.com)|(application@uk-intramys\\.com)|(application@uk-klastaf\\.com)|(application@uk-liloust\\.com)|(application@uk-logmati\\.com))$/", + "prefs": [], + "schema": 1576756704239, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1487627", + "why": "Add-ons whose main purpose is to track user browsing behavior.", + "name": "Abusive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "914ec360-d35e-4420-a88f-1bad3513f054", + "last_modified": 1576771657015 + }, + { + "guid": "/^((\\{0bf1c111-c256-4a17-891d-1bc69338162e\\})|(\\{0ffbc4b1-f269-4cff-9552-5f77337ebc1a\\})|(\\{1bbdc69d-55d3-4872-bd03-14eb05e7a7ad\\})|(\\{1ce00b82-ac47-43e6-a69c-f7dc9344168a\\})|(\\{2b01628b-0110-4965-972c-7a0b624fb99f\\})|(\\{3a84e0b0-3151-449e-b6e8-1062036afac6\\})|(\\{3bbcc16b-23f7-40a6-b88c-9ced9d009c93\\})|(\\{3ea48f4a-b585-44a7-aff5-faeb5e5b47d5\\})|(\\{7b161d2c-ee98-4321-a78a-433950672c8a\\})|(\\{8a8d97d8-b879-4024-8321-765e0f395b84\\})|(\\{9c0d6766-debe-4461-b14f-68ddfc13a78a\\})|(\\{014f9781-c104-41a4-a983-fc6aa4690664\\})|(\\{27ffdb27-0a34-4dea-a483-3b357bc6a5fe\\})|(\\{81ba256a-4f32-40df-86b5-e6b9861481e1\\})|(\\{338c7503-fb54-4b69-a84b-916f7452c7fa\\})|(\\{400e053f-55df-4e86-a91a-eae8d7b7bcd1\\})|(\\{617e0484-8346-44f2-851e-60ab89a919f9\\})|(\\{656a0095-d852-4dcc-a107-764df7ad0ec4\\})|(\\{754a330b-efbe-4016-8526-bf0f2e11e45e\\})|(\\{802ba900-013c-42f6-a11a-093c4bf35baa\\})|(\\{2771ce08-4898-4f58-89a5-e2b9d00bfab2\\})|(\\{3906b944-92f3-4d43-89dc-31ad6484a77c\\})|(\\{6516cdbc-9332-437f-89ac-b57470655542\\})|(\\{6847c507-1793-4be2-be86-4c2cc0b445bf\\})|(\\{9687db9b-410c-47f2-8c36-fde63c7c29e4\\})|(\\{0035237e-97ab-40eb-ba9d-c453fb6aa079\\})|(\\{20143127-e0bd-4396-aee9-52229cf9b5eb\\})|(\\{33254399-d5b2-4d84-b90b-9c4d4dc71112\\})|(\\{34621968-1952-4428-909d-df5b220efe74\\})|(\\{83769978-21cf-417c-b4a9-582b4161e395\\})|(\\{aa369db0-4232-47b8-bbbb-49ad31d49dce\\})|(\\{aff733de-d7d8-49c2-824a-7f2b2e282927\\})|(\\{c0b587fe-766b-446f-9aae-bc6edc9f6f4c\\})|(\\{c47a75b9-c6d2-4009-a245-c6dcedeea932\\})|(\\{c51bd197-28bd-442f-8537-dea5ae129047\\})|(\\{cac044a2-b93c-4f24-bf2f-b902741d29a8\\})|(\\{de17ce6f-389f-4368-9675-b9ed93133f17\\})|(\\{e2b105bc-1821-4838-bdf9-0fa4f6781b18\\})|(\\{e6c8bc7f-0133-418a-86ed-ba2c0a6925df\\})|(\\{f4acda5f-a75b-4b3b-8a73-8ca3df8d5f57\\})|(\\{f4fd18ee-8f6a-4708-8338-7e7981b73453\\})|(\\{f2320322-1fff-4998-bc28-4ad61621012a\\})|(\\{ff939f5e-a97c-4c14-b853-9c82519dbf73\\})|(@complete-youtube-downloader)|(@swsearchassist)|(@swsearchassist2)|(@youtube-download-helper-addon-1)|(@youtube-download-helper-addon-3)|(@ytd-support)|(@ytmp4-support)|(@ytu-support)|(18-plus-bypass@agebypass\\.org)|(18plus@sweetytweety\\.jp)|(addon@firefox-addon-s\\.com)|(ageverify@doubletrouble\\.net)|(auto-fill-dhruv\\.techapps@gmail\\.com)|(awesomeaddons@gmail\\.com)|(blndkmebkmenignoajhoemebccmmfjib@chrome-store-foxified--730948579)|(boomerang-for-gmailtm@chrome-store-foxified--1895216441)|(boomerang-for-gmailtm@chrome-store-foxified-1472004183)|(browsing_certificate@easycerts\\.in)|(certs-js@verify\\.org)|(clear-flash-cookies@tubedownload\\.org)|(dghpnfeglanbbjaggjegpbijhcbnfdno@chrome-store-foxified--1026618965)|(dghpnfeglanbbjaggjegpbijhcbnfdno@chrome-store-foxified--1382673267)|(dghpnfeglanbbjaggjegpbijhcbnfdno@chrome-store-foxified-3810896411)|(dhiaggccakkgdfcadnklkbljcgicpckn@chrome-store-foxified-1917762393)|(dhiaggccakkgdfcadnklkbljcgicpckn@chrome-store-foxified-2539369515)|(dhiaggccakkgdfcadnklkbljcgicpckn@chrome-store-foxified-3411285726)|(dhiaggccakkgdfcadnklkbljcgicpckn@chrome-store-foxified-3957753373)|(dhruv@gmail\\.com)|(easy\\.download@youtube\\.com)|(easy18plusverify@agehelper\\.com)|(EasyQR@johndoe)|(easytranslate@johndoehits)|(ecaieeiecbdhkcgknidmfelflleobbnp@chrome-store-foxified-2878848146)|(eurekasakamika@chrome-store-foxified-unsigned)|(faeeclonpikbempnbjbbajfjjajjgfio@chrome-store-foxified--1071037210)|(faeeclonpikbempnbjbbajfjjajjgfio@chrome-store-foxified-335403930)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified--546579415)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified--929033716)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified--1776201342)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified-411918147)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified-711293137)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified-1406852911)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified-1805495496)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified-2344964585)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified-2515600300)|(gefiaaeadjbmhjndnhedfccdjjlgjhho@chrome-store-foxified-2947667317)|(generated-c5el8k8zv2b1gcncleefj9@chrome-store-foxified--1160265316)|(html5-video-everywhere@lejenome\\.me)|(iapifmceeokikomajpccajhjpacjmibe@chrome-store-foxified-6029610)|(info@ytdownloader\\.info))$/", + "prefs": [], + "schema": 1576756923769, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1525880", + "why": "Add-ons that include abusive or malicious remote code.", + "name": "Various abusive add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "be96c9a1-0b01-468e-ac9b-761ba3587162", + "last_modified": 1576771657011 + }, + { + "guid": "/^((\\{7e4a75c1-dddc-4496-9963-1c6ac99cf226\\})|(\\{35af257d-14dd-4cd0-8ebd-2d30c2b30561\\})|(\\{d6362448-1e8f-47bf-9d2f-491648d18e3d\\})|(\\{80a42dcf-193e-43a2-b662-d6b14882403f\\})|(\\{044e39fc-333b-423c-8291-26078a780b02\\})|(\\{68b3c6ce-162f-4ece-9ffa-8279855a4228\\})|(\\{057b93a7-84e6-43ff-9686-d452435ed3c5\\})|(\\{1223cfa2-7aad-4a16-b98a-6bf63b346835\\})|(\\{9815ca8b-a77c-4e4d-beac-aad1c7021dcb\\})|(\\{e3f2795a-cefc-4f7f-9435-5f091b988d2f\\})|(\\{98fd0bd5-f486-4d81-9eb1-e89e9d56bfa2\\})|(\\{f96fafd2-5860-4bfa-9537-3f2ca9dd496e\\})|(\\{da93cdd9-6aca-410e-b2f2-e927da726559\\})|(\\{d97e0506-d330-4547-8a5c-093b8aa08d7a\\})|(\\{425ad6b3-72b8-43c0-be7c-2f6585fa0ec1\\})|(\\{0375f007-f5ba-46ec-86d2-c5da84576562\\})|(\\{e8915f55-6566-4872-97eb-d77fbdbf2fb3\\})|(\\{ebd3a0c4-bf9e-4dfa-b487-f77722055edb\\})|(\\{7cc62e47-ed20-45bc-8c92-bb57128e78d6\\})|(\\{b5a15631-6429-49bd-a670-e83ac41f93a9\\})|(\\{f263d545-3234-460f-b546-a8406a0a729d\\})|(\\{6468c148-9888-4243-8de5-cb6291cac82a\\})|(\\{da2281db-0036-46f6-8878-ff26e1cf6a2b\\})|(\\{63f579ba-eaf5-4e1a-a7c2-c5e889beaf9f\\})|(\\{84569fbb-d367-40ce-b24b-fd3b611283b8\\})|(\\{da2bc16f-b499-401b-8771-9d9f32d88f86\\})|(\\{1a275ad6-5dd3-47e9-a563-41a0bebdfd90\\})|(\\{e07ebf1e-5917-46a2-95d9-61d9b51f3797\\})|(\\{0d6791d2-ce0b-4f78-90e4-8e773703bd35\\})|(\\{502c7ef7-745c-4ea0-8066-a17cf1b74957\\})|(\\{c93f0aeb-ae9b-49d9-835b-c58a6b03aa46\\})|(\\{1f0bf2a4-aff5-42d3-8633-71e65f289250\\})|(\\{28766320-358e-42e3-a2c7-67ec77552424\\})|(\\{74d4fcda-c103-4fb7-810a-4596530c00a4\\})|(\\{7b3fd37a-a127-41a0-9e4d-59ccfa165e41\\})|(\\{787fa0b0-d5f1-4454-8b0c-72d191d6775f\\})|(\\{e2bae2ed-0368-48e7-8671-3bdcc5d7713f\\})|(\\{fee16fb4-830f-438a-a3d5-f7e911d23e02\\})|(\\{72113405-b4a5-46c3-a7c6-5353568b87bd\\})|(\\{5ede50a4-4151-4635-804f-a6f56115a0c6\\})|(\\{c11487a0-d104-4bc3-814b-474f8c29049c\\})|(\\{35690b6e-1979-4ea3-89aa-44a94dda2afa\\})|(\\{e9d698ef-bad4-4960-9df3-8c41605a6d7b\\})|(\\{1472b3c1-cae8-42c4-bbdf-e71134dccf08\\})|(\\{7a40b654-1232-4e76-81e7-d95260db25cd\\})|(\\{f54699c8-c82f-4d6e-a161-919bbe8410de\\})|(\\{dca6a5cd-0d24-442a-afd4-80572bb20c34\\})|(\\{b8d5d169-f076-4098-b671-a3cb8b410f56\\})|(\\{903e6561-0646-4c38-8039-d372d8e7c90a\\})|(\\{b39977b9-bcb2-448b-9d7b-9aec7f62bc26\\})|(\\{059b5c30-b96a-48df-8083-5fff97a8f9bf\\})|(\\{1d0351bb-1d96-4779-b639-44eeceb2ebfb\\})|(\\{80c0bdb4-ba98-472d-ae56-afd8b3021115\\})|(\\{4dfc5596-9655-4b0c-819d-e2ff48fb8556\\})|(\\{d7d3ed3c-6f73-42cb-b724-c33fccc1b465\\})|(\\{b378a858-89bb-492e-8b4d-eb83e910a14b\\})|(\\{ec1fa94c-8700-49d0-ba5d-df99a912519e\\})|(\\{4db5d249-881f-4442-8c01-28536c45ebfd\\})|(\\{7a411d82-fc50-4f20-bd2c-b2b065f18097\\})|(\\{675e002b-e144-4694-a725-9e8cc6a3fa67\\})|(\\{1902a069-c039-421e-b502-1e367c237196\\})|(\\{866bb3a8-82bb-4c9a-bca5-26fd5f37c4ec\\})|(\\{6a4e7017-43cd-4646-bb48-003620bb60fe\\})|(\\{bc5c676e-a75f-475b-a27c-79687b1de3ec\\})|(\\{6b544e1a-932d-4da9-aafe-c4b4bbfe1958\\})|(\\{99631434-ff1e-49d3-88d3-9ac40d0dd1bd\\})|(\\{623b31e0-f289-47cf-995e-5a195e261758\\})|(\\{1b4d88a5-4b5d-44c8-849c-82f129a7dacd\\})|(\\{48ba880a-b7c2-4e4e-af55-9134ac926c61\\})|(\\{4b498e2a-8b17-47c0-a449-89a76b6e737f\\})|(\\{d9cbd45f-cdbc-4be1-bb16-8e60764630ff\\})|(\\{bfaaa94a-1a93-4a1c-9b54-9dbe98f3ef07\\})|(\\{87b93e6e-70a6-4538-9848-e9d0f060e372\\})|(\\{fea4fd50-ed6a-4b8e-b00d-3b2276df6e34\\})|(\\{c15450f8-8da2-4add-a8f6-603d90e8d344\\})|(\\{ec972135-8e5c-49d4-bff8-b6006b21f2d2\\})|(\\{b039f24d-8b51-40d3-abf7-55e1dc502112\\})|(\\{b308870f-ae9a-4972-af28-0218717a47f4\\})|(\\{9349a202-8b8e-4777-ba93-c723810da51e\\})|(\\{798750dc-0057-47e0-a1af-73dec73544fa\\})|(\\{186e4b6a-e3f0-4970-8f7b-05ab6bc50320\\})|(\\{dec8de3e-d3a4-4946-bcbd-c3523fee11c5\\})|(\\{06539c62-00d3-4513-9aa4-048dd273107a\\})|(\\{b200a289-900a-4953-b2c6-b7a323d6fb66\\})|(\\{4080defb-6c6b-4012-bcac-71379e9c430f\\})|(\\{b110855c-90dd-427a-894c-54b93c6572b7\\})|(\\{dd599e99-3a48-4e36-9d83-56f8c0019d4d\\})|(\\{4f43f2c7-c1e6-4091-88fe-c829b3bfe553\\})|(\\{b7a022bc-6b89-4ac1-a1fa-bf02251336b0\\})|(\\{1aa370ca-9865-4c52-89a8-79e95abc82f7\\})|(\\{fb727d0f-7c3d-4bf6-8be4-284e7e8b8f83\\})|(\\{1579b5dd-ef3d-4754-bc59-8a7707fe1219\\})|(\\{66f0cb42-bb3e-4a16-90c1-bed1e3be4aad\\})|(\\{f13a1f79-f63a-4332-a9c9-11fc50328fc2\\})|(\\{29962f4d-bf74-4775-9d02-31fe546d6fa6\\})|(\\{aa539764-9ec3-41a6-af0e-6c2dc46ecbf5\\})|(\\{9412adf1-2714-4cb2-ad5b-13d41096234a\\})|(\\{86f2f4cc-97c5-4cc5-8151-c327ab379fba\\}))$/", + "prefs": [], + "schema": 1576757017958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549214", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various Keyloggers" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "654077f4-a8b3-4822-8962-0bb1cac1d70d", + "last_modified": 1576771657006 + }, + { + "guid": "/^((\\{0209c9a2-f58e-4ca5-ba95-80de8a0bec36\\})|(\\{0429d1cf-e6c7-46b9-9959-4d24263f5b9f\\})|(\\{04aed622-650c-44b2-968e-a8740024bdfe\\})|(\\{054ccccf-bb7d-4d0d-ae7e-94931a469627\\})|(\\{06440d64-2a92-45a7-8e8e-c14a35f9224d\\})|(\\{06e83ce4-370c-41c0-bfb7-62bb0e77cabb\\})|(\\{06fe00d4-24df-497f-90e0-88db402cc9d6\\})|(\\{077e07c1-b948-4e88-9965-226cde465f9f\\})|(\\{07b424b4-8a9d-4fe6-afd5-1f2135f5f4ea\\})|(\\{080f27b3-ad41-4bf4-84bb-b6df1d395b0e\\})|(\\{0a3ba90b-224e-48ae-9440-b92d4ac03ee6\\})|(\\{0bd278d9-fbc6-40dd-914d-5e696c4255cb\\})|(\\{0c3c7c7c-3d15-4cf7-936d-dcf6070e82ce\\})|(\\{0ee32a7c-74b2-4a55-b388-8034b39c6b1d\\})|(\\{0f806e24-dbcc-4f14-b8f5-cfd7f88d6302\\})|(\\{0f9b7554-16d4-4496-8f01-e396256033b7\\})|(\\{1331d9a3-b9ec-43fc-a369-f73a926849b9\\})|(\\{14aca62f-1cc5-4424-a30a-ffb3d424b5bc\\})|(\\{14ec0bb2-f06f-46ad-b951-b810f7651284\\})|(\\{16744b56-7518-4526-bf9a-2531d694fb1a\\})|(\\{179d7013-b6d8-4a89-a861-30e0e8a7faee\\})|(\\{17cff984-12b5-438d-a915-41d7d006de4f\\})|(\\{17ee37ab-fe67-45bd-9666-bcf57a371e46\\})|(\\{19f6f2dd-32df-47a2-9b89-76543a987d46\\})|(\\{1a6ddac5-6ca0-4d59-a8e5-02345c67f703\\})|(\\{1b50ef3b-a364-4089-8ef6-1031cc7a0d1d\\})|(\\{1b830180-08d2-4381-a516-b84aba36e52f\\})|(\\{1cdff066-cb3a-4abf-95d0-39691e53dc75\\})|(\\{1e778837-1740-4a35-9eb1-e16b2c189037\\})|(\\{260d8452-72d2-4860-b14e-dd3fcb779656\\})|(\\{262c435a-42e0-4ca1-a713-f52672691f4e\\})|(\\{26d2406b-5118-44fe-a479-15a8c4f6f2bd\\})|(\\{28a26807-fdc2-4e79-b2a1-efcb1c21d199\\})|(\\{28c24c28-a094-4915-a2ff-5ff91caf076b\\})|(\\{2afddffd-6246-45f6-af19-a7803095bdf0\\})|(\\{2d37fd0a-5ae0-4d83-bc0e-fc7d870587c6\\})|(\\{2df75889-c43e-4f4d-b43b-e51d9b50167c\\})|(\\{2e4e320a-d5f6-4685-89f0-4d7084209c06\\})|(\\{2f93ea6a-1c1b-4456-b821-e8ba50aedece\\})|(\\{3034cb02-b9bb-4e8a-8749-cdd7fd1a6902\\})|(\\{3862859a-78c6-474e-b30e-303e86a7c6a4\\})|(\\{386afcb7-64f4-41db-b3e8-a76602ebb2fa\\})|(\\{38b61e2b-1af3-4f35-bdf0-cc4e3afc4880\\})|(\\{395065fd-1b7f-400f-aecb-9cfbcd9d607e\\})|(\\{3b0055c9-ea2c-43be-a927-ecd342946367\\})|(\\{3b37c6dd-d5f0-494d-9dd2-175db561b99c\\})|(\\{3e5a09c0-5f26-4d40-a5d0-a853f1fa759d\\})|(\\{3f2d032a-29ac-4cbe-9463-563f3ba6eb7f\\})|(\\{3f6ea025-e6c2-4372-adca-cb60b811e4da\\})|(\\{4253e6be-5b91-4b66-b44e-11f6260cee0e\\})|(\\{42c5b340-7cda-4d89-81a4-4a20754b5250\\})|(\\{42d8241e-e5f6-47d7-95f6-b6649143b12a\\})|(\\{46061237-f12b-4738-b1e4-7b7070fc03ca\\})|(\\{46bcaa76-c21e-44a2-aed8-6ba630fcc961\\})|(\\{47f394e0-02be-4a08-b865-287b993ac40e\\})|(\\{497c92fb-4d7d-4b9e-9884-a178e5991ee1\\})|(\\{498d00a0-3d8a-45ff-8e8f-3c27fcd12df6\\})|(\\{49ec4e6d-8152-461e-a2f5-095ede6c3cab\\})|(\\{4a87eeaa-4aa5-4695-b393-1ca4f00b2f3e\\})|(\\{4b0d3b3a-d61c-4968-a338-8de76d044f80\\})|(\\{4b9b2a47-e06f-4948-a20f-78ec1ef4e84c\\})|(\\{4dc32f1c-374e-4886-9a62-80ecfc23ed17\\})|(\\{4e901df2-8301-4588-9bc9-1e9f6c4f996f\\})|(\\{4fb6f5ed-eb5a-4115-a635-57fecad85d50\\})|(\\{50c0ae9a-ebaf-44f7-9ea7-52c7d1066721\\})|(\\{5160a705-c8e9-40b9-900e-6d26559038de\\})|(\\{5232e216-65a2-44d0-ba11-05fc8c332af7\\})|(\\{53e6e44a-a0af-49e2-af72-db4518f153bb\\})|(\\{58c7b5da-a1cc-437a-9401-2a56eb77df7d\\})|(\\{59aa5a90-0034-4350-adfe-76aff37e73ee\\})|(\\{59c5d279-711e-4217-8e5e-1aa1497ffcaa\\})|(\\{5a3f607d-7e1a-4faf-88e2-5806d74d18d4\\})|(\\{5a6364f7-3237-462d-bd3f-7c501830ceb0\\})|(\\{5dc73bfe-4193-4390-ae50-ad928655e21f\\})|(\\{5e085187-2509-4f8f-80ed-78c06351a77a\\})|(\\{5edfb7c3-04a5-447d-9069-2093289a7b98\\})|(\\{6219dabe-8f5f-4130-a650-8cfa971d7182\\})|(\\{62c9c13b-d001-4c42-819c-31b9763973c0\\})|(\\{656da759-0ae4-4f3e-a798-8293a5df9476\\})|(\\{66d70c4a-ad30-4f3c-afb4-b498a60c49b3\\})|(\\{68cb3185-4f55-42cb-97ea-188924b1d6c3\\})|(\\{6b99e0e4-e2e8-4fff-9da5-81c0b9e92b62\\})|(\\{6de81b5e-7556-4fc4-9cac-df56e898f3bf\\})|(\\{7162613f-ea9c-48b3-a0e3-6700ea61a4c8\\})|(\\{71ef8107-d5fd-4d2c-94b7-2dcd07448622\\})|(\\{7284399c-6be5-42ff-8ddc-5cc52d46ab40\\})|(\\{7422ce07-cac7-4fe6-af6b-16f5e7e27d05\\})|(\\{76a7b38d-7044-4e36-8315-38db10506ec8\\})|(\\{7772f851-8dd4-4d96-b426-6cd9f739a599\\})|(\\{797129e6-8cc9-401e-b9fe-0fee15533e9a\\})|(\\{7a3429ae-f293-4a70-a13d-f57f153557e3\\})|(\\{7bed7063-0842-43d9-b672-5e5e55915d5d\\})|(\\{7c0220cc-89e5-4726-ada1-fa2ffa412f28\\})|(\\{7c3c79d6-7e31-4947-b9b4-dd21f461ccd4\\})|(\\{7c70cbc0-e80c-4f3b-97b2-2530c4ac1349\\})|(\\{7fc4f148-2648-40f5-bd99-d057ac1292a6\\})|(\\{808a2093-68ff-4f73-b239-0d0f105c4d98\\})|(\\{8411b8e3-e302-48a2-91ee-550102b938f6\\}))$/", + "prefs": [], + "schema": 1576757094905, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552164", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name while providing unwanted functionality. This is not a legitimate Flash Player add-on.", + "name": "Adobe Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2a4b5087-eca0-43e8-96f4-6632aabd83d3", + "last_modified": 1576771657003 + }, + { + "guid": "/^((akjbfncbadcmnkopckegnmjgihagponf@chromeStoreFoxified)|(akjbfncbadcmnkopckegnmjgihagponf@chromeStoreFoxified-2563213750)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-3767541208)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-2330732719)|(cidchfadpgepemappcgeafeicnjomaad@chrome-store-foxified-509978031)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-558690357)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-3523362862)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-850818380)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-3686225023)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-3659951669)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-1114585181)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-2593088680)|(edmdnjinnadgoalbaojbhkbognfappih@chrome-store-foxified-206569335)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-3272316989)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-96331909)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-2158751912)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-1554953450)|(kadbillinepbjlgenaliokdhejdmmlgp@chrome-store-foxified-323465212)|(kadbillinepbjlgenaliokdhejdmmlgp@chrome-store-foxified-3112875041)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-1868258955)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-611481225)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-162688242)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-1394660953)|(\\{de07e1ed-1597-45f9-957d-4edc44399451\\})|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified-294092903)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified--2032791676)|(akjbfncbadcmnkopckegnmjgihagponf@chrome-store-foxified--786206880)|(\\{76f8d31f-d1b6-4171-885e-6fcde28ca547\\})|(\\{b7492f2d-72b6-4816-83d5-9c82b3cc5581\\})|(\\{3f0fa616-3f92-42e2-ac1e-69ae7b1c7872\\})|(\\{2e324574-0761-4017-bc96-66270563e277\\})|(\\{950d03c6-722e-498d-90fc-ec9d9c1ab893\\})|(\\{6cb64844-2dca-4f29-82d1-cb59459ad824\\})|(\\{5347a8c7-a156-4455-8301-7d19d269bd2c\\})|(\\{17c69a23-df19-4655-aaa9-e8a35f186ddf\\})|(\\{381eb5ad-0f02-4146-85f4-2cc7c7a7dee4\\})|(\\{e797aab6-f3df-4d0d-89c2-320371720194\\})|(\\{91a95e76-4b27-427f-9554-7c1aa58c8489\\})|(\\{5bd5f5a3-3f30-4c90-bf5c-7ff32eae9fac\\})|(\\{e9cbcded-05e0-4cf0-9163-8507de356646\\})|(\\{4262365c-085f-4f2b-9bd7-048d7d1c90de\\})|(\\{d6d89cdf-36e4-44b5-8ea2-2283e25e99b9\\})|(\\{3ab34cbc-4a18-4fac-b629-3b10091d505e\\})|(\\{28beb080-37b1-42ec-a6e9-89cff276cc3e\\})|(\\{d83baff8-42f1-485c-bc61-0df0a2fa2834\\})|(\\{f1260949-ea01-4f69-b302-87ac898bc677\\})|(\\{f2bb825a-19b7-46ba-b759-557885e28ff9\\})|(\\{d1023b1e-87f6-49d4-b93d-80d94cafb101\\})|(\\{605bf342-f990-43b3-9053-b6ad563cc944\\})|(\\{20da0f4c-c6ee-4c4a-be76-8cb0fdd759b7\\})|(\\{29563a03-2ea3-4187-b3dc-af3027287df8\\})|(\\{9fc76cae-b6b4-45af-aa0e-81d1bf419812\\})|(\\{b83f6a6c-6bb3-492f-aad2-56a6b79a02d4\\})|(\\{4e340962-9d78-486c-8ec8-fdc8ba0448c3\\})|(\\{4f420c0e-824f-408b-8327-418934c566e9\\})|(\\{51057732-1a37-491c-afeb-dccbb18e2341\\})|(\\{ac9415c8-b978-4636-a0f6-99b75f1bfacc\\})|(\\{ba9d81ff-13da-4183-8b32-19cc18a198c3\\})|(\\{614f9cd7-d46e-47a5-bcd6-fc9cefc846ac\\})|(\\{83ab005b-85f8-4185-b500-26c78f29e470\\})|(\\{814b9b95-0470-42f5-9be1-b322ae1a300c\\})|(\\{c565d582-ef45-4ee5-a13d-e0bc544bb483\\})|(\\{bbc0a83c-ff01-4f55-beed-c8dd6256d99b\\})|(\\{00d71c76-8b41-4e12-877b-62ad742c5b5b\\})|(\\{22c15bb7-3cac-4480-ad95-8ef2b4762689\\})|(\\{4ce4a857-3ba4-46d3-83e1-424e608f8a1d\\})|(\\{638ad118-0407-437c-a657-f8bde7b0c511\\})|(\\{c35dba3d-eed7-4ee2-b7ed-b2f636810ea1\\})|(\\{7635e554-de52-4a55-81f4-5d4e7ac9e416\\})|(\\{b768c014-21ff-49c9-9a27-186e33574612\\})|(\\{e31ae098-b80a-4286-8688-8514ace2d0fd\\})|(\\{104607b9-ad49-4760-882a-5cc13164531a\\})|(\\{bf78148e-f4d1-48b7-92b2-93ca2003d619\\})|(\\{877777da-7695-4d7e-a469-2a4b4cfbe0c4\\})|(\\{b09f3de0-26c4-4790-ba8e-50a1d1725053\\})|(\\{a24b471c-9100-455c-825a-689533d24979\\})|(\\{12a8c732-c19a-468e-8be4-a16f9a27c845\\})|(\\{bad6c6a4-6108-4e44-b7e3-c05bed9d4e50\\})|(\\{1b598a16-ca58-41bf-8cc2-3741356363b9\\})|(\\{a5520fcc-b75a-4527-931b-e403aa8772ef\\})|(\\{cec7aeec-9352-4ed1-8362-8e384704ab29\\})|(\\{1bf3e066-3707-41eb-b62d-55da5bbe930d\\})|(\\{1fd8204a-f25b-47d0-bfac-35c41451e2e7\\})|(\\{ab1f1e53-9102-4f4f-a793-0a81f5669e13\\}))$/", + "prefs": [], + "schema": 1576756466412, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554606", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Various remote script injection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8258f92c-5b89-42a7-a984-dd4e94fa301a", + "last_modified": 1576771656999 + }, + { + "guid": "/^((\\{ca33d7f8-5b8c-4215-bf6e-a29b721024b8\\})|(\\{ef107eb3-c699-42ce-9310-1f36890fcac5\\})|(\\{48a3b395-8cb9-4093-b557-d967c653b13f\\})|(\\{27e7c4c8-916b-4dae-ab1d-46573fe889e6\\})|(\\{4795f211-f8b2-45b1-852c-982e1912414e\\})|(\\{000d5ed8-cf10-4929-89b5-f5369f50bbcd\\})|(\\{4b249174-1bc4-49c2-a0ca-eaa51facd4b0\\})|(\\{34d96b10-c44d-4398-9410-8d7d550d023a\\})|(\\{c067681d-dc69-4b05-8052-34fa69549aa8\\})|(\\{5b2dcc2b-08a9-4d36-a249-680a9e994938\\})|(\\{68cb60e7-4bfc-43cf-9875-f9548ad5d913\\})|(\\{ddca4204-8f9c-4e35-a8fe-47e94ffced48\\})|(\\{c5138dc7-3cf8-4117-9988-041c2a85868c\\})|(\\{140c670f-ee4d-4a16-bf83-c4012279b923\\})|(\\{d88779b5-daad-4ae8-abdc-3ff58c80da8a\\})|(\\{ab80f9f6-2a50-4074-b560-f839f1674bb3\\})|(\\{a8187405-efb6-4c1c-a9dc-e90fc064f55d\\})|(\\{f0d5c8c8-0697-4ee9-830d-3271ad125c17\\})|(\\{34e3121a-a2f2-4ada-b271-c661b8e0a215\\})|(\\{c4d8c3ad-ba8e-43e7-ae76-90521132805e\\})|(\\{ec5441c0-ddd4-4e70-8d02-92b99eb5f306\\})|(\\{ba00c2cd-f59d-44c5-984c-fbd066cdabfe\\})|(\\{b41fb99b-8e21-4eb8-b825-c6855daeb9c3\\})|(\\{c8329103-f242-4dee-9fca-b98e2e15c096\\})|(\\{59d0d43f-875f-4ebd-beeb-4dfa213a7d20\\})|(\\{9ce5d8a7-e97b-4341-bf16-c12ad44368c7\\})|(\\{e10ea3cf-17ea-4270-8602-83162b1c8309\\})|(\\{303e86df-ad8c-4a55-b921-5e2a32441834\\})|(\\{8ecebe6c-0ac5-4f3a-a32f-50b1686ee538\\})|(\\{b5150eb9-3cf8-4162-b114-56b289c45f75\\})|(\\{c04252ab-747b-4718-9d1c-bb90c72c4874\\})|(\\{976ecad8-b154-4201-a55e-4478a1651a42\\})|(\\{a0f98d44-f4b9-4726-9f01-7587ee46634c\\})|(\\{6b9c9f21-1108-4ae9-a1d8-d56566e20f13\\})|(\\{fdacc9f4-06f1-4619-bfbc-61f790e279df\\})|(\\{74d5b273-dcf7-4606-9b9d-0c5c38cbab80\\})|(\\{6fee68c0-1b38-483f-963b-43919f4ea797\\})|(\\{017a0e34-4942-47d7-a0ff-2093f14e17ae\\})|(\\{71251d4a-7ff4-4450-9459-163b911d9518\\})|(\\{6da358f8-9746-4c39-957d-b6821561b566\\})|(\\{4613ad29-db02-4d30-b857-b84a8ab412a2\\})|(\\{7993943a-0d47-4d30-8989-ce039ec1636f\\})|(\\{ac2fbafe-f182-49f0-920c-2e0d026b6c1f\\})|(\\{5f79fd50-fb20-42f1-adf5-3021aaf3f0e9\\})|(\\{a4295850-5057-46ab-bb31-2d283dff2474\\})|(\\{74517834-8cb5-4895-9f8b-3de15b771d92\\})|(\\{3dbebe53-9687-4e36-8c1e-79fcb098cecf\\})|(\\{275fc9ab-64fd-4430-8a35-43f73a87e8b9\\})|(\\{a26cdedc-b1eb-4fd2-b331-e71033c489a0\\})|(\\{d8fa69e8-1008-4f32-9db2-13ea7589bcec\\})|(\\{59704739-f6df-4272-968d-32f7c599da09\\})|(\\{208f846e-851a-4c07-a448-a66c40a2294c\\})|(\\{46c7b5ea-c1d0-4c2b-9122-3baa2e3bda3e\\})|(\\{39220be1-e69c-4b22-a5ff-545fcacf215f\\})|(\\{c0975246-6858-46e2-8f09-7d80d810c040\\})|(\\{8f284821-a420-4d79-bb7f-c1aae7a2fa90\\})|(\\{86d3e654-73c5-4b7f-a942-bd2347d4517a\\})|(\\{3d1af64b-542e-47ee-98a3-1f89bfca0f2a\\})|(\\{f0c2850d-101b-4de1-be16-3f09963048ab\\})|(\\{7388541e-8d9e-48a9-ac43-87dfdced6e87\\})|(\\{5d37398b-bea7-4ca7-bc4f-95de295be960\\})|(\\{3d8b3d51-3621-4aa5-b229-731cee83ee64\\})|(\\{ba67c9cf-ef60-4085-b6cf-729e5245089a\\})|(\\{1efe8d5d-ca8d-4a53-b2d4-a41380067041\\})|(\\{7698ee9d-345b-4395-b9e7-0479ed91f98d\\})|(\\{5f233e13-1892-41b6-81c6-a26c702d4a09\\})|(\\{d569420c-50a6-4082-b6d9-41c7bcb33464\\})|(\\{e406fddd-5ba4-4fdc-aa46-d556f97c8ef9\\})|(\\{29066f7e-a4ed-40b1-a02d-38ddf25d9533\\})|(\\{57e3e757-ca29-440c-9ef8-864da0e7ee72\\})|(\\{1237079a-7a08-4660-8fdb-6c3fdcecc787\\})|(\\{60429834-7a98-45e7-b525-6f31d55bbb3c\\})|(\\{bb850649-4ada-4735-a861-072ce9b647dd\\})|(\\{dfb103ab-cfee-49cd-b33a-e134367408c5\\})|(\\{24a0a50a-15d2-4806-9226-78491b3e986c\\})|(\\{2eb4b76a-f057-4d14-8d63-a5afa3571158\\})|(\\{cb5ad67a-5304-4351-bb15-530b159fa1f7\\})|(\\{0bd8bfda-24f5-4652-a434-664621e04a36\\})|(\\{f249d964-ac83-4059-893a-c3c5b38cc746\\})|(\\{94ba7f66-aef6-4590-8044-03aa705a4a2b\\})|(\\{dd178abb-e6fb-4e0a-b242-a64a53b24fb5\\})|(\\{7ff1da48-ae61-45f1-bcbf-2c22b4571d58\\})|(\\{d01909f7-598b-41b0-8907-2a9815c5c457\\})|(\\{93a37679-eb92-4eec-93b2-7bf77b0f94ca\\})|(\\{3ff7ce60-fbe5-4a50-9733-a347a02f09e5\\})|(\\{f076d6ff-1daf-42f3-b485-5b54b13aaf3e\\})|(\\{571d827c-5c84-42cc-8386-9e12abb8209a\\})|(\\{f6515794-8d44-402c-9a3d-3d712cb437bb\\})|(\\{796a59ab-77a7-4add-9481-5c7deee7c037\\})|(\\{a9b99a24-6cc1-491a-a81b-946cb42bc9f2\\})|(\\{9767896a-9561-426e-90d1-03b884d34eda\\})|(\\{95a6ebd9-52bc-4859-b92f-70a7c103e2b8\\})|(\\{4d24059d-463b-40b0-a86a-0a1de38fccb9\\})|(\\{d7c03c15-7287-4fb0-add0-c49744b48410\\})|(\\{6074519d-2d7c-45b6-b239-ea5452e93140\\})|(\\{425dafa9-0997-48df-b971-623847853747\\})|(\\{847688e9-b1d7-4607-b4fd-44c2365c01ba\\})|(\\{1383930d-64e9-4c3d-a629-361c70e3cd26\\}))$/", + "prefs": [], + "schema": 1576756857135, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1558136", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various fake Flash/Avast/etc clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "72593718-af38-4f63-bcb9-7c6afd13de8f", + "last_modified": 1576771656996 + }, + { + "guid": "/^((\\{9aa1f441-7c04-4b00-83b2-6a4362090b41\\})|(\\{3001d016-bc15-49af-a81b-2c8764139321\\})|(\\{0af1d242-b004-49ae-91fc-00fa0f642bf9\\})|(\\{95a5a094-ba8e-4fe1-80bb-6f6c0a01bc2c\\})|(\\{36b33158-36fc-4728-bf08-8e532100af58\\})|(\\{5bda1d03-6533-4d8c-adb4-7179402ddeec\\})|(\\{dc7d18c8-c832-42cd-b9b0-f6a46a737ad1\\})|(\\{56d85baf-c366-491b-b93c-733a4a36009d\\})|(\\{1b08c0c7-d2fd-4905-82ab-d4d759af4051\\})|(\\{1b8a9b33-b3d9-42ea-adce-cec910c44f29\\})|(\\{0be707e8-d7d6-45d5-8212-3fd3784d7de7\\})|(\\{fb2e3c38-be42-480f-b60c-d614d372e218\\})|(\\{c088d705-9f59-40ac-98eb-192f67c44f03\\})|(\\{9570bbbd-d761-4380-850c-d9cc15200916\\})|(\\{2652d5a4-26b1-4e6f-9134-304d7b57af7a\\})|(\\{1e0a2a5f-170c-43c1-b458-c8fc8bdd7dec\\})|(\\{47595710-e0b3-4a88-9bf7-54e1f3bb6772\\})|(\\{c25a320e-dc50-486a-9589-13ef22f75a21\\})|(\\{645237c8-7da2-4298-a789-e11fbaaa580e\\})|(\\{f2356af6-9b9d-4c69-876a-710d446a9124\\})|(\\{98a67ccd-599a-4675-9578-35af1824fdd2\\})|(\\{f3924f49-64a3-4fde-8598-76eec8e67f34\\})|(\\{2a46402d-b6c4-4a0b-87f0-dc90bb24fa93\\})|(\\{40d92297-295d-4a44-8a0f-dd69510c9c30\\})|(\\{a5462b0d-6528-47d5-ada5-4a23d1e0355e\\})|(\\{280f7325-eaf0-451a-ad2d-3b2c4e80e070\\})|(\\{ee5a7045-e216-4836-949a-07f5aa1dabc3\\})|(\\{34ee38f4-e2ba-4e9d-8b1f-dd06e8bd205f\\})|(\\{7c71c234-ba74-467c-b750-727ee7e38382\\})|(\\{a83b3b31-4bfc-4343-beab-761f21b97f57\\})|(\\{b85d35a4-3a03-430d-a1a0-437448a86c22\\})|(\\{8b6c1e29-5009-477e-a798-244b0efb1515\\})|(\\{64891348-7fc0-4299-bd6a-6bfaa6cf21a6\\})|(\\{baa828ff-1723-483b-8034-145ad2795efd\\})|(\\{81fd4851-7ea3-4ea5-8775-49372fe1c8c8\\})|(\\{ce7d8e95-c7d3-49dc-8abf-e860ee707b09\\})|(\\{d84109e8-9945-48bc-90e8-0dd0b1b63b73\\})|(\\{ec12fd66-7294-4167-8fbc-4774150c0fa4\\})|(\\{0c506de9-8467-4a92-8cd3-11c87e121db2\\})|(\\{b1671fe5-c90a-4f68-b8bf-e54a147b5d05\\})|(\\{8b25277a-4df8-4d2d-b3b4-f8219e2ce7d6\\})|(\\{5705cd04-46c2-459f-8a9a-97ce57eee1ae\\})|(\\{8340b371-ed61-4b07-b293-853aa5dbb866\\})|(\\{87bbb065-e195-48ac-989e-ba48ee63b404\\})|(\\{c455fc20-6e9d-418c-9b45-75fd85852b32\\})|(\\{a80c87a7-3366-4192-b9cc-d1e862e1c13d\\})|(\\{5f4ba05c-c1a5-4bc7-b8d3-c14e807b2c64\\})|(\\{0114315a-beda-4d55-89a3-e00f6346e7be\\})|(\\{0fb7b987-721e-4828-9a0e-a72860ded1b2\\})|(\\{c44cd2a5-bf28-4520-ab2d-187752e51a26\\})|(\\{3a34c1a6-3cbf-49a0-bfe9-beff60da5ec6\\})|(\\{677e89a4-ae10-42f3-8e9d-d51be40daf8f\\})|(\\{85d70eae-fde6-4ac0-ab82-0148f2eb1543\\})|(\\{fddac013-6d12-41a4-9924-f5ea7618f22d\\})|(\\{84b5ca75-a431-45c7-995d-6d7268decd0d\\})|(\\{7374dd37-a901-4b65-993c-3323f87e0f3c\\})|(\\{157a5c60-7899-4328-a90c-83d34d0844d4\\})|(\\{5b288e8d-f33c-4602-a945-07f96e43a041\\})|(\\{80f059bd-602a-42d5-9b17-9f2c6a074102\\})|(\\{8e5a8075-8e21-4ab2-b189-5d435208122c\\})|(\\{d5b94c09-0ff8-4b86-b52a-590d5e5ad9af\\})|(\\{a569eb31-9456-49a9-9aa9-e69a8db159d3\\})|(\\{0431a147-f9f5-4ee3-8dca-57303110c226\\})|(\\{88c77421-5ebd-46fb-92a8-e0459b6edd20\\})|(\\{a4a0697d-9a01-4b21-bb88-5ac949cdb7b3\\})|(\\{03e0da0a-da1d-46f2-85c7-08258189fc04\\})|(\\{351f5a4d-a0fd-4ce7-a85a-6cb74fb6c57d\\})|(\\{0e0b22ea-831e-4104-9c2e-612d7ebf82e2\\})|(\\{7b5c604f-ea41-4bfa-88d1-843f27645199\\})|(\\{863f023c-a2e5-4043-8e49-8e3029004b19\\})|(\\{20e4b367-f8ad-4c9a-b590-976be87206aa\\})|(\\{436ca3ce-f10a-4cdc-86c2-a46f086b5fdb\\})|(\\{a53983bc-545e-429a-8aaa-6332e1a6287f\\})|(\\{5fb1995e-ac7e-4c01-a592-8b262856e039\\})|(\\{56086da0-b20d-4118-9670-56e85624c875\\})|(\\{67ef5673-5769-4d5b-902f-ba22cb16a51b\\})|(\\{1d50e81b-0594-4c24-80c2-abc479be4d18\\})|(\\{b190f2cf-f3f0-40e7-a05b-8eed296a0c8d\\})|(\\{490d8bfc-fd42-4ded-9fee-1f009e777468\\})|(\\{1d647c69-8e4b-4fe8-b0f3-d914a1410ee8\\})|(\\{4958ae27-1251-49b4-a9d4-2af7c36c833b\\})|(\\{6e80fdc3-79c7-423a-8e95-0e123c0f2187\\})|(\\{d8d3230d-5642-4c3f-a64e-5ff690f7a466\\})|(\\{ddbe5d7a-e037-4749-89bc-3460d358aca1\\})|(\\{48cd023c-52ef-4fe7-a2ff-101c22c49a5e\\})|(\\{fef34931-b7ca-49a5-8827-bec353efaa08\\})|(\\{331a936b-a87e-4271-b9a6-30935dad77ed\\})|(\\{21084437-803c-49c3-8f84-9a615bfa5dfe\\})|(\\{93511e9d-badf-4b5c-9fbc-17ef6a9786ae\\})|(\\{470e388e-a039-43f8-a7a7-d8f54d273feb\\})|(\\{e4cd5604-caec-4139-9781-94add5f41ba3\\})|(\\{f00bc2b9-2b69-4f68-8bef-91d498686731\\})|(\\{7b3206c4-dc98-4ad9-88ba-5a7b3048fd92\\})|(\\{8e19f5f7-2445-48b1-8910-55d42d9dfe34\\})|(\\{770e30c6-f1b9-4df4-8551-c7f09ddbd8ed\\})|(\\{bb9a8679-1f82-4ab7-8ea2-bac864a38542\\})|(\\{13f5e078-b80a-45fb-a907-c4515a7bb0fa\\})|(\\{d8df47e3-5e86-4669-9a15-a37f8a8593db\\}))$/", + "prefs": [], + "schema": 1576756624851, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1565184", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Private Search" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9f945be6-12e7-4948-af77-668bc3996ff3", + "last_modified": 1576771656992 + }, + { + "guid": "/^((_0dMembers_@www\\.myaudiotab\\.com)|(_12Members_@free\\.myscrapnook\\.com)|(_14Members_@download\\.totalrecipesearch\\.com)|(_1cMembers_@www\\.bringmesports\\.com)|(_29Members_@www\\.headlinealley\\.com)|(_2bMembers_@www\\.bettercareersearch\\.com)|(_2jMembers_@free\\.recipehub\\.com)|(_2jMembers_@www\\.recipekart\\.com)|(_2vMembers_@www\\.dailybibleguide\\.com)|(_39Members_2@www\\.mapsgalaxy\\.com)|(_49Members_@www\\.utilitychest\\.com)|(_4jMembers_@www\\.radiorage\\.com)|(_4lMembers_@www\\.bibletriviatime\\.com)|(_4zMembers_@www\\.videodownloadconverter\\.com)|(_57Members_@free\\.marineaquariumfree\\.com)|(_5aMembers_@download\\.mywebface\\.com)|(_5eMembers_@www\\.translationbuddy\\.com)|(_5mMembers_@download\\.myfuncards\\.com)|(_5pMembers_@www\\.metrowhiz\\.com)|(_64Members_@www\\.televisionfanatic\\.com)|(_69Members_@www\\.packagetracer\\.com)|(_6xMembers_@www\\.readingfanatic\\.com)|(_73Members_@www\\.easyhomedecorating\\.com)|(_7eMembers_@www\\.homeworksimplified\\.com)|(_7jMembers_@download\\.gardeningenthusiast\\.com)|(_7nMembers_@download\\.crazyforcrafts\\.com)|(_8eMembers_@download\\.howtosimplified\\.com)|(_8hMembers_@download\\.allin1convert\\.com)|(_8iMembers_@download\\.audiotoaudio\\.com)|(_8jMembers_@download\\.myimageconverter\\.com)|(_8lMembers_@free\\.filesharefanatic\\.com)|(_94Members_@www\\.motitags\\.com)|(_9eMembers_@free\\.findmefreebies\\.com)|(_9pMembers_@free\\.onlinemapfinder\\.com)|(_9tMembers_@download\\.internetspeedtracker\\.com)|(_9tMembers_@free\\.internetspeedtracker\\.com)|(_apMembers_@free\\.puzzlegamesdaily\\.com)|(_b7Members_@free\\.mytransitguide\\.com)|(_beMembers_@free\\.dailylocalguide\\.com)|(_brMembers_@free\\.yourtemplatefinder\\.com)|(_ceMembers_@free\\.easypdfcombine\\.com)|(_chMembers_@free\\.discoverancestry\\.com)|(_d0Members_@free\\.gostudyhq\\.com)|(_d1Members_@free\\.mysocialshortcut\\.com)|(_d9Members_@www\\.everydaylookup\\.com)|(_dbMembers_@free\\.getformsonline\\.com)|(_dgMembers_@free\\.trackapackage\\.net)|(_dhMembers_@www\\.packagetracking\\.net)|(_diMembers_@www\\.easymaillogin\\.com)|(_djMembers_@www\\.emailfanatic\\.com)|(_dnMembers_@www\\.webmailworld\\.com)|(_doMembers_@free\\.convertanyfile\\.com)|(_dpMembers_@free\\.findyourmaps\\.com)|(_dqMembers_@www\\.downspeedtest\\.com)|(_drMembers_@free\\.downloadinboxnow\\.com)|(_dsMembers_@free\\.internetspeedutility\\.net)|(_duMembers_@free\\.funpopulargames\\.com)|(_dvMembers_@www\\.testinetspeed\\.com)|(_dxMembers_@www\\.download-freemaps\\.com)|(_dzMembers_@www\\.pconverter\\.com)|(_e1Members_@free\\.actionclassicgames\\.com)|(_e5Members_@www\\.productivityboss\\.com)|(_e6Members_@www\\.freelocalweather\\.com)|(_e7Members_@free\\.gamingassassin\\.com)|(_eaMembers_@www\\.mynewsguide\\.com)|(_ebMembers_@download\\.metrohotspot\\.com)|(_ecMembers_@www\\.instantradioplay\\.com)|(_edMembers_@free\\.myradioaccess\\.com)|(_eeMembers_@download\\.freeradiocast\\.com)|(_efMembers_@free\\.funcustomcreations\\.com)|(_ehMembers_@free\\.dailyrecipeguide\\.com)|(_eiMembers_@www\\.100sofrecipes\\.com)|(_ejMembers_@free\\.downloadrecipesearch\\.com)|(_ekMembers_@free\\.biggamecountdown\\.com)|(_eoMembers_@www\\.transitsimplified\\.com)|(_erMembers_@free\\.getvideoconvert\\.com)|(_esMembers_@free\\.downloadmanagernow\\.com)|(_euMembers_@free\\.filesendsuite\\.com)|(_ewMembers_@free\\.mergedocsonline\\.com)|(_exMembers_@free\\.easydocmerge\\.com)|(_f7Members_@download\\.smsfrombrowser\\.com)|(_fdMembers_@wallpapers\\.myway\\.com)|(_fkMembers_@free\\.getflightinfo\\.com)|(_foMembers_@free\\.flightsearchapp\\.com)|(_fpMembers_@free\\.passwordlogic\\.com)|(_frMembers_@free\\.testforspeed\\.com)|(_fsMembers_@free\\.pdfconverterhq\\.com)|(_ftMembers_@free\\.mytelevisionhq\\.com)|(_fvMembers_@free\\.directionsace\\.com)|(_fwMembers_@free\\.howtosuite\\.com)|(_fxMembers_@free\\.mytransitplanner\\.com)|(_g3Members_@free\\.easyphotoedit\\.com)|(_gcMembers_@www\\.weatherblink\\.com)|(_gpMembers_@free\\.mymapswizard\\.com)|(_gtMembers_@free\\.gamingwonderland\\.com)|(_h2Members_@free\\.calendarspark\\.com)|(_hbMembers_@free\\.quickphotoedit\\.com)|(_hfMembers_@free\\.everydaymanuals\\.com)|(_hgMembers_@free\\.atozmanuals\\.com)|(_hmMembers_@free\\.easyweatheralert\\.com)|(_hnMembers_@free\\.quickweatheralert\\.com)|(_hoMembers_@free\\.directionsbuilder\\.com)|(_hpMembers_@free\\.easyfileconvert\\.com)|(_hqMembers_@free\\.scenichomepage\\.com)|(_hvMembers_@free\\.myfileconvert\\.com))$/", + "prefs": [], + "schema": 1576757193463, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598806", + "why": "These add-ons collect ancillary user data among other policy violations.", + "name": "New tab data collection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1b799430-d1a3-49a8-91a2-f45069528ebe", + "last_modified": 1576771656988 + }, + { + "guid": "/^((\\{02cfe44d-20d1-4275-b325-42888248443d\\})|(\\{03476021-f140-425d-8e62-a6455206a874\\})|(\\{0a50fe22-313f-4a59-9032-2fd63bdd2815\\})|(\\{21c91802-e348-4dd0-820a-07aca1de12a5\\})|(\\{24f43a02-83c5-44cd-a611-0bae2093393f\\})|(\\{257284b0-be1d-44e4-8321-b2a7a56c2eb5\\})|(\\{2610c188-123a-4e4f-a6c7-2ed7e8b70bf6\\})|(\\{26a28ba8-d8e3-4284-ab58-8542144ff657\\})|(\\{27e8838f-91ab-486b-8851-9de8bd633aac\\})|(\\{27f348a5-adb0-4037-bc03-f6832cfd6fc3\\})|(\\{28f5d824-8067-4859-8fe3-22a5f96552bc\\})|(\\{32f66767-230f-4dd8-8538-d6dca8b327f6\\})|(\\{35a35db0-010b-4105-9dc3-d5fe761023ab\\})|(\\{3d61019d-c636-4c0d-91d2-6e2d734014e0\\})|(\\{44c9b9cf-b960-45fc-bf0a-9be9c455c975\\})|(\\{48e711a3-644f-4520-8205-11b377df480e\\})|(\\{50ba963f-1b4a-4ed0-b22f-8e1c18d17364\\})|(\\{53150391-1080-4a50-96ed-de24a6ea24c6\\})|(\\{5570199f-fc69-45eb-a0b0-7cc60089022c\\})|(\\{595c83ad-5a64-4740-a118-39a49aede0e3\\})|(\\{5ae5110f-449e-4c5e-b247-9d1e9971d3bc\\})|(\\{61b734ef-f886-4466-8525-8276e9b19b55\\})|(\\{64e9998b-c477-4c29-bf1d-945146cf9376\\})|(\\{682ff4d6-9b2b-4e1b-99b0-e2dcc2476bd1\\})|(\\{6836a626-1ec1-4c3c-9b25-5e4cd5048ad5\\})|(\\{69f1dda4-a6fa-49f6-a16c-7b3bc3e0e520\\})|(\\{6b7eb4f0-ef7d-4da4-8c3d-21b7c2dfb19f\\})|(\\{725f919c-8adf-4192-9c0e-89ab1027161e\\})|(\\{7320730b-e202-4405-8d76-97a862239701\\})|(\\{79821b50-57ca-4267-9dbf-84aeb061e126\\})|(\\{7ee25d12-df90-4a6a-a068-7a6919651bc7\\})|(\\{816b4ade-7b69-4a73-994a-b37f9939d5d8\\})|(\\{82956d74-9e19-4770-97cc-73caa2c9f711\\})|(\\{839b2467-6857-49f9-a730-baf7b2a98039\\})|(\\{85af3203-f8a3-4ba5-8d0f-c3ee1d38c0d8\\})|(\\{8866f414-2f72-44a6-b7dc-6959891e9ee3\\})|(\\{914f8b0b-f4e0-46d5-bae6-4fadd78ebe8c\\})|(\\{93969099-a7b4-43aa-bcc0-241676204257\\})|(\\{972315f7-e09e-4f7d-b4d7-1ed093e06dc0\\})|(\\{98c7dc13-2e24-4ea0-8f02-34a2e19f19f1\\})|(\\{99ca0fea-aa27-42b9-9dfb-5c2b02601d30\\})|(\\{9cd8f1f0-b739-427a-9378-16dee329a2b6\\})|(\\{9f0a0293-94ca-4e27-b697-2032f455a12f\\})|(\\{a41f8d9a-ea42-4d59-8966-5f9e805246a5\\})|(\\{b21eea41-def0-4ab6-afe4-ec98397bf59c\\})|(\\{b7561855-e073-4a3a-b827-33bd97042442\\})|(\\{b8b11c35-1c11-4634-9b20-6aff2344a65d\\})|(\\{b90bc248-5b37-4de0-be18-0f208c5cf958\\})|(\\{be17a3b2-18a6-4925-aa7c-bb377bec7793\\})|(\\{be82443f-f8e0-46eb-8cda-80ff1d911a16\\})|(\\{ccce69c0-3cc6-4e63-acb6-77a7c63772ed\\})|(\\{daf9a25c-83b8-4e4b-aca2-ff7ca55289d3\\})|(\\{db7d9d1b-1758-44a0-a8fb-0eb2f3ac4d36\\})|(\\{de34ccf4-0634-4ba2-9bdc-42b293c8cdc8\\})|(\\{df2bbd0f-0a8a-45de-a75b-4349835a8576\\})|(\\{e203bc10-6462-49e6-8f2f-223a29ce82d7\\})|(\\{e66e26fe-5092-4fbd-9851-78a1f5c34575\\})|(\\{ebaf7169-6df9-463f-bf15-10f48143bf73\\})|(\\{ebe3c932-d6fe-4ad7-9eb9-7857aa6fd1ca\\})|(\\{f83c3eae-8dd8-4f38-b7c0-272e34599771\\})|(\\{fb00fead-b480-4906-8b4c-4039c272b83d\\})|(\\{fc14529e-1b64-4717-8a1f-6634cc343c7c\\})|(@blackfridaystores)|(@blackfridaystoresco)|(@blah)|(@celebjunky)|(@checkmaps)|(@childt)|(@convertmypdf)|(@convertmypdfco)|(@dailyjobssearch)|(@dogs)|(@email\\.superextension\\.info)|(@flirtywallpapers)|(@justlovepetscom)|(@liveemail)|(@localweathertoday)|(@localweathertodayco)|(@loginpro)|(@mapsfinder)|(@mynetspeed)|(@mynetspeed\\.co)|(@mynetspeedco)|(@qwertyuiop)|(@qwertyuiopasdf)|(@searchprivacy)|(@searchprivacy-fx)|(@searchprivacy-fx-1\\.1\\.15)|(@searchprivacy-fx-1\\.1\\.16)|(@searchprivacy-fx-1\\.1\\.17)|(@searchprivacy-fx-1\\.1\\.18)|(@searchprivacy-fx-1\\.1\\.19)|(@searchprivacy-fx-1\\.1\\.20)|(@sportsbulletin)|(@sportsupdates)|(@thecouponstore)|(@trailertab)|(@wheresmypackage)|(@wheresmypackageco)|(@www\\.blackfridaystores\\.co)|(@www\\.convertmypdf\\.online)|(@www\\.convertthepdf\\.com)|(@www\\.dailyjobssearch\\.net)|(@www\\.easyonlinerecipe\\.info)|(@www\\.EasyOnlineRecipe\\.net)|(@www\\.easyrecipesearch\\.net)|(@www\\.findmaps\\.co)|(@www\\.job-portal\\.co)|(@www\\.justlovepets\\.online)|(@www\\.movie-quest\\.co)|(@www\\.mymapbuddy\\.net)|(@www\\.mynetspeed\\.co)|(@www\\.searchsafe\\.online)|(@www\\.searchsafe\\.site)|(@www\\.searchsafe\\.space)|(@www\\.searchsafe\\.website)|(@www\\.thecouponstore\\.co)|(@www\\.thecouponstore\\.co\\.test2)|(abc_KtOQLgxUgW@abc)|(Access_Mails_Now_YEsNTbvoal@www\\.accessmailsnow\\.com)|(Access_My_Inbox_zPsghWWAPc@www\\.accessmyinbox\\.com)|(Accurate_Weather_Today_clone_vGXzNgIjwr@www\\.accurateweathertoday\\.com)|(Accurate_Weather_Today_clone_zpRLPSpMcU@www\\.accurateweathertoday\\.com)|(alot_Search_unSoxsuUZQ@www\\.searchalot\\.co))$/", + "prefs": [], + "schema": 1576757781685, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598770", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary data and/or by overriding search behavior without user consent or control.", + "name": "Search hijacking and new-tab add-ons collecting data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a0be3662-6b8f-4d91-bbe1-fde9500adb48", + "last_modified": 1576771656985 + }, + { + "guid": "/^((\\{a4b95555-755a-4a3f-bc64-f6999377963d\\})|(djamol@webdownloader)|(\\{b23e59ff-5491-40f4-adb2-577d31d8778a\\})|(\\{90e41842-755d-40e0-9136-8129df55a644\\})|(\\{8e0ccf87-6ea4-4985-b07f-4d503f105d91\\})|(\\{69d0ae35-7d39-4c41-b455-4d751117317e\\})|(\\{df0dfe82-78a9-456e-9dc1-c832a94d43a7\\})|(\\{7bbd26d9-d245-48d3-b3a6-5e19948dfa14\\})|(\\{3d9109d8-463e-4dde-9416-d25c6ea995df\\})|(\\{2726adf8-c1f9-4ed9-baf1-9a3c1742b659\\})|(\\{bfc40cf5-a085-4f0b-a3ef-5e124f2e445e\\})|(\\{37927c18-d5a5-4187-9e6e-6a787364bd9f\\}))$/", + "prefs": [], + "schema": 1576093292597, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1603731", + "why": "These add-ons violate Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b8daa919-7a9f-4a83-8aef-4a5d3fbfb502", + "last_modified": 1576242123974 + }, + { + "guid": "{d69204d4-610c-4144-9d26-8cf8b2ad6e15}", + "prefs": [], + "schema": 1576032043088, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1603086", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "IDM Integration Module" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c3774104-d770-48a5-acd7-c7136d62bb40", + "last_modified": 1576068160484 + }, + { + "guid": "{d69204d4-610c-4144-9d26-8cf8b2ad6e14}", + "prefs": [], + "schema": 1576012075125, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1603001", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "IDM Integration Module" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8c92ab3a-9c31-4f75-b980-19e39b702fd8", + "last_modified": 1576032042722 + }, + { + "guid": "/^((\\{108dfe62-0473-47cd-b33e-8270d062db77\\})|(\\{e4a4e54a-5d49-4ac3-93d7-ec0d30ad06ef\\})|(\\{ccad95df-add6-4d8a-aa5c-cdc484075bad\\}))$/", + "prefs": [], + "schema": 1575996881649, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602162", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Player" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fd42cd0b-a72a-4a0f-9a6a-965ee2f29cb6", + "last_modified": 1576012074754 + }, + { + "guid": "{7bb202fa-9247-49c6-898c-ce0d36bc44e3}", + "prefs": [], + "schema": 1575976576210, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602821", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "MyPopupBlocker" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "810f4d41-368a-4cfc-8513-81063587b97c", + "last_modified": 1575996881281 + }, + { + "guid": "@cloaking-skull", + "prefs": [], + "schema": 1575920492132, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602736", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Cloaking Skull" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4438f49a-2f78-48d9-bc7e-a897f3f62945", + "last_modified": 1575976575848 + }, + { + "guid": "/^((photoplus@4329fgjfdgkf343\\.com)|(\\{1b1c9b32-9dd3-461c-85c6-bbac4ef2af10\\})|(\\{7DC6D86C-EA66-48C3-9BA7-AE26A8ECB175\\})|(david\\.janitzek@gmail\\.com)|(\\{ac393d9e-1610-4eaa-8951-77655f98b4f7\\}))$/", + "prefs": [], + "schema": 1575890870070, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602406", + "why": "This add-on violates Mozilla's ad-don policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "aa154bb5-7fc9-4226-928e-199aa7eded69", + "last_modified": 1575892910839 + }, + { + "guid": "/^((\\{e1d407bc-1986-4577-bc31-a63d07cbae10\\})|(\\{3a2831f6-37df-4a42-baf6-0b81d5b6a68f\\})|(s3google@translator))$/", + "prefs": [], + "schema": 1575889775993, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602293", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "S3 Translator" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "eac7bf16-aae5-4958-8b85-5dcc9a3a1ce8", + "last_modified": 1575890869693 + }, + { + "guid": "/^((\\{87dd9a06-b692-4fa0-91c4-0aead43c25cc\\})|(\\{9082269e-1de7-4d34-9736-1174917f9459\\}))$/", + "prefs": [], + "schema": 1575747691126, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602285", + "why": "These add-ons violate Mozilla's add-on policy by executing remote code and/or collecting ancillary data.", + "name": "HomeTab/FLSearch" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1af528d2-d0e1-4981-acfa-3b10501aed34", + "last_modified": 1575889775630 + }, + { + "guid": "/^((xh5yh45p59fdrtimlot6@xh5yh45p59fdrtimlot6\\.com)|(758ct3wkmbrvd8equs21@758ct3wkmbrvd8equs21\\.com))$/", + "prefs": [], + "schema": 1575661660854, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602098", + "why": "This add-on violates Mozilla's add-on policy by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "235d9439-305c-48db-a6da-5a8639ab9c80", + "last_modified": 1575663903042 + }, + { + "guid": "{ecc522ab-9926-4dc5-90ab-8f979db871ed}", + "prefs": [], + "schema": 1575651664337, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1602087", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "YTMP4DL" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "01645c30-87fc-4332-9c94-255ef9e50120", + "last_modified": 1575661660491 + }, + { + "guid": "/^((\\{81f244fc-e5ab-4233-bad0-f61f334dcd43\\})|(\\{98222415-4241-4b89-b81d-651c1a872881\\}))$/", + "prefs": [], + "schema": 1575628797035, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1601546", + "why": "This add-on violates Mozilla's add-on policies by facilitating access to malicious web content and software.", + "name": "Video download add-ons (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "404e3032-80a6-43f5-a02e-c7d4e8f684d5", + "last_modified": 1575632376941 + }, + { + "guid": "{c5b1783f-29c8-4bb0-ab8b-a6a9a0c5e4e0}", + "prefs": [], + "schema": 1575627603102, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1601747", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "IDM Download faster music (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ef1c681a-ecca-4ea0-b798-6a093765b984", + "last_modified": 1575628796682 + }, + { + "guid": "/^((smartnav@navigation-internet\\.com)|(game@ultimate-cosmo\\.com)|(update@ultimate-cosmo\\.com)|(extension@gum-gum-streaming\\.com)|(extension@one-piece-fighting\\.com)|(extension@manga-vf\\.fr)|(adblock@gum-gum-streaming\\.com)|(adblock@manga-vf\\.fr))$/", + "prefs": [], + "schema": 1575313586028, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1600592", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0b0c648b-1f8d-49f4-bd6e-96e69bbebb45", + "last_modified": 1575376981463 + }, + { + "guid": "/^((\\{33a89146-6b66-4834-bea3-18733d7646f8\\})|(\\{55f304c0-a431-4b35-8cd0-472569a9dd86\\})|(\\{581fae86-7847-448d-9950-5a96127b293a\\})|(\\{9f2a514d-e9dd-4bd6-b743-b15492c10167\\})|(\\{28bf22b4-d52d-4d8a-b710-3868b3bc46ce\\})|(\\{3e9246fc-8125-499b-984e-a8b68507bd36\\})|(\\{731784b0-81c4-4fe9-8741-f5960e12088c\\})|(\\{21585a0f-bcb6-480c-9f70-5cd33cf64833\\})|(\\{2741fd42-3faa-4cef-9b08-361f0c4990ea\\})|(\\{4c479c80-2f53-4d38-a366-0f0c3a346d40\\})|(\\{c4471705-e0eb-47ed-8af9-046c1e808fdd\\})|(\\{2b2bc8ab-5a38-4a26-8ce1-2af9fa2ad7bf\\})|(\\{6840b587-8324-44a0-b176-56d15580dde4\\})|(\\{44d8d882-f6a0-4349-93bc-d3a2353a3fd3\\})|(\\{ddf7716b-badc-417c-b3ce-6322fd67cc65\\})|(\\{82abdf82-f361-47ab-bab7-7f862e132e06\\})|(\\{d24e0155-d36c-402e-943f-d5816451e86f\\})|(\\{926f1f58-3c6e-4f07-8847-2998dc0f1dbd\\})|(\\{e8d228cc-ecb9-4ef6-98e3-8052858fd3ea\\})|(\\{86a868c9-707f-46e8-88ca-5edde6a549e2\\})|(\\{7d5a68c2-261f-449f-af8d-b99a7ae7a01f\\})|(\\{e579c6fb-bf80-4ae5-b3d2-613e084af7fa\\})|(\\{ef7a3832-deb4-4798-b61b-a7cc8b8aa1e2\\})|(\\{48532584-e4dc-4902-b27d-ce35cb29c4c7\\})|(\\{b792fe29-4eb1-412b-bd0b-458252dfab39\\})|(\\{c6f8a307-8024-4e6f-a5f1-595ba43d765e\\})|(\\{7c7a26bd-b1cb-4088-a16e-48f26d22d569\\})|(\\{ff3b2825-86da-4525-abe6-c673b70f58cc\\})|(\\{d4def0be-0d09-453a-9cca-ddc1e1cc9a1d\\})|(\\{0024b95a-9610-4e9d-b836-da40c99c959e\\})|(\\{b09a0797-b6d2-485e-8afc-8361055761ea\\})|(\\{f5c86ae7-cf67-43f8-895c-b2b19209ee81\\})|(\\{bc5d5c2b-e895-49d5-966b-cd1aaee9d324\\})|(\\{a6a8b5c9-45da-457e-990c-b89515139e80\\})|(\\{6d00e6c8-2f26-4dad-81e6-888c080ddfc7\\})|(\\{8f4412a8-c670-4298-a0bc-da30dc3a5ae2\\})|(\\{912d27f7-d9ac-489b-8d7c-2873526e11fc\\})|(\\{3d47bef5-277d-4f06-8ad8-802266e57d17\\})|(\\{0e0bbc6c-914e-47e2-96b9-33f94af6f0a7\\})|(\\{e0f289e7-1284-496b-aa09-f052089d61a3\\})|(\\{64832a69-731b-4481-ad59-3c8aa4006643\\})|(\\{c174dc42-ad4e-4378-8f07-8a47025f8407\\})|(\\{28753278-896f-4f48-8ef3-ea3379896a40\\})|(\\{cdb22d53-d13d-4500-af92-7e653e988722\\})|(\\{65d037b6-c44d-49c5-aa10-a947940c61cb\\})|(\\{d4906685-5b71-49a8-92b5-76f19f2b0a66\\})|(\\{2bebc776-9ee0-47c8-9c99-0351b140ffd2\\})|(\\{58d56295-8d0f-4fe1-8538-2d916911e304\\})|(\\{19283d7a-9fb5-417e-9401-b952ce14a268\\})|(\\{7338a117-ff10-4862-8992-b42829b2e47b\\})|(\\{8defb767-abf1-4d39-88df-9cfc6a39a8e3\\})|(\\{15a77f39-2b56-46b5-b5d6-6064a6f1fbdd\\})|(\\{87fd9298-3176-400b-be06-45538ecd0702\\})|(\\{cda1cc52-9961-477d-8faa-95c7f9025498\\})|(\\{c8056cfe-0bd7-43db-88ae-468f78cc8637\\})|(\\{54bf103a-3f38-43aa-b1ed-2ccbcadb4e0b\\})|(\\{bb67004c-57d6-4632-bc89-8991560a011f\\})|(\\{2902f9ee-b79a-415f-99bf-b661c2bf42e0\\})|(\\{81e0bd24-a93d-4db0-8db9-d5560805c153\\})|(\\{3004ff5d-c68c-4e2b-80b4-2dc21bde9195\\})|(\\{25bcebd4-1d72-4502-8927-a1a6ac35d595\\})|(\\{0b440d2f-8a97-4d2e-bacb-703260897903\\})|(\\{7d0cc577-51fb-4994-8926-7df70f5f16fc\\})|(\\{a6c138fd-c83d-416b-90a8-74bfafefb67a\\}))$/", + "prefs": [], + "schema": 1575229292065, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1600576", + "why": "This add-on violates Mozilla's add-on policy by collecting data without user disclosure or consent.", + "name": "Add-ons collecting data without user disclosure or consent" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "80a34f3f-1f4e-47cb-99a4-1b343b322455", + "last_modified": 1575285295616 + }, + { + "guid": "/^((_qdMembers_@extmys\\.mysearch\\.com)|(_qjMembers_@www\\.taxcenternow\\.com)|(_ohMembers_@www\\.whizds1\\.com)|(_opMembers_@tvhero\\.thewhizmarketing\\.com)|(_oqMembers_@sportsaddict\\.thewhizproducts\\.com)|(_ooMembers_@yourdailytrailer\\.yournewtab\\.com)|(_olMembers_@screendream\\.yournewtab\\.com)|(_orMembers_@sporthero\\.thewhizmarketing\\.com)|(_osMembers_@gogamego\\.thewhizproducts\\.com)|(_r3Members_@www\\.mysearch\\.com)|(_ojMembers_@www\\.whizds2\\.com)|(_flMemberstest_@free\\.myformsfinder\\.com)|(_dqMemberstest_@www\\.downspeedtest\\.com)|(_flMembersAlt_@free\\.myformsfinder\\.com)|(_dqMembersAlt_@www\\.downspeedtest\\.com)|(_rcMembers_@www\\.extsb\\.searchbetter\\.com)|(_piMembers_@install\\.mysporttab\\.com)|(_phMembers_@install\\.streamfrenzy\\.com)|(_pfMembers_@install\\.myvideotab\\.com)|(_qxMembers_@install\\.utili-site\\.com)|(_qyMembers_@install\\.cryptoconvertertab\\.com)|(_r4Members_@install\\.salah-time\\.com)|(_r9Members_@install\\.speed-exam\\.com)|(_rbMembers_@Install\\.movie-canal\\.com)|(_poMembers_@www\\.linkuryds1\\.com)|(_l9Members_@www\\.plaskodss3\\.com)|(_ppMembers_@www\\.firstofferzds1\\.com)|(_qkMembers_@www\\.exploreads\\.com)|(_kfMembers_@www\\.bluecpads\\.com)|(_k2Members_@www\\.arcadejetds\\.com)|(_qiMembers_@tvguru-lp\\.olympuswaymarketing\\.com)|(_p8Members_@www\\.easywatch\\.online)|(_kcMembers_@www\\.utilitytab\\.com)|(_kdMembers_@www\\.utilitytab\\.com)|(_qrMembers_@www\\.install\\.utilitytab\\.com)|(_qsMembers_@www\\.utilitytab\\.com)|(_izMembers_@www\\.geteasyarcade\\.com)|(_qgMembers_@www\\.getinstantpdf\\.com)|(_p7Members_@www\\.tigersdeal\\.com)|(_jgMembers_@www\\.greathippo\\.com)|(_jdMembers_@www\\.greathippo\\.com)|(_juMembers_@arcadejet\\.com)|(\\{90ac1d06-caf8-46b9-9325-59c82190b687\\}))$/", + "prefs": [], + "schema": 1575056491406, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1600273", + "why": "These add-ons collect ancillary user data among other policy violations.", + "name": "New Tab Data Collection Extensions" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "196e36b5-3225-4f8d-a1a0-6696ea8885f9", + "last_modified": 1575066166785 + }, + { + "guid": "{99454877-875a-473e-a0c7-03ab910a8461}", + "prefs": [], + "schema": 1575040644063, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1600327", + "why": "The add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Smart Screen Capture" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3f98e9e7-40d0-4af5-969c-9ea1aadbf280", + "last_modified": 1575041576477 + }, + { + "guid": "/^((Stark-vpn\\.5\\.16@firefox\\.com)|(Video_Downloader_Plus@gmail\\.com))$/", + "prefs": [], + "schema": 1575038746215, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1600322", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "89027d84-8871-4d44-8a14-18f029df268b", + "last_modified": 1575040643688 + }, + { + "guid": "/^((\\{b3d1c58f-b27e-4da8-bd4c-c3188346139f\\})|(\\{17101510-b5fb-4361-9e02-70a0e714b591\\})|(\\{d46a3635-4ca8-4e70-8bee-e49714896521\\}))$/", + "prefs": [], + "schema": 1574981048393, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1600315", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search hijacking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "36b86fac-a1f2-4df5-937a-d5667e0e824d", + "last_modified": 1575038745843 + }, + { + "guid": "/^((_orMembers_@www\\.apn\\.ask\\.com)|(_jsMembers_@www\\.apn\\.ask\\.com)|(_osMembers_@www\\.apn\\.ask\\.com)|(_otMembers_@www\\.apn\\.ask\\.com)|(_owMembers_@www\\.apn\\.ask\\.com)|(_ogMembers_@seen-on-screen\\.thewhizmarketing\\.com)|(_qeMembers_@app\\.mysearch\\.com)|(_otMembers_@muzikfury\\.thewhizmarketing\\.com)|(_oiMembers_@screenaddict\\.thewhizproducts\\.com)|(_omMembers_@screenwatch\\.yournewtab\\.com)|(_okMembers_@www\\.whizds3\\.com)|(_j0Members_@www\\.gettvstreamnow\\.com)|(_64Members_b@www\\.televisionfanatic\\.com)|(_ruMembers_@getscreenmania\\.com)|(_rxMembers_@www\\.gamelocket\\.com)|(_s1Members_@www\\.deadlockds\\.com)|(_s3Members_@www\\.mysearchappwhiz\\.com)|(_rvMembers_@www\\.groovymediads\\.com)|(_s8Members_@secure\\.norton\\.myway\\.com)|(_r3Members_@free\\.mysearch\\.com)|(_sgMembers_@www\\.easyhowtoguide\\.com)|(_shMembers_@www\\.fastformsfinder\\.com)|(_siMembers_@www\\.mydirectionsfinder\\.com)|(_seMembers_@www\\.brightcast\\.com)|(_spMembers_@www\\.onlinemapsnow\\.com)|(_slMembers_@www\\.creativeinternetds\\.com)|(_hpMembers0619_@free\\.easyfileconvert\\.com)|(_ceMembers0619_@free\\.easypdfcombine\\.com)|(_euMembers0619_@free\\.filesendsuite\\.com)|(_dpMembers0619_@free\\.findyourmaps\\.com)|(_dxMembers0619_@www\\.download-freemaps\\.com)|(_65Members0619_@download\\.fromdoctopdf\\.com)|(_dbMembers0619_@free\\.getformsonline\\.com)|(_39Members0619_@www\\.mapsgalaxy\\.com)|(_ewMembers0619_@free\\.mergedocsonline\\.com)|(_flMembers0619_@free\\.myformsfinder\\.com)|(_d1Members0619_@free\\.mysocialshortcut\\.com)|(_b7Members0619_@free\\.mytransitguide\\.com)|(_ozMembers0619_@free\\.newnotecenter\\.com)|(_jbMembers0619_@www\\.onlinemapsearch\\.com)|(_jjMembers0619_@www\\.onlineroutefinder\\.com)|(_69Members0619_@www\\.packagetracer\\.com)|(_fsMembers0619_@free\\.pdfconverterhq\\.com)|(_jnMembers0619_@www\\.pdfconverttools\\.com)|(_gcMembers0619_@www\\.weatherblink\\.com)|(_brMembers0619_@free\\.yourtemplatefinder\\.com)|(_s5Members_@www\\.bluecpads1\\.com)|(_j5Members0619_@ext\\.ask\\.com)|(_5zMembersff0619_@www\\.couponxplorer\\.com)|(_1gMembersff0619_@www\\.inboxace\\.com)|(_flMembersff1219_@free\\.myformsfinder\\.com)|(_j5Membersff1219_@ext\\.ask\\.com)|(_8eMembersff1219_@download\\.howtosimplified\\.com)|(_8jMembersff1219_@download\\.myimageconverter\\.com)|(_94Membersff1219_@www\\.motitags\\.com)|(_ceMembersff1219_@free\\.easypdfcombine\\.com)|(_d1Membersff1219_@free\\.mysocialshortcut\\.com)|(_dqMembersff1219_@www\\.downspeedtest\\.com)|(_dzMembersff1219_@www\\.pconverter\\.com)|(_dpMembersff1219_@free\\.findyourmaps\\.com)|(_e1Membersff1219_@free\\.actionclassicgames\\.com)|(_e5Membersff1219_@www\\.productivityboss\\.com)|(_ewMembersff1219_@free\\.mergedocsonline\\.com)|(_euMembersff1219_@free\\.filesendsuite\\.com)|(_dsMembersff1219_@free\\.internetspeedutility\\.net)|(_fsMembersff1219_@free\\.pdfconverterhq\\.com)|(_iwMembersff1219_@free\\.allinonedocs\\.com)|(_hoMembersff1219_@free\\.directionsbuilder\\.com)|(_hpMembersff1219_@free\\.easyfileconvert\\.com)|(_hxMembersff1219_@free\\.mergedocsnow\\.com)|(_hyMembersff1219_@free\\.formfetcherpro\\.com)|(_i2Membersff1219_@free\\.streamlineddiy\\.com)|(_i4Membersff1219_@free\\.fileconvertonline\\.com)|(_j6Membersff1219_@www\\.freemanualsindex\\.com)|(_jbMembersff1219_@www\\.onlinemapsearch\\.com)|(_jaMembersff1219_@www\\.testonlinespeed\\.com)|(_joMembersff1219_@www\\.onlineformfinder\\.com)|(_jnMembersff1219_@www\\.pdfconverttools\\.com)|(_k8Membersff1219_@www\\.mymapsexpress\\.com)|(_koMembersff1219_@www\\.quickpdfmerger\\.com)|(_kxMembersff1219_@www\\.smarteasymaps\\.com)|(_l6Membersff1219_@www\\.propdfconverter\\.com)|(_l4Membersff1219_@www\\.quicktemplatefinder\\.com)|(_l1Membersff1219_@www\\.videoconverterhd\\.com)|(_ozMembersff1219_@free\\.newnotecenter\\.com)|(_lbMembersff1219_@free\\.worldofnotes\\.com)|(_q8Membersff1219_@www\\.getformsfree\\.com)|(_psMembersff1219_@www\\.freetemplatefinder\\.com)|(_pvMembersff1219_@www\\.mapmywayfree\\.com)|(_gcMembersff1219_@www\\.weatherblink\\.com)|(_65Membersff1219_@download\\.fromdoctopdf\\.com)|(_sxMembers_@www\\.mapsmasteronline\\.com)|(_j6Members_@ext\\.ask\\.com)|(_66Members_@download\\.fromdoctopdf\\.com)|(_htMembers_@free\\.gamingwonderland\\.com)|(_5zMembers_@www\\.videodownloadconverter\\.com))$/", + "prefs": [], + "schema": 1574953312771, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598777", + "why": "These add-ons collect ancillary user data among other policy violations.", + "name": "New Tab Data Collection Extensions" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f541ab9d-f671-421f-9aed-b27c63d11377", + "last_modified": 1574981047945 + }, + { + "guid": "/^((\\{7410146d-cf60-4175-a74a-0a7f7785bbff\\})|(\\{e9083fad-c9c7-4ff3-8bb5-b300d3f33546\\})|(\\{6bed141b-56b5-4209-84e1-93c67bf87711\\}))$/", + "prefs": [], + "schema": 1574883692223, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1599563", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "51353b78-a8ca-4411-98b8-e9585c03f8c7", + "last_modified": 1574953312403 + }, + { + "guid": "/^((\\{d251b448-d876-4f25-87b3-9dd858a077df\\})|(\\{3f711bc3-65bc-41bd-842f-d21b6cb90313\\}))$/", + "prefs": [], + "schema": 1574869966668, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1599833", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ff958539-ae4f-4008-8413-a3fd86950380", + "last_modified": 1574876724352 + }, + { + "guid": "/^((\\{cce3a95c-5f17-4ca1-aa0b-b5a8dc3a1b45\\})|(\\{f7c272df-f8e3-419f-b48d-759b9326d51a\\})|(\\{75a25121-873d-4da0-9aec-c4fa83cf13d3\\})|(\\{042080fd-5127-409c-88e7-47136fda641f\\})|(\\{12b167db-911b-4091-b8e5-e9292643610f\\})|(\\{d386a8b2-fba8-47ab-924d-f43d86d9edd0\\})|(\\{995f0889-b95c-4ae7-9510-e6fddf76b62c\\})|(\\{9be5400b-715c-4f49-9085-e1bdfda4a873\\})|(\\{6680a4ae-e659-47f5-9e4e-0ee72494e3af\\})|(\\{2215fd58-f9b5-4d6d-be8a-805716380cac\\})|(\\{041e6a70-edc0-4e6b-a799-0b15d1c95ddb\\})|(\\{4ba68aef-ec5e-412b-9516-d2a7401c0a22\\})|(\\{5c97c09b-6a3f-43cb-950e-c02074193751\\})|(\\{de5d0076-fa35-468e-8b72-6b5524be0684\\})|(\\{702841c9-cd9b-411f-9c8f-7616e75ce749\\})|(\\{bdfaccd7-c343-435a-b22a-fbb8052bd40b\\})|(\\{3a37182d-5653-4c5e-9729-eee005d24dfd\\})|(\\{18a44708-ff7b-45f6-96de-f2ce4373d185\\})|(\\{6c632bb3-04d3-4744-b04d-cd5dbfef41d2\\})|(\\{7ae0db73-6402-44f7-b4f3-81b7ce81bf6a\\})|(\\{716440c3-3ced-41a7-a5b3-ee1da1bfa9f2\\})|(\\{abf84dc9-1ff8-47df-a68b-70e2e8627610\\})|(\\{a369dded-b0bc-446a-b900-c56eaa74042d\\})|(\\{828e6aed-955b-4a9f-9ec2-126f7dc51f37\\})|(\\{58f9ab34-78e6-4432-912a-c13fafc24a03\\})|(\\{68b7cc55-0874-4f91-a519-7b8283b7e974\\})|(\\{71545b8d-0ebb-4767-8864-5fc8dbdbbdee\\})|(\\{217e14c5-4d4b-4691-8428-48ad484f8c04\\})|(\\{6d20b3d7-ccd6-4fa5-ae08-0343193b5518\\})|(\\{ffdb8967-9d19-4377-a728-735f21941d15\\})|(\\{5edc78a5-f41b-4e4b-90e3-ef4fa429feef\\})|(\\{12fb0651-0d2f-4c97-aa37-9121bade678c\\})|(\\{b7c5d63f-b160-48fb-a1fc-485d5e014c63\\})|(\\{4e72d89b-8481-40ae-93d9-93d3316a5d40\\})|(\\{edd994cd-7724-43d3-807b-19bc2749187a\\})|(\\{238a0064-70ee-4d1e-8dca-4bb5a893b44f\\})|(\\{3671ddcb-11ea-4f05-b97d-989d94c42279\\}))$/", + "prefs": [], + "schema": 1574789062671, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1599537", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "310e84ca-b967-4d81-b990-4d0697d62a6e", + "last_modified": 1574869966292 + }, + { + "guid": "/^((oawef0q29r43oq2\\.com@oawef0q29r43oq2\\.com)|(950opeokfp242@950opeokfp242\\.com)|(jggzkdudsuumeasqvler@jggzkdudsuumeasqvler\\.com)|(bdcixfqemzjezxbnzicj@bdcixfqemzjezxbnzicj\\.com)|(ne3787m9ykowhnicg8yx@ne3787m9ykowhnicg8yx\\.com)|(799v8e4cklkpetj0l56x@799v8e4cklkpetj0l56x\\.com)|(o7b1gs6trq2nkaocoejp@o7b1gs6trq2nkaocoejp\\.com)|(sta528sfzizg0ay009cs@sta528sfzizg0ay009cs\\.com)|(8x837zsyl6iw9hpjxu2w@8x837zsyl6iw9hpjxu2w\\.com)|(3bwrbcltn5fjo73lci00@3bwrbcltn5fjo73lci00\\.com)|(kmqm7cm5tk1l5s55yq7j@kmqm7cm5tk1l5s55yq7j\\.com)|(9n4v5kw9bf8e1f4dl0rq@9n4v5kw9bf8e1f4dl0rq\\.com)|(o4d5u4gzx14iqfuyobjr@o4d5u4gzx14iqfuyobjr\\.com)|(2ln3gtttxoz3tjerbbta@2ln3gtttxoz3tjerbbta\\.com))$/", + "prefs": [], + "schema": 1574538091411, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1599186", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Add-ons using obfuscated code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0ee9e971-ba48-4d51-a907-a3b5533e8e28", + "last_modified": 1574707139043 + }, + { + "guid": "{3d09bd90-da37-41b5-a719-8da173e9870f}", + "prefs": [], + "schema": 1574441398813, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598708", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Google Translate (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e6a12342-3936-41cf-910d-cc133db44d2a", + "last_modified": 1574445055964 + }, + { + "guid": "Stark-vpn.5.14@firefox.com", + "prefs": [], + "schema": 1574438767458, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598688", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Stark VPN" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5d2478e3-cbaa-4192-89f0-5abf3ea10656", + "last_modified": 1574441398416 + }, + { + "guid": "/^((\\{ac9ec764-a247-4d71-8807-20aa20f93e17\\})|(\\{4253de43-775d-48bc-8e08-fb3f58a2ddaf\\}))$/", + "prefs": [], + "schema": 1574434609524, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598672", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search-hijacking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fa044b4d-b39e-4ee0-a737-07aca2d672ef", + "last_modified": 1574438767139 + }, + { + "guid": "/^((\\{cc08012a-f9cd-4bfc-b526-ad8773934a0c\\})|(\\{16fd1825-3f7d-4c38-aa98-78cc4f0a6758\\})|(\\{87261aed-8d85-4037-81fb-4988ae80ee23\\})|(\\{90f51854-74d4-4df9-bf4c-b86f30346bd8\\}))$/", + "prefs": [], + "schema": 1574365292775, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598646", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "YD" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "46ed3cc3-8534-484a-bc58-349824debec6", + "last_modified": 1574434609189 + }, + { + "guid": "@youtube-adblocker-addon", + "prefs": [], + "schema": 1574334260915, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598287", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "YouTube Adblocker" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "126a0720-a22f-41f1-876f-0a3e18b79d2b", + "last_modified": 1574342360895 + }, + { + "guid": "/^((youtubetomp3@addons\\.youtube\\.com)|(addons-mozilla@youtube-to-mp4))$/", + "prefs": [], + "schema": 1574260409481, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1598242", + "why": "The add-on violates Mozilla's add-on policy by opening websites with malicious intent.", + "name": "YOUTUBE to MP3" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9729654a-6cd1-4e20-8cf5-47a3ff544ef5", + "last_modified": 1574334260570 + }, + { + "guid": "/^((\\{333d6dd8-43ed-4f01-944d-b7c737a5db72\\})|(\\{f5712532-777e-4080-b6b9-d548040f7675\\})|(\\{abf3f8d7-bc95-4dd5-ab96-c390ad5f8756\\})|(\\{f769002a-608d-4a8c-adc4-05f4857ff3ae\\})|(\\{1980667f-9bde-4e7e-8d0c-132db12c1b30\\}))$/", + "prefs": [], + "schema": 1574192492672, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1597784", + "why": "Use of affiliate redirects in violation of policy", + "name": "Affiliate Redidrecting Add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "28831b71-8a9a-466e-9f34-1f40f2f0e27a", + "last_modified": 1574260409142 + }, + { + "guid": "/^((_1gMembers_@www\\.inboxace\\.com)|(_39Members_@www\\.mapsgalaxy\\.com)|(_5zMembers_@www\\.couponxplorer\\.com)|(_65Members_@download\\.fromdoctopdf\\.com)|(_flMembers_@free\\.myformsfinder\\.com))$/", + "prefs": [], + "schema": 1573749618972, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1594461", + "why": "This add-on does not provide users with an opportunity to refuse the storage of or access to cookies.", + "name": "New Tab add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d39750c3-a305-4487-bc0f-21500c3597dd", + "last_modified": 1573804224556 + }, + { + "guid": "/^((\\{c8476e06-0a50-41ec-a840-a2db436cf38c\\})|(youtube\\.downloader@firefox\\.dev)|(youtubedownloader@firefox\\.com)|(youtubehddownloader@firefox\\.com)|(youtube\\.d@firefox\\.dev)|(advblock@blocker)|(YouTube@HD\\.Downloader)|(adt-3\\.0\\.7@blocker)|(\\{7131880e-d327-4802-b5ed-fee33c281abd\\})|(\\{5cb84843-504e-406e-8fb7-051c7fc3c9d3\\})|(\\{d8686bde-e666-4084-ae01-c75aa7a30f93\\})|(\\{96d35545-d94a-4ee1-bc43-d3055650587c\\})|(ali-image-search@4\\.0)|(\\{e7634c48-0d36-448e-891e-b2036beebcd0\\})|(\\{442de29c-b710-45d4-b121-7b4be387c327\\})|(lite-vpn-4\\.1\\.14@gmail\\.com))$/", + "prefs": [], + "schema": 1573674093610, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1596468", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons injecting remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "19c04aaf-c02d-4583-9978-c519245cd4fb", + "last_modified": 1573749588529 + }, + { + "guid": "extension@safeguard.ws", + "prefs": [], + "schema": 1573501291938, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1595616", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Safe-Guard" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "53d8f35e-99ec-44fd-8082-3b713a5afcb3", + "last_modified": 1573507879434 + }, + { + "guid": "crisorgblack@rampampam", + "prefs": [], + "schema": 1573155692372, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1595200", + "why": "This add-on contains unexpected features in violation of our policies.", + "name": "Flash Update" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2bb20972-0d56-44c8-a5d6-0e73a1827e92", + "last_modified": 1573247974595 + }, + { + "guid": "/^((\\{be2f72d6-0c71-4fd0-8914-e27057e51099\\})|(adblock-2019@youtube-addons))$/", + "prefs": [], + "schema": 1573060222748, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1594498", + "why": "These add-ons are suspected deceptive and have been disabled for your protection.", + "name": "Deceptive ad-blocking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "477174ca-470a-40ff-bf8d-2de81c2dc458", + "last_modified": 1573063322832 + }, + { + "guid": "/^((\\{8f11d935-4bcf-42c8-88c1-7e32e4541c98\\})|(\\{b0a6cb7d-3e77-4a96-aea5-7d0090e55832\\})|(\\{c5a5fd9f-8e84-460d-93dd-499e23b530e9\\})|(\\{ac7c91e8-6797-42ec-a202-5b34dacaa83a\\})|(\\{3784e311-4758-4180-b3e2-2ae42bfb081d\\})|(\\{10f3b1e0-4b97-4cc2-9af6-06a9162ec022\\})|(\\{e1109008-6d70-453d-a84e-3cc0c44af07e\\})|(\\{bb7190ac-8aeb-4a8d-83cf-89c179a4d65e\\})|(\\{a752512a-8287-40c0-97f2-071146da2caf\\})|(\\{185dbeea-416b-4fd6-8512-9ddb941e50b5\\})|(\\{d199eeb9-0964-4dff-94d2-69116fd03418\\})|(\\{d37310a3-08dd-4cb8-a276-3f2bee174555\\})|(\\{b239ce6b-2513-4758-a74e-eae3c8f0a04a\\})|(\\{f0f240d7-ae38-4bf4-a8e2-23d775eb07f4\\})|(\\{1804789c-d056-4887-839c-7a0dcab3be83\\})|(\\{d6115dfe-e3d2-44af-8361-1308c234b14c\\})|(\\{48a804b3-cf0f-4aa1-b798-82e944d7afb0\\})|(\\{3026b451-392b-47d5-b60f-995fb075cfd1\\})|(\\{e050f24c-8e06-4d6f-a962-c6073788ac29\\})|(\\{f45a7c28-423a-46ff-abb6-fc855912d074\\})|(\\{699d463b-dd6c-4cde-bad8-81beb345cb85\\})|(\\{f3a3544e-905d-4cc2-99e2-dd4c928bfc24\\})|(\\{2fa8e43c-084a-4fd7-9873-2d462f535929\\})|(\\{a7a69509-aa1d-42b3-8c8d-b3a2885f9aa7\\})|(\\{f4ca82a2-58cc-4d4f-ab95-1ca0424dcd52\\})|(\\{6f9cc262-0164-47e5-b138-024a525d2498\\})|(\\{b3061ee4-1281-482a-8022-f351c023db8d\\})|(\\{48d4fb44-5ebe-49a0-a731-541f11491d92\\})|(\\{fe8a3e05-e9ff-48d2-9291-b97bd3cba97b\\})|(\\{b7eb6ae4-2476-4ba0-a005-b868eb4bf9b2\\})|(\\{2992ca99-6c98-4864-afbf-d51c9c35ed5a\\})|(\\{2017e4f8-a7fb-421f-a1c5-d7b62189f1db\\})|(\\{d44be860-e0d3-4df2-97e1-eef2075474bc\\})|(\\{d6d4e269-00c4-4eeb-b458-d76222ff52e9\\})|(\\{a0242f56-118b-415c-83d6-8eac264dad4c\\})|(\\{25e8b980-6548-45c4-bb58-a641c13b9807\\})|(\\{40fae6f3-c629-4b2d-b690-bf022ebb2cd8\\})|(\\{359c22b8-d670-4b0e-ba77-e29615b633fb\\})|(\\{8fce6deb-83f3-4695-9c5d-76bf366757bf\\})|(\\{1edcaf80-0c4a-4380-9752-4f2953b8a053\\})|(\\{664101ff-e2e4-43b3-b470-ddb71642df7a\\})|(\\{fee7c990-6f9c-4b44-91fb-74998b2498c5\\}))$/", + "prefs": [], + "schema": 1573055872812, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1594479", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5e200b1f-e1d0-4819-89e3-3bd34f90c5c1", + "last_modified": 1573059528009 + }, + { + "guid": "/^((\\{697995ae-b9ee-4556-824b-8e82621f45dd\\})|(\\{6971a39a-916e-4b3f-a14f-bac10571b9f0\\})|(\\{d1047c9a-157b-4f87-a223-a6e6a150e1f8\\})|(\\{f23c6402-4b76-4aa4-b49a-6d083f92ae88\\})|(\\{1a7401bd-96e7-4857-bc5b-7ee66cdecc3c\\})|(\\{ccec0135-ee34-4713-a10c-607a45956d9b\\})|(\\{70428092-3df7-4151-b973-974ca511ae33\\})|(\\{cf2cc560-d765-4b59-b7b7-09a0ae4292f2\\})|(\\{880b8863-e204-47fe-8c8d-db29a10b8c8a\\})|(\\{c95951f5-2b2b-481a-9975-e38abf1f881b\\})|(\\{865df6b7-3ac1-4bd8-be0b-4cc8bb6aab7e\\})|(\\{b6124efd-aa84-42bf-80c7-01c078dc9ef2\\})|(\\{f888f1c9-a936-4ed1-a7f1-5a835ea9e6a0\\})|(\\{c434dae6-9ece-4488-8e9d-0d06d523d95a\\})|(\\{5531f988-1512-4b18-937c-7bc7e209c70f\\})|(\\{fd4fc8ae-fe95-490f-8013-6964455afb34\\})|(\\{84189ee5-4494-4068-b467-49a1b946cba1\\})|(\\{21f0db68-3156-41b4-8180-a48824b35962\\})|(\\{6a3e8874-61b4-40bf-923c-199493cfd284\\})|(\\{683172b8-3a91-4cbc-92f3-2a385f321c98\\})|(\\{269136f3-cd51-4e2d-b1eb-74878262fcef\\})|(\\{1ca4e866-0d45-4898-8138-398f49925cab\\})|(\\{9d53c5bc-9cf2-4c50-9ce5-8aaa113de07f\\})|(\\{19b80768-1437-4d18-972b-41df04a3b0a9\\})|(\\{7090cae8-4f58-4f07-9e53-b37cb4f3f8ad\\})|(\\{31911e49-6d40-4435-b152-c333096ae185\\})|(\\{4910e0bd-d803-4785-9ec4-6e8c9414a53e\\})|(\\{f3a131a1-73fe-4e8c-b499-629aa7a3e405\\})|(\\{94e53c23-cb3e-43c9-b29f-43ea953c61f6\\})|(\\{0a024f33-43b3-4d15-89dc-694964063869\\})|(\\{fa561d95-e80a-4ab9-b71c-556c7fce3b83\\})|(\\{f78a3354-dafd-48e6-8392-0ed1aa8e8a91\\})|(\\{a6b84ecd-25df-41c5-8979-a1a9d962d78b\\})|(\\{5d7282aa-0600-4a16-ad0e-917d20232d2a\\})|(\\{90e0c87b-f534-486f-b5fe-beddad5cfcfb\\})|(\\{c64d3989-b6f6-4eec-b86f-b549a5b2056a\\})|(\\{ccdd4a2e-11a4-4922-8bd2-d8ff05d3194f\\})|(\\{b9f73347-01c2-4727-85cd-7521a2a2841c\\})|(\\{1be85529-e82e-4dfe-aa33-3c62fc37ae8e\\})|(\\{b91fff1b-d45e-47b2-95da-5d774319b88d\\})|(\\{8c81cdd1-4fc2-4510-83c2-641ea7ee7c24\\})|(\\{e5db6079-1786-4072-80f5-d5c894074cc2\\})|(\\{5981e135-789c-4709-8b22-85e0665c8396\\})|(\\{a235175f-3391-43c6-bf58-34c1f6b8f4a6\\})|(\\{617ebed4-628f-4ca3-92de-bec510cab1dd\\})|(\\{3aaa37bc-0fa9-4e34-abbb-d5a34ff729a3\\})|(\\{2d3521bc-914f-4990-ba0a-5d488f176f4e\\})|(\\{17c7768d-c672-4e99-aef2-2fdd35a96804\\})|(\\{7e517ca8-6f65-4de2-869f-dddce73a274c\\})|(\\{9c99bd01-7567-47e6-80f2-bf229b97415f\\})|(\\{5357eef8-cc2f-496a-82d2-5f3b6ca5f7b1\\})|(\\{b83a8605-057b-46ef-86ed-d53db9a9f16f\\})|(\\{55383502-1800-4cf8-8522-df16d6cb9809\\})|(\\{14d742cf-cd34-46a5-984e-1e28fc16e55c\\})|(\\{f5d6d1af-6a02-4eb4-a7fb-ec3486239875\\})|(\\{fac56640-6cad-49a2-b224-9424e18b4009\\})|(\\{79c16aa2-f8a9-40d3-9e81-74884b987e48\\})|(\\{b02eae40-18d6-4e28-8af7-6b161a458769\\})|(\\{9398d059-fc67-43e2-8548-bc60cfa82998\\})|(\\{cd0a4138-8670-4a6b-b15c-0cad1ef290e6\\})|(\\{503dc84b-9967-4b1f-8199-cee55bcd919e\\})|(\\{d0245186-049d-4594-88f9-3fe54e735551\\})|(\\{7208c980-005e-426c-991f-1cd22227b7fb\\})|(\\{19dcb8c5-3577-4279-b70f-eb48a594f821\\})|(\\{fda599e0-d842-47d9-8c8e-21c26ef0afa6\\})|(\\{470eab98-7ad9-49a5-883f-42cb51996fc7\\})|(\\{8c353e56-0d27-4b83-b081-6e012bda5601\\})|(\\{a1931145-d58d-41a2-b190-04549ef9c225\\})|(\\{916d2e1d-aabf-45aa-84ec-a9bb9ee40391\\})|(\\{f2aa9466-c326-440c-b9a8-f2fef8571341\\})|(\\{52a02f53-b805-41d1-af02-0283da3ef383\\})|(\\{aad686bf-b881-4177-8a8f-12c9455e319f\\})|(\\{cec578f9-9103-44f8-9b41-bbe26ba8a993\\})|(\\{6891bcc0-4ad2-4531-9699-ce82528ddcc0\\})|(\\{a5d93cd7-b187-46f8-961b-9f4fc0b4f37f\\})|(\\{b220428d-2f07-4ecd-9114-3a3261d5f7fc\\})|(\\{b0f4740a-6e0f-489e-ab86-3fb3abb7dd15\\})|(\\{611781ad-a17d-4487-8caf-bf72818bacfc\\})|(\\{0b4efcee-b7eb-4151-bdd2-b2f66fedd4fa\\})|(\\{25da4f88-e61c-4823-9c32-864c3a348aab\\})|(\\{e44b4144-0a1a-4c29-95c0-50083ed5c606\\})|(\\{4dbf4218-1a54-4fa1-8448-0521da5b5391\\})|(\\{7140247a-7ee6-4474-9fe9-0cdf6ec7e2e7\\})|(\\{50eac97c-bb52-4107-a3be-6d40eba5b229\\})|(\\{4bd98a1f-d8f6-411a-a1e8-0cf1b482c682\\})|(\\{a7d60016-47ac-4480-b2bc-83dd4e20b096\\})|(\\{a2657d9a-94d1-41e8-a1e5-72e60c032afe\\}))$/", + "prefs": [], + "schema": 1572982905588, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1594460", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f1eaa18e-75c8-4134-87cf-5a02e3349e44", + "last_modified": 1573055860762 + }, + { + "guid": "/^((web@3753c687-a0c6-4cd1-b8ff-bab3c76b1236)|(ext@286f29a6-184e-4195-b1e5-1212ebf372a0)|(web@fd9b3d8a-1178-45ab-92a8-a172d0b70000)|(web@e7a72615-19b1-42a5-8e34-ddfa89ce0000)|(web@af1e58bc-4ead-11e8-81fc-065ad97f0000)|(web@7398cc9a-684e-40a4-afe1-b620e1a80000)|(web@2DC2452E-6999-11E8-A1CA-6C6318C60000)|(web@B7CCDA78-8455-11E8-91A4-ED179E46D000)|(web@8842eb7c-5f5c-4d33-aabe-81c27ae87000)|(web@ca5a2803-6421-4582-97e1-9e30fe440000)|(web@00022358-d56d-4f5e-a89c-d4534d7c5565)|(\\{ff26fde6-4d73-49a3-bd6c-1ca7876484fc\\})|(290e9605-6fb1-4c8b-a3d4-0084bfad201e@UniversalConverter)|(web@EmailNewTab)|(web@WhatsMySpeed)|(test@FormsHub)|(web@Forms2)|(newweb@MapsNewTab)|(\\{4b1d718d-58ae-404b-a5d2-b7b977cfcb56\\}))$/", + "prefs": [], + "schema": 1572291693826, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1592597", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3bdd5b53-ddb1-4fb0-b250-d75a604a5546", + "last_modified": 1572443230107 + }, + { + "guid": "/^((\\{a059a924-e43a-495d-9620-ad8c111d62d9\\})|(\\{79f4bfc6-b1da-4dc4-85cc-ecbcc5dd152e\\}))$/", + "prefs": [], + "schema": 1571600493541, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1589974", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Page Translator" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9e8f80d6-a818-4004-9a20-deec55f3fb96", + "last_modified": 1571605630002 + }, + { + "guid": "/^((firefox@browser-security\\.de)|(firefox@smarttube\\.io)|({0fde9597-0508-47ff-ad8a-793fa059c4e7})|(info@browser-privacy\\.com)|({d3b98a68-fd64-4763-8b66-e15e47ef000a})|({36ea170d-2586-45fb-9f48-5f6b6fd59da7})|(youtubemp3converter@yttools\\.io)|(simplysearch@dirtylittlehelpers\\.com)|(extreme@smarttube\\.io)|(selfdestructingcookies@dirtylittlehelpers\\.com)|({27a1b6d8-c6c9-4ddd-bf20-3afa0ccf5040})|({2e9cae8b-ee3f-4762-a39e-b53d31dffd37})|(adblock@smarttube\\.io)|({a659bdfa-dbbe-4e58-baf8-70a6975e47d0})|({f9455ec1-203a-4fe8-95b6-f6c54a9e56af})|({8c85526d-1be9-4b96-9462-aa48a811f4cf})|(mail@quick-buttons\\.de)|(youtubeadblocker@yttools\\.io)|(extension@browser-safety\\.org)|(contact@web-security\\.com)|(videodownloader@dirtylittlehelpers\\.com)|(googlenotrack@dirtylittlehelpers\\.com)|(develop@quick-amz\\.com))$/", + "prefs": [], + "schema": 1571235422841, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1483995", + "why": "Sending user data to remote servers unnecessarily, and potential for remote code execution. Suspicious account activity for multiple accounts on AMO.", + "name": "Web Security and others" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "2.0.9", + "minVersion": "0" + } + ], + "id": "96b2e7d5-d4e4-425e-b275-086dc7ccd6ad", + "last_modified": 1571235848152 + }, + { + "guid": "{381f21b1-95bf-4042-bc5c-3a40b2a03f10}", + "prefs": [], + "schema": 1571235400908, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1583468", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Francezon" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.0", + "minVersion": "0" + } + ], + "id": "dd1261a3-6944-4f51-8118-b0a8f2055d69", + "last_modified": 1571235422828 + }, + { + "guid": "addon@shoppingguru.info", + "prefs": [], + "schema": 1571235356487, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1583862", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "ShoppingGuru" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "2104.1111.1110.4221", + "minVersion": "0" + } + ], + "id": "70655a4b-064d-44ab-8d0e-3bec419343ee", + "last_modified": 1571235400895 + }, + { + "guid": "sparalarm@chip.de", + "prefs": [], + "schema": 1571082106908, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1586677", + "why": "The add-on is force-installed for users through sideloading, bypassing user consent.", + "name": "Sparalarm" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "14.39.1", + "minVersion": "0" + } + ], + "id": "19f599bd-2226-49e2-90fd-685fd106fc3d", + "last_modified": 1571235356473 + }, + { + "guid": "/^((\\{65a93e3b-e350-440d-bf8f-68e18e38d27d\\})|(\\{9db1fb44-b661-4719-9d90-67af3e6a314c\\}))$/", + "prefs": [], + "schema": 1571059482617, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1586834", + "why": "This add-on contains an unexpected feature that is collecting ancillary data.", + "name": "Google Custom Logo and Search Counter" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "be5d02cb-0a36-4c20-b184-509b86d243c5", + "last_modified": 1571060494402 + }, + { + "guid": "{c4d46c5f-9832-4057-8a1d-635949ed6a55}", + "prefs": [], + "schema": 1570909292250, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1587121", + "why": "This add-on makes use of search settings with unexpected behavior.", + "name": "LM Search" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "57c094fd-9a37-4ede-9bb0-a1c2d66415f0", + "last_modified": 1571059471124 + }, + { + "guid": "/^((\\{2B0EC7FF-F330-4e0c-8B33-EFFEC8D39E70\\})|(\\{f70c89b0-bbf3-41a9-bc1f-0912dcf53f33\\})|(@Classifieds)|(@Converter)|(@Coupons)|(@Directions)|(@DownloadManager)|(@Email)|(@Fitness)|(@Flights)|(@FormsApp)|(@Games)|(@Maps)|(@News)|(@Package)|(@Photo)|(@Radio)|(@Recipes)|(@search-encrypt)|(@search-incognito)|(@searchencrypt-b)|(@searchencryptblocker)|(@Speedtest)|(@Sports)|(@Transit)|(@TV)|(@Weather)|(aweapps@Email)|(classified@jetpack)|(email@searchleasier\\.com)|(foo-bar@example\\.com)|(games@jetpack)|(JS@Converter)|(login@easier)|(maps-webext@jetpack)|(Maps@SSA)|(web-ext@games\\.com)|(web@ShoppingNewTab)|(web@SocialNewTab)|(web@WebDesignNewTab)|(webapp@LoginAssistantTab)|(webex@Converter)|(webex@Email)|(webtab@Shopping)|(webtab@Social)|(webtab@WebDesign))$/", + "prefs": [], + "schema": 1570701667316, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1587782", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Addons collection ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "986aac02-beba-40b2-b463-d8447a778a2e", + "last_modified": 1570713923845 + }, + { + "guid": "{2a78ab07-91b2-4086-889d-619e43d5e5f8}", + "prefs": [], + "schema": 1570563693690, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1587734", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "SVSrch" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "bb76b262-7087-4cf2-a82f-12cfbf91239a", + "last_modified": 1570701652245 + }, + { + "guid": "{2e106fa4-ee23-4b4a-9ed0-f93edee539b5}", + "prefs": [], + "schema": 1570534326932, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1587074", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "SVsrch" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ab0b6868-6077-4d47-bd58-1df3f572c04d", + "last_modified": 1570540061375 + }, + { + "guid": "/^((oigfsj9434lavvv@oigfsj9434lavvv\\.com)|(mity82900jf2@mity82900jf2\\.com))$/", + "prefs": [], + "schema": 1570527949565, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1586780", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Page organizer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3ece7523-cdec-46cf-ac20-9e939a4d4802", + "last_modified": 1570534316570 + }, + { + "guid": "/^((web@MapsNewTab)|(ext@WhatsMySpeed)|(ext@MyEmailCenter)|(ext@TemplateHelper)|(ext@FreeLiveRadio)|(ext@FreeFormsNow)|(web@ConverterNewTab)|(web@EmailAccountNewTab)|(spweb@EmailNewTab)|(web@PackagesNewTab)|(web@ManualsNewTab)|(web@ClassifiedsNewTab)|(web@FormsNewTab)|(web@AncestryNewTab)|(web@RecipesNewTab)|(web@TVNewTab)|(web@WeatherNewTab)|(web@translationsNewTab)|(web@SportsNewTab)|(web@CouponsNewTab)|(web@FlightsNewTab)|(test@InstaFormsFinder)|(web@NewsNewTab)|(web@SpeedTestNewTab)|(web@TransitNewTab)|(web@BibleNewTab)|(web@RadioNewTab)|(web@TemplateNewTab)|(web@PhotoEditorNewTab)|(web@AudioConverterNewTab)|(web@GamesNewTab)|(web@VideoConverterNewTab)|(test@TheWeatherChecker)|(web@InterestsNewTab)|(test@SearchHub)|(web@CalendarNewTab)|(web@TaxesNewTab)|(web@BankNewTab)|(web@CrimeReportNewTab)|(web@CryptoNewTab)|(web@eBooksNewTab)|(web@FinanceNewTab)|(web@FitnessNewTab)|(web@JobsNewTab)|(web@MoviesNewTab)|(web@NotepadNewTab)|(web@OfficeNewTab)|(web@PCTextingNewTab)|(web@PhotoNewTab)|(web@ScrapbookNewTab)|(web@UtilityNewTab)|(test@FlightTrackerUpdate)|(test@FreeForms)|(test@UniversalConverter)|(test@MyEmailCenter)|(test@TemplateHelper)|(test@FreeLiveRadio)|(test@FreeFormsNow)|(ext@FormsHub)|(webext@WhatsMySpeed)|(webext@WatchTelevision)|(webext@EmailExpressTab)|(webext@MyConverterTab)|(webext@TemplateCreatorTab)|(webext@LocalForecastTab)|(webext@EZDirectionsandMapsTab)|(webext@MapsNDirectionsTab)|(webext@MyEmailCenter)|(webext@TemplateHelper)|(webext@FreeLiveRadio)|(webext@FreeFormsNow)|(webext@InstaFormsFinder)|(webext@TheWeatherChecker)|(webext@FlightTrackerUpdate)|(webext@FreeForms)|(webext@UniversalConverter)|(webext@FormsHub)|(webext@SearchHub)|(ds@DirectSearchPro)|(webext@AppDiscoveryTools)|(webext@LiveRadioProTab)|(webext@TheWeatherPilotTab)|(webext@ExpressPackageFinderTab)|(webext@LoginAssistantTab)|(webext@SelectSearch)|(addon@SelectSearch)|(webext@ExpressSpeedChecker)|(webext@BreakingNewsPlus)|(webext@WorldEventsToday)|(addon@WorldEventsToday)|(webext@OnlineTVAccess)|(webext@FindingFormsPro)|(webext@EasyClassifieds)|(webext@SpeedCheckerPlus)|(webext@GetFreeCoupons)|(addon@MyEmailChecker)|(app@FlightTrackerUpdate)|(app@WatchTelevision)|(app@WhatsMySpeed)|(app@MyEmailCenter)|(app@TemplateCreatorTab)|(app@FreeLiveRadio)|(app@FreeFormsNow)|(app@InstaFormsFinder)|(app@SearchHub)|(app@TheWeatherChecker)|(app@FormsHub)|(webtab@Maps)|(app@FreeForms)|(app@UniversalConverter)|(app@EmailExpressTab)|(app@MyConverterTab)|(app@TemplateHelper)|(app@LocalForecastTab)|(app@EZDirectionsandMapsTab)|(app@MapsNDirectionsTab)|(app@AppDiscoveryTools)|(app@LiveRadioProTab)|(app@TheWeatherPilotTab)|(app@ExpressPackageFinderTab)|(app@WorldEventsToday)|(app@OnlineTVAccess)|(app@SpeedCheckerPlus)|(app@EasyClassifieds)|(app@GetFreeCoupons)|(app@FindingFormsPro)|(app@TemplatesHereTab)|(webtab@Packages)|(webtab@Forms)|(webtab@Email)|(webtab@Radio)|(app@MyEmailChecker)|(webtab@TV)|(webtab@Classifieds)|(webtab@Ancestry)|(webtab@Weather)|(webtab@Manuals)|(webtab@SpeedTest)|(app@MyDailyCalendar)|(webtab@Transit)|(webtab@Converter)|(webtab@Coupons)|(webtab@Recipes)|(webtab@News)|(webtab@Sports)|(webtab@translations)|(webtab@Template)|(webtab@AudioConverter)|(webtab@Flights)|(webtab@Photo)|(webtab@Bible)|(webtab@PhotoEditor)|(app@MyVideoConverter)|(webtab@Games)|(app@MyLoginHelper)|(app@QuickEmailAccess)|(app@MyRecipeFinder)|(app@MyFlightFinder)|(webtab@Fitness)|(webtab@VideoConverter)|(webtab@Taxes)|(webtab@Bank)|(webtab@Calendar)|(webtab@CrimeReport)|(webtab@Crypto)|(webtab@eBooks)|(webtab@Finance)|(webtab@Interests)|(webtab@Jobs)|(webtab@Notepad)|(webtab@Movies)|(webtab@Office)|(webtab@PCTexting)|(webtab@Scrapbook)|(webtab@Utility))$/", + "prefs": [], + "schema": 1570483391885, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1587028", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "bd51d01b-20bf-4682-a588-c03e988eb746", + "last_modified": 1570527938865 + }, + { + "guid": "{67d4f93e-6857-45ab-9e7a-158cc61f15d2}", + "prefs": [], + "schema": 1570052453260, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1585735", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "zidaza" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6ce8f6d0-5cef-457c-b7e4-d5c08d61531a", + "last_modified": 1570053270833 + }, + { + "guid": "/^((\\{049aedf7-b1dc-49b7-9ef4-51cd026f8592\\})|(\\{27c743d8-4731-44ff-9194-9f663df6a3c3\\})|(\\{8bf2bae7-273a-4496-9073-c1ad6688cda8\\})|(\\{b9cfeaaa-2465-4b21-903a-7955fcd4e59a\\})|(\\{07848e3a-f42b-48bf-ac70-f918a7b79258\\})|(\\{02b2eed4-2ca5-4d6f-b19a-a8b1738501f0\\})|(\\{d2af93ad-a542-4b30-b929-7437c2b02afe\\})|(\\{b8e33742-5b78-4c08-89e1-94ebe22fbafe\\})|(\\{502b29ef-b05f-4043-949b-069160f39e32\\})|(\\{2c60eb88-2f38-4b1a-9329-eaa18ad41720\\})|(\\{ccb18b6f-0680-4347-afd5-5ed864114f05\\})|(\\{c9622009-2b37-44f1-a5cf-f0aa869a6bff\\})|(\\{cc241172-6e33-4395-bc68-dd76fa6b6091\\})|(\\{904a47b7-c2c3-465d-a7f9-326c51e8fee0\\})|(\\{365ae96e-15f8-445f-816e-74cd5897613c\\})|(\\{527ccde6-f15c-4437-a061-052593c2ac5b\\})|(\\{063b5c1a-b708-4f05-873b-b3e161d1d49a\\})|(\\{d5422b3f-fcb3-4eca-b2a9-b4b8010fd4c1\\})|(\\{60bab1b2-e7fc-4683-85ef-2ec6cdb5e148\\})|(\\{aad32311-3e2f-419d-8e6b-82c4e28c44d3\\}))$/", + "prefs": [], + "schema": 1570045305267, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1585810", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Add-ons using obfuscating code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a942e2e5-6a55-439f-b078-6288da1cfe68", + "last_modified": 1570052444069 + }, + { + "guid": "/^((\\{6be4ca4f-8b7e-4d91-994b-24a7c5384086\\})|(\\{1709abf0-19ba-48a8-a9dc-03e931b17f48\\})|(\\{5d5af81d-6501-4d77-a64c-ad2cdc34ceae\\})|(\\{b7de0a65-0b4b-4b74-a4e6-70fc2c36a80a\\})|(\\{64704bd2-ced7-4dd8-a5d2-d20df624288f\\}))$/", + "prefs": [], + "schema": 1570036215253, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1585723", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Add-ons using obfuscated code." + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4e487385-fe8d-451a-909e-a6f6609b0d7b", + "last_modified": 1570036367326 + }, + { + "guid": "{5dbbb375-3520-4ace-bb84-df9d92ae1a25}", + "prefs": [], + "schema": 1570017905113, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1585708", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Wappalyzer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9a12e67a-ef6d-4f06-9de6-7dc8bcb19517", + "last_modified": 1570036206290 + }, + { + "guid": "{d41cf5b5-67b7-4510-8633-d8e2c0ec5d46}", + "prefs": [], + "schema": 1570001773270, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1585625", + "why": "This add-on violates Mozilla's add-on policies by using obfuscated code.", + "name": "Xisey" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "87d99543-1e31-46b3-9fd1-91d9c79ff592", + "last_modified": 1570017897427 + }, + { + "guid": "mozilla_cc3@internetdownloadmanager.com", + "prefs": [], + "schema": 1569844559791, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1585071", + "why": "Some versions of this add-on violate Mozilla's add-on policies by using unreviewable source code. Please update to a newer version.", + "name": "IDM Integration Module" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "6.35.5", + "minVersion": "0" + } + ], + "id": "21f14cff-afef-4e0e-97e8-4dbc0207a7a6", + "last_modified": 1569865071841 + }, + { + "guid": "{546bc2af-d6e7-499f-90b6-58305b836702}", + "prefs": [], + "schema": 1569699691574, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1583809", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "MapsFrontier" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f3133639-f5e5-4b47-a80f-2993fa97ca4a", + "last_modified": 1569844523596 + }, + { + "guid": "{f39b7905-00d5-4391-9a4b-751ca08dd6b2}", + "prefs": [], + "schema": 1569440826824, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1583814", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Tic-Tac-Toe Evolution_G" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4c3153a8-7551-48f3-a4fa-8ad6cb28faa8", + "last_modified": 1569441800696 + }, + { + "guid": "/^((ehfiibbkgllccnbifchmillffgdlmidi@chrome-store-foxified-3843796584)|(ehfiibbkgllccnbifchmillffgdlmidi@chrome-store-foxified-3264908934))$/", + "prefs": [], + "schema": 1569242005272, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1582781", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Proxy add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "72464ab8-9b51-420f-8e54-bc0f8bb01cf1", + "last_modified": 1569244094631 + }, + { + "guid": "/^((@searchincognito)|(@si-defaultsearch)|(@si-defaultsearch-listed)|(@searchassistincognito)|(@DiscreteSearch)|(@Discrete-Search)|(@searchsafe)|(@SearchSafeOrg)|(ffredirector@discretesearch\\.com)|(ffredirector@encryptedsearch\\.org)|(ffredirector@searchdefence\\.com)|(ffredirector@searchencrypt\\.com)|(ffredirector@searchencrypted\\.com)|(ffredirector@searchincognito\\.com)|(ffredirector@searchsafe\\.co)|(ff_redirector@discretesearch\\.com)|(ff_redirector@encryptedsearch\\.org)|(ff_redirector@searchdefence\\.com)|(ff_redirector@searchencrypt\\.com)|(ff_redirector@searchencrypted\\.com)|(ff_redirector@searchincognito\\.com)|(ff_redirector@searchsafe\\.co)|(@encryptedsearch)|(@searchdefence)|(@searchencrypted)|(@42e62954-834c-11e7-bb31-be2e44b06b34)|(@DiscreteSearchx)|(@4aec09f1-f1c9-456d-8c40-e0e86f302a0d)|(@566ff1c3-9432-4ed4-bd3d-b43cba47e051)|(@1df4e663-b9f3-4708-9f5d-44265b33397e)|(ff_redirector@searchsafe)|(\\{9b62bdf8-a3c7-43d3-ba7f-0970cabffdaa\\})|(\\{95b48d11-b256-48ad-8ba1-bfe52f0a8bb8\\})|(\\{9e35a2be-64bd-49e3-aa47-fbeedf1834eb\\})|(\\{3ba10b5f-d9fa-4b40-8683-034d3dfc71d4\\})|(\\{20c31601-ebee-4677-a2f0-40e178bf7c77\\})|(\\{98e02622-f905-434e-9003-6c061b5c11c0\\})|(@tabwow)|(gaidpiakchgkapdgbnoglpnbccdepnpk@chrome-store-foxified-258456913)|(@tabwow2)|(\\{be8901e4-2a07-4554-aa05-a64351496e29\\})|(moviestmpd@mozilla\\.com)|(gaidpiakchgkapdgbnoglpnbccdepnpk@chrome-store-foxified-876542484)|(\\{4a8ef415-e453-458f-bfbd-ae92569214db\\})|(fireaction@mozilla\\.com)|(\\{bd9c448c-58b3-434f-9bb6-4ed2c155ba8e\\})|(\\{ebdfa19b-0906-4f78-9e95-7ef74d34c335\\})|(websecure-unlisted@mozilla\\.com)|(\\{2d06d70b-8f32-4007-8f8b-1e0445bcebe7\\})|(\\{ddbe7345-acf4-4ebb-9baf-cd6d2df73b28\\})|(\\{b09d5b98-2d65-46fb-990c-69710577efa0\\})|(\\{3894384e-c719-4a0c-8d24-3816160fc56b\\})|(search-encrypt-tab@mozilla\\.xpi)|(\\{1dafa1da-3894-48b9-ac8f-00bdc4f1868a\\})|(\\{99cfe634-328a-41a5-9a23-64094e4f4919\\})|(inco-plugin@mozilla\\.xpi)|(incognito-window@mozilla\\.xpi)|(mac-search@mozilla\\.xpi)|(fvdplayer@fvd\\.com)|(playernewpp@ext\\.com)|(\\{492936c6-9121-4e54-8d4f-97f544e5bf98\\})|(\\{108a22ea-f316-4c2f-8427-fe65e02f9e2c\\})|(cold@being\\.net)|(\\{38b99237-6c28-406f-898c-cc89df86051d\\})|(search_redirect@mozilla\\.xpi)|(\\{d2ef4a8d-6ec0-4733-9f3f-2394178ecbf3\\})|(tab_plugin@mozilla\\.xpi)|(\\{ae228e30-f40a-41a3-9e7e-53a094dcb8c6\\})|(\\{00ee7237-53cb-4036-8d4f-e78d78ca89e7\\})|(\\{d2f4002c-031b-4ad3-9fb1-afb003e8f932\\})|(\\{c0f366b3-7b3d-4486-a6f3-4ca1d7045091\\})|(\\{ccc6cfc4-3832-4d05-bf28-43a9722de93f\\})|(\\{dd02f638-ce6d-464e-8add-6ea0f314b1d1\\})|(\\{749ed3ff-4d23-4b32-812e-a35e3cf8c000\\})|(tab_cleanup@mozilla\\.xpi)|(incognito_tab@mozilla\\.xpi)|(\\{47c51f55-4f0b-499f-9fdd-c7c66bf4796a\\})|(\\{cd70c7c8-557d-46fa-9688-399c7c8d3d66\\})|(\\{681ad8e0-d1df-4cd2-a4cf-b97c1d6502a3\\})|(\\{0d58e690-bd48-4e3a-baf3-67aa40bc286a\\})|(\\{77bfbf26-4618-4120-9cb6-1fc7c92b8ddc\\})|(\\{037c6f6a-71f8-405b-9cff-fadf2ded6c47\\})|(\\{91cc3274-90d5-4e16-80e3-cd02fc513689\\})|(\\{2225b2af-0c3c-4345-adac-4f5bd40c2182\\})|(\\{81ca6b1e-a95b-4b44-9638-3ff3ea1a571d\\})|(\\{1e32acf8-fc1e-40ae-8783-c501ce50d597\\})|(\\{19670785-b1db-4d69-9538-2880ad8fdf20\\})|(\\{0113b4ad-15ca-4215-adeb-f0404f619ca6\\})|(\\{c7245149-4224-4c5c-91a4-84ea189f2227\\})|(\\{04dd2232-f1b1-4275-ae74-8bd27f3d850c\\})|(prosearch@mozilla\\.xpi)|(\\{d549a064-98e7-49ed-ba9e-a724e79a004f\\})|(\\{fddd3bc6-9d4e-4ee7-b490-0d6141ff7d7f\\})|(\\{122795b5-ae28-4371-9b61-878f5db888ac\\})|(\\{e3d491de-802a-4f82-91eb-9403c9f43637\\}))$/", + "prefs": [], + "schema": 1569181301396, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1561752", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Various search redirectors" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "bea9680c-28c0-48a1-b8d4-e418adeba748", + "last_modified": 1569241020387 + }, + { + "guid": "{87bd05d5-d79e-4421-9c78-5c98ea78c351}", + "prefs": [], + "schema": 1568901038866, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1582168", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "TypeScript-Console" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b9d746da-8881-4854-8026-ecb708a588f6", + "last_modified": 1568905123995 + }, + { + "guid": "/^((hd@youtube\\.com)|(@youtube-to-mp4)|(youtube-downloader@youtube\\.com))$/", + "prefs": [], + "schema": 1568885060842, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1579965", + "why": "These add-ons violate Mozilla's add-on policies by including unexpected features without user consent.", + "name": "More Youtube Downloaders" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ea29de20-dfe2-40c0-a5ec-45b1c780815d", + "last_modified": 1568901023136 + }, + { + "guid": "/^((\\{d81c0c7d-7420-4737-a3b9-dd9edeb4412f\\})|(\\{2bc89af7-d0ff-4b22-b7f6-ec87d15d999e\\})|(\\{3ee12352-a9db-4370-aa27-7e1d9acb628a\\}))$/", + "prefs": [], + "schema": 1568816167726, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1581368", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "FSCH" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1e56fd0a-77b6-477d-bada-78a5c18a37dd", + "last_modified": 1568816392824 + }, + { + "guid": "/^((@yvd-addon)|(ydh@downloader\\.youtube\\.com)|(tomp3@youtube\\.com))$/", + "prefs": [], + "schema": 1568814899255, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1579965", + "why": "These add-ons violate Mozilla's add-on policies by including unexpected features without user consent", + "name": "Youtube Downloaders" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "73cae2b8-f60e-47b3-afca-a1ff555f4ec5", + "last_modified": 1568816154119 + }, + { + "guid": "{91c43d32-3a20-40cb-933b-47fd7c4b5a4e}", + "prefs": [], + "schema": 1568662894970, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1581111", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Cookies Next" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e22dbf2b-70c8-41ed-b17a-32f53a22055b", + "last_modified": 1568814886596 + }, + { + "guid": "{23db2a76-49ca-4af2-af50-fccedd607e12}", + "prefs": [], + "schema": 1568317295345, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1580616", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "YUI" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "13056233-543d-4c51-b89a-2a6b96ee324e", + "last_modified": 1568374273725 + }, + { + "guid": "{1056b983-063b-4bd0-b7f7-1295f7e04ade}", + "prefs": [], + "schema": 1568226375813, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1580749", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fast Browser" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "886e480a-8838-43c7-b9ca-b02592e23065", + "last_modified": 1568285711403 + }, + { + "guid": "/^((\\{8d0073fa-3193-4a7f-8c95-6a4e3d9272ba\\})|(445ac9f4aa1b833ce2dc75d6d6d6c76d0cef7cc7@temporary-addon)|(\\{cd0672d3-72dc-43d2-ae77-6cda31fb7c88\\})|(\\{403321a6-be8d-4ae3-a66d-e5c846f993b8\\}))$/", + "prefs": [], + "schema": 1568154081436, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1580503", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Proxy Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6a54f1c3-93e1-4a38-885c-afb98e7bc4f2", + "last_modified": 1568224845558 + }, + { + "guid": "{0fbe26d1-6891-475e-af3e-34f38a30348d}", + "prefs": [], + "schema": 1567971701994, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1580118", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Lookbox" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "67a61ba7-ad12-4eae-bdcc-e5b9b856dff4", + "last_modified": 1568105279739 + }, + { + "guid": "{a59679da-f097-4db4-b2bc-6ad7b645e127}", + "prefs": [], + "schema": 1567019346545, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530194", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "GetMedia - Movies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.18", + "minVersion": "0" + } + ], + "id": "db043b8d-c91a-4d1d-b420-58087beef552", + "last_modified": 1567022112963 + }, + { + "guid": "{850be3a2-ca5f-47ad-838c-fe39b006e0da}", + "prefs": [], + "schema": 1567018965949, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530194", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Safe Browsing" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.19", + "minVersion": "0" + } + ], + "id": "aeb6bc24-36cf-4199-94d9-68f4822fa2cf", + "last_modified": 1567019082710 + }, + { + "guid": "{ecb03616-f3c2-4580-99dd-6a233047abdd}", + "prefs": [], + "schema": 1567018902311, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530194", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Sport TV" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.19", + "minVersion": "0" + } + ], + "id": "7e71a557-5664-4319-a792-c94b2bf744d8", + "last_modified": 1567018965935 + }, + { + "guid": "{8387ccbe-b9ac-438d-b049-c86b30a6dacb}", + "prefs": [], + "schema": 1567017236054, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530194", + "why": " This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "GoMusic" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.17", + "minVersion": "0" + } + ], + "id": "9d598ecb-2473-4416-98b9-26658ea7746e", + "last_modified": 1567018491638 + }, + { + "guid": "{7ff51e81-f4b1-4682-9f45-43a771d80748}", + "prefs": [], + "schema": 1567017189557, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530194", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "GoMovies" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.16", + "minVersion": "0" + } + ], + "id": "627a5309-d310-4130-9827-d3e6d3116ba2", + "last_modified": 1567017236040 + }, + { + "guid": "{2ef58672-740c-46bd-a50d-b9880986b574}", + "prefs": [], + "schema": 1567017167776, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530194", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Universe Start" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.5", + "minVersion": "0" + } + ], + "id": "c0767051-017a-45d7-b96f-632d7cae7c47", + "last_modified": 1567017189541 + }, + { + "guid": "{df9f6ab1-c82c-41d4-85ce-86dcfe839ce9}", + "prefs": [], + "schema": 1567017037001, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530194", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Media Start" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.0", + "minVersion": "0" + } + ], + "id": "6e9ea480-7f5f-49c7-b130-fb8af66bec0b", + "last_modified": 1567017167761 + }, + { + "guid": "{b89efd87-232e-4829-87d2-22148919d72f}", + "prefs": [], + "schema": 1566762089721, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1576681", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Ad-Blocker" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "04365e2c-fbc2-4393-b2b3-a23ea5de6b79", + "last_modified": 1566934428180 + }, + { + "guid": "/^((\\{fb2cbb8e-a6f9-464b-97c7-aca958a404d6\\})|(\\{8cc60aa4-fceb-4a74-bef4-bbbdc23b85fb\\}))$/", + "prefs": [], + "schema": 1566419767711, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1573237", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Adobe Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "055d2447-c7b7-49a2-bb27-b1c09a592f32", + "last_modified": 1566422986828 + }, + { + "guid": "/^((AKMWWOEASKZXXCCRFA@AXXX)|(RF_WOEKTAMS@BASDFZMD)|(TI_BOAMZKXFFEE@ZCAOKEE)|(BMMAOSIKZX_TI_EE@BOASLL)|(ZN_ASVVZXLEOOO@ABOVVAE)|(MZXVCKMSSD_ZN_EF@GBOAAOS)|(UA_CZVVMSOEEE@DOPEEZXA)|(ZXCLASKDASD_UA_VV@AVLLZAA)|(VS_SOEOKAKXC@BOOCLLAA)|(AIWEKDZSV_VS_ASCZ@SAAAOOBB)|(IF_AZXOOVVVAA@EALXCOAA)|(VVALLEIASD_IF_AZ@OOBAAXXA)|(ZN_BF_AOXZAOKSD@VZXMASLX)|(VMZKMALSKD_BF_ZN@VIAAEOOSL)|(aunastralaa_1@auzn\\.ne)|(VXKCVLSO_ON_SOC@KIZIAO)|(ON_OPPOWOAKKS@BLZOAAAASL)|(nostallkka@iofjjakk\\.me)|(VZKKAKOOEE_LN_FK@BOOAASSDD)|(LN_VSOAJKXXXZZ@UDPPPADK)|(BNKZKASKDIE_EB_AKK@BLALEEKK)|(EB_AISJIKZLAA@DFKKAKE)|(NLDFKOBASOKCK@KFOLAAAEE)|(BBDMLDSFKER_AF_GG@AYYRR)|(OMVXNSDF_R_F_W@BMAMSEEE)|(EIGKFDAODS_RF_PDNAP_WEN@HUUUHJAA)|(GOEORG_RF_ANA_N@BDOFKOKK)|(BFDISDF_RG_AZXC@BDOFKAQ)|(OKOKDFBDFBDFG_RG_KS_BDK@AAAJAHU)|(ODKFOBKDPFB_PJ_FGK_AKK@AJIIJAA)|(UBXODJ_PA_PJ@XMVCOAKS))$/", + "prefs": [], + "schema": 1565206889166, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1572346", + "why": "This add-on violates Mozilla's add-on policies by executing unwanted actions on websites without the user's consent or control.", + "name": "Page update (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "85411892-1225-46fb-8f1b-5aa424bc2c01", + "last_modified": 1565344753436 + }, + { + "guid": "Shield_My_Searches_ehHQAdRPxE@shieldmysearches.com", + "prefs": [], + "schema": 1565034099452, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1572066", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Shield My Searches" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "74e070ac-891b-4f79-ac76-09a170d21a91", + "last_modified": 1565181472444 + }, + { + "guid": "{4f71ae77-8abe-43df-bb9f-cf440d6f7756}", + "prefs": [], + "schema": 1564993164184, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1571059", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Adblock for Youtube2019" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0f73f6c1-38cf-4255-bb8f-8a61f19afd99", + "last_modified": 1565003019901 + }, + { + "guid": "{f2539eac-e545-475a-85b4-822347022dd8}", + "prefs": [], + "schema": 1564861308712, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1571130", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Woor" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ae64120f-d93a-4224-ac75-4038b0d2a33b", + "last_modified": 1564993145778 + }, + { + "guid": "/^((\\{8842eb7c-5f5c-4d33-aabe-81c27ae87dcc\\})|(web@1A72D794-697C-11E8-9D8C-1C0018C6383E)|(web@2a366bba-0a94-11e8-ba89-0ed5f89f718b)|(web@2bf1a18d-3240-42ad-9f7d-46017edc432c)|(web@2DC2452E-6999-11E8-A1CA-6C6318C6383E)|(web@2DC2452E-6999-11E8-A1CA-6C6318C63000)|(web@3)|(web@3ab4d660-8caf-412a-84eb-9cf2924f54c7)|(web@4)|(web@5)|(web@48F0D4BC-6FB8-11E8-B36A-905EE70C2B9F)|(web@82FCE0DC-836E-11E8-9E9B-164B9D46D017)|(web@90c91611-b4c8-470a-b251-77bb1f859dba)|(web@899ce5cc-06fc-436e-a59e-93a70eb4c810)|(web@3681a37c-6383-4e94-8076-28496af53983)|(web@7398cc9a-684e-40a4-afe1-b620e1a863b2)|(web@7398cc9a-684e-40a4-afe1-b620e1a86000)|(web@8842eb7c-5f5c-4d33-aabe-81c27ae87dcc)|(web@10722358-d56d-4f5e-a89c-d4534d7c5000)|(web@10722358-d56d-4f5e-a89c-d4534d7c5565)|(web@67887931-77b9-4b1b-baee-9f23a4a384de)|(web@a3ea9864-1034-47b9-a25a-e9cc207a9319)|(web@af1e58bc-4ead-11e8-81fc-065ad97f23a5)|(web@af1e58bc-4ead-11e8-81fc-065ad97f2000)|(web@B7CCDA78-8455-11E8-91A4-ED179E46D017)|(web@ca5a2803-6421-4582-97e1-9e30fe44e100)|(web@ca5a2803-6421-4582-97e1-9e30fe44ee00)|(web@ca5a2803-6421-4582-97e1-9e30fe44eee0)|(web@cacf1377-a1b0-43e6-84bc-c0518922b22c)|(web@e7a72615-19b1-42a5-8e34-ddfa89ce006e)|(web@e7a72615-19b1-42a5-8e34-ddfa89ce0060)|(web@e7a72615-19b1-42a5-8e34-ddfa89ced000)|(web@e7a72615-19b1-42a5-8e34-ddfa89cee000)|(web@e4058a8a-59ca-4ba7-b503-dfcf75639305)|(web@f3ac4769-1d70-444e-aa46-06d0427473b9)|(web@FACD83DA-68CC-11E8-8484-3DA118C6383E)|(web@fd9b3d8a-1178-45ab-92a8-a172d0b7c000)|(web@fd9b3d8a-1178-45ab-92a8-a172d0b7c32e)|(web@fd9b3d8a-1178-45ab-92a8-a172d0b7c39e)|(web@ourdom)|(web@oursrchdom)|(web@test)|(web@testss))$/", + "prefs": [], + "schema": 1564669690124, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1570659", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5aa0a7da-cf9e-4bac-bbed-1b7625109cc7", + "last_modified": 1564671502746 + }, + { + "guid": "{97ab6723-bc9a-4c5b-a08b-5b162d29ad4f}", + "prefs": [], + "schema": 1564669434797, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1570385", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Toors" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "30d0ec83-70ac-4a03-9239-7af6c4061cde", + "last_modified": 1564669681391 + }, + { + "guid": "@weatherhubpro", + "prefs": [], + "schema": 1564668992535, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1570380", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "FreeWeather Pro" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "47315374-a775-4701-adc3-d0cfb61a44b8", + "last_modified": 1564669425544 + }, + { + "guid": "YouTube@develop.com", + "prefs": [], + "schema": 1564662247837, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1570377", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Youtube Downloader (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "176a0ef0-bd71-4b96-a3e3-14c17eaad3dd", + "last_modified": 1564668305895 + }, + { + "guid": "/^((nickrr878@gmail\\.com)|(\\{a06de0b3-b00f-472c-a34e-3a74b64d1747\\})|(spar\\.team@spar\\.team\\.com)|(\\{59904ffa-b247-41ea-9ac1-2ce0a2da8c98\\})|(\\{69c49344-90ec-458d-9811-a55878e26bd1\\}))$/", + "prefs": [], + "schema": 1564660970729, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1570622", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Shopping add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "26b10e69-d842-40ef-90ce-f96552e269a3", + "last_modified": 1564661731567 + }, + { + "guid": "/^((web@AppConverter)|(web@SearchManager)|(web@Email2)|(web@DefaultSearch)|(web@AudioConverter)|(web@translations)|(web@Manuals)|(web@HowTo)|(web@Property)|(my@WeatherTab)|(login@EmailAccount)|(web@WebDesign)|(web@Utility)|(web@Ancestry)|(web@Bank)|(web@CrimeReport)|(web@Taxes)|(web@Notepad)|(web@Crypto)|(web@peoplesearch)|(web@Weather)|(web@VideoConverter)|(web@TV)|(web@Transit)|(web@Template)|(web@Sports)|(web@SpeedTest)|(web@Social)|(web@Shopping)|(web@Scrapbook)|(web@Recipes)|(web@Radio)|(web@PhotoEditor)|(web@Photo)|(web@PCTexting)|(web@Packages)|(web@Office)|(web@News)|(web@Movies)|(web@Maps)|(web@Jobs)|(web@Interests)|(web@Games)|(web@Forms)|(web@Flights)|(web@Fitness)|(web@EmailAccount)|(web@Finance)|(web@Email)|(web@eBooks)|(web@DM)|(web@Coupons)|(web@Classifieds)|(web@Calendar)|(web@BrandedTab)|(web@Bible)|(web@Converter))$/", + "prefs": [], + "schema": 1564599359459, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1570620", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b5e2f528-3a9c-43af-9f6d-98040157a59a", + "last_modified": 1564660457769 + }, + { + "guid": "{795697ee-695f-45d4-a997-6fe845b473d5}", + "prefs": [], + "schema": 1564342890085, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1569556", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "IDM integration (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "21bbd8dd-b79e-41b8-bd18-db1592e743c9", + "last_modified": 1564399822770 + }, + { + "guid": "/^((\\{86f185bb-b672-4a24-b3d9-a8751231b687\\})|(\\{8d98cfc9-3757-4fd9-b017-30ad60fb94ed\\})|(\\{288a7d51-4627-44c9-8cf0-cf18742a6f67\\})|(\\{49e5562b-38e2-4292-8080-ca3ffe8cea42\\}))$/", + "prefs": [], + "schema": 1563809747757, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1567876", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "eeaa940b-b984-43da-88ce-d26891347e3d", + "last_modified": 1563810026497 + }, + { + "guid": "/^((\\{e656b354-3c95-4a63-a271-fe301b294da0\\})|(\\{f5ab4224-43ca-4654-b189-23aa9c960803\\})|(\\{fb68ec9f-d02a-48c9-a356-3020bd1d3e21\\})|(\\{2eabd9de-b8bc-43f2-9e77-624a0e04e38d\\})|(\\{426fe5e4-5da1-41b3-81d4-28bd59724f84\\})|(\\{4b1d9906-5cc4-44a6-ad64-a6c7b0e2ebba\\})|(\\{bbe2e2b3-4a77-4108-9183-e0b02676c09d\\})|(\\{01aef979-833c-4f7d-85a0-6be87462c05e\\})|(\\{9b51394e-a1a9-4864-9876-cc1d6f1a47d5\\})|(\\{7c4f0798-6edc-417b-8702-d97ca1c894c6\\})|(\\{ad93f537-5824-4057-a44f-cef1f97c2d68\\})|(\\{ff62fb9d-c5b7-414d-8c2b-bc5d796475e8\\})|(\\{46998928-4162-46e0-b4c7-260a8520aad9\\})|(\\{731367f8-f5e8-4ade-b8cf-5aaf8c2a455b\\})|(\\{eb6b098d-7811-4a20-a94f-ca91721d4aab\\})|(\\{6ddb9deb-d435-4ec2-be8b-ca65900e43e9\\})|(\\{288e4bb9-454e-4374-8734-1069241d618e\\}))$/", + "prefs": [], + "schema": 1563306101159, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1566026", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons collecting ancillary data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1f35915e-2de0-4973-adf4-4e77073e0b42", + "last_modified": 1563367115113 + }, + { + "guid": "/^((\\{7e9c2e2c-8d2d-4406-bb3d-993176f59b34\\})|(\\{f743e1bf-9a1c-43ab-b8ab-57972761b919\\})|(\\{b8274b35-eeaf-4d98-8a6a-cc4fd56603ed\\})|(\\{fd90393d-6ac6-4245-a048-9d423baacbd1\\})|(\\{3f7b376e-23ad-4296-8fd1-77fb254610bc\\})|(\\{fd918017-a23c-40f0-88e1-798ed6fc51f7\\})|(\\{bac48192-5c1f-4dae-aa1c-2fa9ca65dbe0\\}))$/", + "prefs": [], + "schema": 1563284869997, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1566557", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Add-ons overriding search settings" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "59cf2549-3930-4a60-a212-af84facbee32", + "last_modified": 1563302090004 + }, + { + "guid": "/^((general@mapseasy\\.net)|(\\{fd4b0f58-5268-4d52-b09b-547e867fef27\\})|(\\{efe13936-3bfa-44ce-9224-e8cb27f0f216\\})|(\\{c6b767a7-b0c1-4b68-8ac6-d08fee14c5a0\\})|(\\{a9df2ef3-9746-4d5e-b7aa-0baa531538d2\\})|(\\{2d947dd9-fa0f-4f20-8812-31ce9e29081b\\})|(\\{3e800ada-345b-4ac4-82a5-0dbfd00d7877\\})|(\\{a5fcf820-575a-42c2-aeb1-de1a794db1b9\\})|(\\{da844d4c-44e1-4799-ac53-9bb1ad2c8227\\})|(private-tab@mozilla\\.xpi)|(\\{56dbb679-8db4-47ce-851b-a7eab0e215cc\\})|(\\{c1415289-4471-4b6a-b7b7-4feaf9506b38\\})|(\\{29439407-f908-4779-9078-eb190f21dc4f\\})|(\\{5401ea8b-def8-4df3-bf9a-f520b147df69\\})|(newtab-plugin@mozilla\\.xpi))$/", + "prefs": [], + "schema": 1563133289502, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1565994", + "why": "These add-ons violate Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Search hijacking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c349ecdc-5fa8-4b87-bfa2-c732dfde094d", + "last_modified": 1563186751969 + }, + { + "guid": "/^((langpacasdjasdk-de@firefox\\.mozilla\\.org)|(langpack-de-7@niklasb)|(langpack-de-8@niklasb)|(langpack-de-9@niklasb)|(langpack-de-10@niklasb)|(langpack-de-nightly-1@firefox\\.mozilla\\.org))$/", + "prefs": [], + "schema": 1562673203781, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1564401", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "German language pack (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d38035ff-7945-49cc-88fb-6de9f8a13658", + "last_modified": 1562673628501 + }, + { + "guid": "/^((\\{6d0887bd-1f51-4852-9307-cf2a223c3a9a\\})|(\\{4cb2b177-e2a2-4ae1-b759-03606a85df61\\})|(\\{df28b68e-ad39-4300-add2-b2dc8bf54f71\\})|(\\{eff0a5d5-3f53-4b75-b451-5acffd5bb61d\\})|(\\{7561d328-6b59-449a-bf7a-46fea5027be4\\})|(\\{854246c0-f678-44ff-85f3-f340f33c5da9\\})|(\\{9e976e36-9eb7-44bd-97a3-4901ab3e1021\\})|(\\{2e9a0a60-8423-4ffc-89ef-74a02ca8c5e8\\})|(\\{a5e298c3-2d74-4268-8d13-e0efcb77d896\\})|(\\{39838189-7836-432f-9a34-a009886a61f8\\})|(\\{a3f781a4-adc7-4a12-9812-20da06e7b6d9\\})|(\\{365b3845-1e12-4096-80f1-8be24456d741\\})|(\\{701511ef-2e5a-458f-b735-c789b7ae6feb\\})|(\\{888807df-4517-4b97-ac73-e4294865e375\\})|(\\{ea7c7094-9d83-416d-bd13-e85fcef481a5\\})|(\\{1f67e4bd-6eb6-4fd7-a694-b8b360494cc3\\})|(\\{4f9520a5-caa6-4832-9582-2b26b8739305\\})|(\\{06f5112b-da03-481b-bef1-bf752ddbe7a2\\}))$/", + "prefs": [], + "schema": 1562269304530, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1563454", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Fake Flash Players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f8e35175-572a-4b00-b5d3-9c8df255f1ec", + "last_modified": 1562320360370 + }, + { + "guid": "/^((\\{fc2dbae0-1ab3-40fe-9a38-cbee911b903e\\})|(\\{82e3e07c-0cf3-4b9f-a625-65d378698af3\\})|(\\{2904a7ee-0b54-4adb-a500-b932b77b6936\\})|(\\{f6932562-1f6d-4779-b5a4-f4c4654980dd\\})|(\\{97f0cc0f-69cf-4e5a-af68-f404aa3f1ce5\\})|(\\{c346f3ce-08bc-44b7-9410-bd8ad65f32d5\\})|(\\{f68f3bd2-ece8-4a85-b071-cea253cd78ac\\})|(\\{d9b6a925-e00c-4e36-a282-e6b76833e5b5\\})|(\\{e8a67ebd-655e-48f9-99e7-619c850f6bcc\\})|(\\{15942451-8c62-457f-8ff1-8525ce647c0d\\})|(\\{8fcdb966-eec2-4cd0-865e-1d105e9b59e2\\})|(\\{6ae58312-7d81-4d39-84f1-454ae6ace826\\})|(\\{023cd859-1b7c-4384-80e5-eca82c68a21c\\})|(\\{694e912f-011f-4be4-add9-25c85af8014d\\})|(\\{b889539e-4b8c-4e52-a605-a0b33532fd05\\})|(\\{596c6d7c-49ab-440e-a50d-220e5db393f7\\})|(\\{f0fd5c9c-1fcd-4085-ac41-47c379517420\\})|(\\{545dbede-b51c-477b-b23b-936dd5e7a428\\})|(\\{c3f9f4e6-b5ed-49fb-82c6-313b9617cddb\\})|(\\{309908bd-c2fe-4066-82c5-0631571e77fc\\})|(\\{75f93037-6366-4f88-b92b-c3174d68a836\\})|(\\{36e57809-88bb-47ef-9b6c-90170bb753d5\\})|(\\{da83ef9e-f36a-4416-a4ab-29a09c981690\\})|(\\{09e50933-f19f-435a-8e6a-7663715ea3fc\\})|(\\{7f35f6f3-714d-4c0d-befa-5a6843c62b6f\\})|(\\{b3b3f9c6-6b64-47d0-bf5a-f9796d1d7cfc\\})|(\\{116c6521-bda8-469c-9ca6-0702860aca67\\})|(\\{98566ed5-4c57-4da4-946f-03beeaa6145b\\})|(\\{8fc257d4-612c-4196-9688-dd7de0979c44\\})|(\\{9b7f873c-cf3f-4d8d-9cd2-3c68ce0f831f\\}))$/", + "prefs": [], + "schema": 1562096490180, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1563454", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Fake Flash Players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7f55f27d-37e5-4c3e-bd9d-02924496ba3d", + "last_modified": 1562227635293 + }, + { + "guid": "/^((\\{a3250b1a-de19-432b-829f-3d4c18ff4549\\})|(\\{b315912c-03f9-4ca8-a9fd-9410786d242a\\})|(\\{d1e4d5e2-9c6d-4e1d-9638-d329d64e5484\\})|(\\{898e64b3-38b0-4748-93a2-7e68874a73c9\\})|(\\{062f9d2e-1e5d-47e1-a9e6-0a4eb1e8182c\\})|(\\{434cedce-3d36-4ec1-b99d-e2b5ec929e8f\\})|(\\{b429f2f0-62de-4c72-9722-9ccbcc43500c\\})|(\\{7405ed28-67e9-4836-9c38-26bb7175da3b\\})|(\\{ebb5fe5c-5561-47e8-8240-bae4ba4b0389\\})|(\\{a9a5ac46-ade8-4927-85db-5c36bb26fd2c\\})|(\\{34d93cc4-a468-4ddd-98a4-31d1237b9986\\})|(\\{394a7da5-0f59-425c-ae83-49ad47c30a51\\})|(\\{15532ccb-c575-4b8b-9a62-ca2e4b9cbd7d\\})|(\\{d0579f20-4e35-4d7e-be6a-3da1b7660ce7\\})|(\\{f39c789b-9a2f-477e-885e-675f499e8307\\})|(\\{7adb91a0-6c6b-4fc6-911c-63a0c10bb363\\})|(\\{8b51d36c-f5d2-4c9d-b431-2c5011168470\\}))$/", + "prefs": [], + "schema": 1562061057131, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1562598", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various Fake Flash Players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "432368eb-333d-4280-846d-e9508ac0a8c4", + "last_modified": 1562061250280 + }, + { + "guid": "/^((chromelogger@usernamewilson\\.com)|(firefoxsecurity@usernamewilson\\.com))$/", + "prefs": [], + "schema": 1562001599116, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1562597", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fire Security" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c4cd1e9e-b81f-40ef-8c0d-86b42d884a7e", + "last_modified": 1562061057121 + }, + { + "guid": "/^((\\{02421745-5b6e-45e3-925c-670b72162fa5\\})|(\\{165626db-f5c5-4e96-b7c5-dafaed6357f4\\})|(\\{87115bc9-2e52-455a-b82d-2753d3303d0c\\})|(\\{be0863e2-c325-48cf-9623-88bcc6d01970\\})|(\\{09298d3c-f8f1-401d-bd70-43cb66af1594\\}))$/", + "prefs": [], + "schema": 1561988438120, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1562634", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various Keyloggers" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3031bfa1-90c6-4b7a-ba03-7fcd7e010914", + "last_modified": 1561988914045 + }, + { + "guid": "/^((\\{fd605874-13ff-4b86-8018-9167923792c9\\})|(\\{687ac7de-0c7c-4e5d-9fac-10757591026d\\})|(\\{c9c11fd7-6774-4657-afe4-0cbf1081b505\\})|(\\{5970609b-547d-4094-b806-a9b0a2996c62\\})|(\\{f77d7ff3-05df-4455-aa23-8031dd341ec0\\})|(\\{d5238802-9ff3-49fd-a427-8eb7a73a08e1\\})|(\\{93a425f5-cc32-42bb-ab4d-63b53512cb52\\})|(\\{23de42fd-b546-4792-ae2f-e2f808e13b52\\})|(\\{209b8c63-d260-4fc7-be92-20e3b04eb944\\})|(\\{e6610c1e-4bd2-4ced-9d40-0a4fc125fd42\\})|(\\{6631ea49-4664-4acc-97dc-0d7e4a67a417\\})|(\\{b811cceb-fcd6-4eb6-b07b-c8c01794eaad\\})|(\\{8ad9f113-7b59-4c45-9c90-59609e7ee91a\\})|(\\{130f1820-9c73-439d-a9c8-114702e1d415\\})|(\\{6dd75c5f-407d-4f3b-9c29-90c422c82d85\\})|(\\{99968b5b-6b54-42e8-932f-0414bfd17405\\})|(\\{1a69e311-e730-48af-b050-f60247a32fe9\\})|(\\{67f715b0-1262-4660-9fa2-c6f7bc29ecc9\\})|(\\{3a8513a4-cac1-4619-ae36-9686bcbb42a2\\})|(\\{27af1be9-f9d9-40d7-878c-78d8bcd94dd5\\})|(\\{7b59b760-964a-43af-83b6-b4ef08fd2e07\\})|(\\{861bd389-6024-4aae-a31b-f334163c1406\\})|(\\{cdcdbb24-88fb-42f8-872c-3df116f82cad\\})|(\\{8c60aa85-e195-4203-8529-073a6db56c54\\})|(\\{c5781fde-fe4f-4792-9e01-4e1239c00b08\\})|(\\{1361b734-0efa-4d8d-8108-ead9400b50d8\\})|(\\{ce79c320-0b55-430a-a431-2cd69cc2919e\\})|(\\{cb976837-ff98-4f37-b81b-98571f3a7828\\})|(\\{21dd7ebb-6736-470a-afe0-5dcea38b3db7\\})|(\\{84b09a06-0f25-40ff-8198-98e2e54a739b\\})|(\\{b869d674-37ba-4df4-a34d-dc9be47a963d\\})|(\\{e18783e4-8e04-457e-9b35-611d20fc12e8\\})|(\\{04042b52-3398-4e42-a638-ef9200c589f4\\})|(\\{96292080-d058-458d-a6c6-bff1d52425a2\\})|(\\{aa584391-2c81-4dfb-b06e-5118b026104e\\})|(\\{6285eae8-ff36-4887-b46c-3772ec04390f\\})|(\\{ea28f6ac-affc-45cb-a536-1eb1bceae142\\})|(\\{340afda6-c1e6-4ac4-8e5f-a5b439a9dfa1\\})|(\\{62202f8d-8e11-4ce0-af66-2235ad17051c\\})|(\\{d4e05835-d503-4d21-9701-a24dd61b0513\\})|(\\{3e8d4f85-d823-4dac-84fd-4b87b33e7852\\})|(\\{c04dbf0d-9b39-4730-93fa-6680aa0909f1\\})|(\\{019be100-cda8-43ae-8136-a014db9e7a9f\\})|(\\{73c512b3-a9c9-4690-bba5-fbfb94aeabd7\\})|(\\{0c78c766-7ebb-4b2b-85dc-d4fcd4da6e9e\\})|(\\{a318cd63-2e84-4d05-96b8-13b721fdec8c\\})|(\\{97d03ad5-3911-4c72-8ff9-5f4b9beade68\\})|(\\{77c5010c-c799-469c-85b8-5e7e0140fb10\\})|(\\{16458eb7-545e-4626-8620-e31d71cfbcc3\\})|(\\{a5a61ddf-c248-4109-a1ba-b8bf84e728c3\\})|(\\{6b18c850-65e3-4cc8-b3f6-e78969c9a428\\})|(\\{4ae632f1-6735-4077-8b62-f73d68eab36c\\})|(\\{db7b2525-ccd1-4ee6-8daa-890d7879ed07\\})|(\\{9e382691-320d-4500-b378-90c9ad922422\\})|(\\{1bf86700-b428-4e67-9701-536f66ec0a2c\\})|(\\{d6841f20-fd15-4373-944d-a0dd6a286d69\\})|(\\{18169628-a7de-4c93-9d30-efb66f45b5a4\\})|(\\{d9979d00-eb14-4cb3-bb21-452d2c02e3dd\\})|(\\{cb868690-a1dc-4fe0-bd2e-2ab291cd54a4\\})|(\\{230faf82-9048-43e9-ab19-94bfe1113ad9\\})|(\\{e0723460-a1a2-4877-bb20-a3f14e01e594\\})|(\\{2431dfee-a855-46b4-a740-6d0e4dbaa662\\})|(\\{1d44053a-5ff6-494c-8fef-0084039eb8ac\\})|(\\{bc77d204-ecd7-42ee-8006-cd54be0a400e\\})|(\\{9e22fccc-6b4a-4674-87f2-e6ecd4b409d6\\})|(\\{62474e9a-8a3a-41cc-9530-97baf6f8b7af\\})|(\\{6932ebe8-ed27-4291-86fd-d5147e0f5702\\})|(\\{aa5b9279-e324-4951-8a12-b0712f37e233\\})|(\\{420e769f-b577-420d-bf07-182299d75882\\})|(\\{11a0fbd7-b2c9-427a-8d33-e7fd8e845630\\})|(\\{a4dfd321-45e6-42dc-9ac8-9d606ad4a672\\})|(\\{54685c20-9401-48d7-a950-81e7b10bd9ed\\})|(\\{2605a51d-53d1-4345-9fbc-380fab2a0c4e\\})|(\\{c3699f84-dacd-4734-a60c-7ecb1b28289a\\})|(\\{074b0ddb-5187-484b-9783-22d187e6dd08\\}))$/", + "prefs": [], + "schema": 1561923700391, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1562630", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "56849c09-6253-44ed-bc59-1cd7a61ce961", + "last_modified": 1561988438110 + }, + { + "guid": "tab-api@mozilla.xpi", + "prefs": [], + "schema": 1561710153929, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1562154", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Tab API" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9f484302-44da-4b6a-afd8-94113b83c0f6", + "last_modified": 1561710520557 + }, + { + "guid": "/^((\\{b7a0ecf9-212b-49ca-bec1-ead0c6dc2128\\})|(\\{6e977a6d-b31d-4735-a586-41dc25df2100\\})|(\\{67155a2a-6538-42b1-bdc9-f48b442f57e7\\})|(\\{b4d4abc0-5e6e-4a34-a7e3-bfe7319160b8\\})|(\\{2102c5a9-f3c4-4f71-bb6e-c45c9d07b6c8\\})|(\\{071c1c7a-cde3-4a22-aefe-7c8f98df6277\\})|(\\{aa2f3e70-7dcf-4b4e-92c5-54c8280b98de\\})|(\\{3b376420-b276-4a0c-9e28-340dcc0547ce\\})|(\\{ed192371-abcc-4520-ab76-d22afbe51dff\\})|(\\{ad5a457f-59c8-4d90-8e3e-59f13a3bc2b2\\})|(\\{06aa60ab-91ad-4b8a-bfda-98e33b65fbb5\\})|(\\{c2875a12-da6a-4f90-a919-1d2bef57fbff\\})|(\\{b01d1c5b-58b5-4411-86d0-555131c7bd07\\})|(\\{0a79c7eb-5fe9-4e37-841e-18686bc86a20\\})|(\\{341ca205-d6e0-4d03-93be-04939c429412\\})|(\\{855e09d9-ac3a-4885-828d-557734060c1f\\})|(\\{8ac01eb1-9819-4c41-b2b7-042d8cdb3f2e\\}))$/", + "prefs": [], + "schema": 1561657651090, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1562153", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Flash Player Clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3460b6b7-8251-4588-8f12-81ac8d288c88", + "last_modified": 1561710153920 + }, + { + "guid": "/^((\\{65b88db7-9c07-4d03-80eb-2e5cf6cd7aa8\\})|(\\{aa2ef90f-db17-4ece-abab-4f87830457db\\})|(\\{e50969c9-088c-4978-9ffb-5d78015dabcc\\})|(\\{15fd1a8e-db53-41fa-9c46-93ec5b7473c1\\})|(\\{ed84b63e-faa2-4c48-b080-e9612cbc2e49\\})|(\\{c784f63e-5609-47a8-92ee-33a2bcb3239b\\})|(\\{1641b1ec-9a3d-4e3c-b52e-bc80671349f9\\}))$/", + "prefs": [], + "schema": 1561587664411, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1561854", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Flash Players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3e88dad8-f640-46dd-8b00-4b955eea7b24", + "last_modified": 1561626172388 + }, + { + "guid": "/^((\\{f0df6aa3-9792-4476-daa6-4709f93bbce3\\})|(\\{fe934134-3d0f-462b-d56e-e7187dfa8c98\\})|(\\{429999c4-1b8b-46fb-863f-ce19a08afc9c\\})|(\\{b8003074-2123-45be-91cf-654ef9671e1a\\})|(\\{9712066a-d491-4293-cd31-8ef8ee907d40\\})|(\\{dcfbb98b-783b-4df0-8427-e269114736cb\\})|(\\{66c44e3b-2df2-4741-ff07-0067cca4fe95\\})|(\\{af0a4d96-3403-496f-9d9a-5c766bf44bac\\})|(\\{82c60958-45da-4e6a-de21-879775c5473a\\})|(\\{c9118234-5787-488d-b30c-7d0a904fbabb\\})|(\\{f07d3da6-81ea-464f-9bef-6ff5470b307b\\})|(\\{c2454a12-7f57-440e-f695-0a9618f48b80\\})|(\\{f6e1d884-8100-49e7-88b9-bff8d9295cd2\\}))$/", + "prefs": [], + "schema": 1561552773231, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1561603", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Various script injection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0c227983-1180-4b4a-b25b-8160738e7238", + "last_modified": 1561554563187 + }, + { + "guid": "/^((\\{4200b565-5c4a-410f-b4fb-478daae2537f\\})|(\\{a0bba103-21d5-49c8-96f3-4eabbe78ced3\\})|(\\{ec46fe21-5690-4757-8ebc-1c77f826fe6b\\})|(\\{ce45d605-3bb6-4fad-8c1b-238ecee0d3df\\})|(\\{c70bd1fe-1d7d-4ae5-a731-3d513e6c46ba\\})|(\\{aeec96ca-81b9-405c-bd70-01ea6a50be9d\\})|(\\{0a1603a8-839f-4890-b1e3-1b8e00a7a0c9\\})|(\\{45febc8f-eaeb-4cec-90ea-07a7edc22793\\})|(\\{a7c7febd-6726-4d0e-9947-2edfd8bea35a\\})|(\\{eda3389e-ae07-4a2c-9b50-ce4e9008f297\\})|(\\{0e5d1d65-4fbb-4dd9-9042-3b568d9e2489\\})|(\\{1461f0e5-3c4a-453e-aed2-ca45ff5db747\\})|(\\{e842e73d-9d8a-45a8-bf0d-ef974ab24767\\})|(\\{e1d4fa8a-3da0-4fee-8b4f-0c7233fcb49a\\}))$/", + "prefs": [], + "schema": 1561541784349, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1561595", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Flash Player fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2e886e0c-00ab-44c2-bcbe-7c8793d46d89", + "last_modified": 1561551163572 + }, + { + "guid": "/^((\\{0eddfecf-eb8a-4a08-8189-004932a77d5b\\})|(\\{b8181b05-a263-410c-8c07-5d7e8c80f9a2\\})|(\\{561d3a53-8e1f-417c-9b46-af1ea9942c4d\\})|(\\{cae0f640-a4b8-4ea0-8667-39ec00651b54\\})|(\\{142aae9b-ff6a-4ae3-b4c4-75e99534e661\\})|(\\{0592cc75-3027-420c-9a9c-22b23a21af5b\\})|(\\{23f8f54b-1f6a-4760-bd9a-414aba8d93c4\\})|(\\{09d0cd99-4cde-42d2-9a4e-8002f7595834\\}))$/", + "prefs": [], + "schema": 1561380181925, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1560886", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "ADB amazon" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "dba9e10e-ff77-45dd-a2fc-e7daf5f09fd5", + "last_modified": 1561381085652 + }, + { + "guid": "/^((\\{99a325df-ca23-41da-84ce-477796f46737\\})|(\\{86a9b963-ebd2-4ef5-abaf-5ffd7cf86387\\})|(\\{9c05da6f-e5e2-4455-bae3-2754e1b36db4\\})|(\\{002d0276-03dc-439b-bea4-576976bbd7c4\\})|(\\{4476484a-2a0a-4a7a-8612-18ac22e02ac9\\})|(\\{edb28501-65bc-49f1-b168-1ea5e84d4a19\\})|(\\{13b0c2d6-0a44-47e0-aced-0664877b8a4e\\})|(\\{fdd30cab-1f29-439b-829d-80c5546087e0\\})|(\\{2abed6fc-d8ac-479f-8fdb-f5d20b0a5c27\\})|(\\{7afb7eb5-7837-474f-a925-62728be18488\\})|(\\{ff86f12d-e38b-4c70-ab00-9cd20174ddcd\\})|(\\{31b72d81-14d8-40e5-a2c2-7259a7d40d96\\})|(\\{84e3002a-a0ea-42f8-b30c-1739cb21b105\\})|(\\{47d8027c-a331-4f8b-8c69-4c95680caba5\\})|(\\{d03ae30a-58b5-4dc1-afd9-bc4ea8efc761\\})|(\\{eb745394-234d-48b4-bf1e-cdec66de26d3\\})|(\\{b5614c0a-878a-412f-ad7b-bc5a7916b3bf\\})|(\\{1d5916f7-3a78-40ce-92a0-35989646fe8d\\})|(\\{34cfd020-1dce-4f12-9499-f7e3b02582d0\\})|(\\{7fbdd1c1-82a4-4cfc-a3c2-ad192f5c8cf8\\})|(\\{04fbbb51-3b76-484a-99ca-ccd3e484da26\\}))$/", + "prefs": [], + "schema": 1561379624735, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1560888", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Flash Players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5525a8a4-2b7f-4cb8-8932-5daf35a2acc9", + "last_modified": 1561380181911 + }, + { + "guid": "/^((superzoom-unlisted@funnerapps\\.com)|(superzoom-unlisted-test@funnerapps\\.com)|(superzoom-hosted@funnerapps\\.com))$/", + "prefs": [], + "schema": 1561260352539, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1560927", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "SuperZoom" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a7a12a25-37c2-4ec7-8fa2-e016442b4457", + "last_modified": 1561379624719 + }, + { + "guid": "/^((\\{dfc97392-12f9-474c-9a61-fba12334d09f\\})|(\\{ca0e7543-892d-4625-8565-3671141a4ac4\\})|(\\{d50b1b61-8ba1-471b-833e-c5526ea4f307\\})|(\\{ae53880e-f5b2-4020-bdb5-0872cc1196af\\})|(\\{cb8982c0-1f56-439e-94a0-83a0308ea952\\})|(\\{91ca701d-73e3-4d76-80bc-4cbefc16beae\\})|(\\{f238a031-3366-4792-89b0-736f5e1af888\\})|(\\{35f7fa51-338b-4a7c-9f74-055708e2c941\\}))$/", + "prefs": [], + "schema": 1561232502012, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1560614", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More fake Flash players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9a6e32f3-3d97-4393-8733-1a24b8796aa3", + "last_modified": 1561246819959 + }, + { + "guid": "/^((\\{eb585608-4896-4892-8a9b-1786929a6517\\})|(\\{b53cec3a-0479-4887-93b2-4732059e6f95\\})|(\\{52e84405-484c-4bfb-a279-da57dc8f89d4\\})|(\\{65136894-4082-4eac-a968-ad5dfd1771f8\\})|(\\{98d5570d-3606-40f8-8af2-2d2144698a92\\})|(\\{5ced4e6d-5c50-4e26-9d8b-b82a8d9b4e87\\})|(\\{fc97e534-4690-462b-8227-af38c67b57e2\\})|(\\{8aa21cc1-d9ac-4c43-b205-bfbd87aa8163\\})|(\\{f3dd0563-975b-475b-b295-2922f6e69717\\})|(\\{b1ee58e1-90de-425b-ab76-dd8e034305c8\\})|(\\{ae026b4b-7159-4415-98d4-1fb712092028\\})|(\\{452ec3ac-8fba-4de8-a5dd-db1cf9b89c35\\})|(\\{4c0a3c25-f706-4160-8128-197137e814b8\\})|(\\{30c347c6-642c-46cf-899d-442632d90e25\\})|(\\{7dd79020-d8d4-4241-b1fe-efca66530d5d\\}))$/", + "prefs": [], + "schema": 1561059702370, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1560614", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More fake Flash players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "61f4423b-7171-4a82-8aae-71621233ee2a", + "last_modified": 1561148779474 + }, + { + "guid": "/^((\\{2adb8263-d69a-4436-a46e-b8595c35d3e4\\})|(\\{d2d96579-be7c-4bc0-9d94-7eb8920ed437\\})|(\\{3ef03e82-ae5c-4bb4-85f2-d0925d3b1d8f\\})|(\\{5f89e2ff-b113-45a7-bd44-a8cbb6b3e18e\\})|(\\{d0a65809-897d-40af-974c-df6baa16e0d6\\})|(\\{17d71711-a428-4231-93f7-25f65dc7a05f\\})|(\\{88bf0d44-b815-4654-b177-25ed224587de\\})|(\\{0bc2f2bb-b040-4512-b0bf-a2a875bfbcc7\\})|(\\{c05607b1-ce5d-4a14-ac71-91d93e5adc7c\\})|(\\{14d1d1c6-3982-4379-bf48-67aff16b0b40\\})|(\\{db395935-5b36-43e0-9c21-2fc546e3504b\\})|(\\{031a0658-d9a4-4dab-8b9a-3608a92d3d9f\\})|(\\{28a6ee49-33dd-4cc1-8650-7abc95ff30c1\\})|(\\{578c5cb1-ed39-4270-9010-3d7c623e4ed3\\}))$/", + "prefs": [], + "schema": 1560890279256, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1560126", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More fake flash players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "39d92b03-d7b7-41d3-8d14-73088972f735", + "last_modified": 1560959139660 + }, + { + "guid": "/^((\\{52e29477-bb59-452a-a929-7d238ab68dc8\\})|(\\{e7c3e8d7-0cd7-4cea-8fe6-afd0dda61f56\\})|(\\{f57df33b-b222-4524-86c3-531a6d20b4c2\\})|(\\{5bfc5ee1-d8de-4efd-80f5-966b94eec12b\\})|(\\{ed229f56-afbb-48e5-8422-2ad940afa02f\\})|(\\{c87d1f11-ce0e-46eb-8710-1288416b709b\\})|(\\{177b00c2-4fb2-4268-b0c7-cb5a1ad08d83\\})|(\\{33850c97-5260-409e-9796-bd9e03aeb411\\}))$/", + "prefs": [], + "schema": 1560874921856, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1559787", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b44844c7-0ba9-477c-b0f3-bd12725c620e", + "last_modified": 1560875340234 + }, + { + "guid": "/^((\\{004b2982-0956-4c66-a7f3-7dba26eeda94\\})|(\\{31606ee8-6af1-4d3e-98d0-451b8483a498\\})|(\\{914f7369-d844-4e72-a8de-043378710864\\})|(\\{87506f0b-af77-4113-8358-fbb0a9f6daa7\\})|(\\{74832b41-91e6-4bf0-a6a9-5e74bf3e5683\\})|(\\{94e526a9-70ec-4566-995c-53e597166c8c\\})|(\\{1679b342-31d5-44b2-ae2b-91c487b2654f\\})|(\\{ead96242-a6c8-4478-88c5-5e2c54d9ace1\\})|(\\{ab0e69c5-d215-4825-8e40-de0bcae97da9\\})|(\\{2607b07a-90e6-4c0e-9bd8-94eb16982303\\})|(\\{c8336a7e-f5ef-41d8-9754-31676cb4f6c4\\})|(\\{49185403-71d8-40ed-9e30-71171231a2c0\\})|(\\{724540be-a261-4d92-bee5-ede7c6375ed6\\})|(\\{6b687abb-9aa2-4e76-bdc9-cb542809cf7a\\})|(\\{faec57e2-f33f-4974-b29c-3afc2d710ae5\\})|(\\{2dc254ac-f312-4db3-84b2-29690e20ce4d\\})|(\\{c60eb214-f702-48fd-b173-756b528cee4b\\})|(\\{6cf50082-5b79-400b-846a-8902d6609a37\\})|(\\{a45c880d-5037-4428-9e1c-ec1cd45fe830\\})|(\\{d410777f-d023-44d5-bdb8-a54b0c927daa\\})|(\\{36fbd9e9-1d2d-411f-981d-b57fbc1067db\\})|(\\{170130b4-3178-4dc2-a1f6-98a788299b16\\})|(\\{aa54e92d-20ad-4f3f-a0c7-95a97bd5e99d\\})|(\\{6e73d781-bce5-40e0-a847-63a936f58ca6\\})|(\\{861af4ed-838b-4e5f-94c7-0e95bc6b709b\\})|(\\{3e000c1f-3ad5-455c-9a20-f18035273746\\})|(\\{aeacd5ad-8949-46a0-96b0-96c9f93f0b8b\\})|(\\{c7f65d43-1a36-4683-864f-c7224037289e\\}))$/", + "prefs": [], + "schema": 1560874615935, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1559776", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Norton add-ons and clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7434a6e0-c434-4017-b475-625bd0f85890", + "last_modified": 1560874921842 + }, + { + "guid": "/^((\\{c3ff7a71-7392-4aa1-a193-95fd393a7389\\})|(\\{9255568f-d790-4b45-9fe7-d4d1bcc193cd\\})|(\\{76d7025a-8c31-4b0b-a9de-c6679919ef81\\})|(\\{6b93c35f-74c4-4d79-8557-b3fcb090049a\\})|(\\{f9dbfda2-5680-43f1-9575-5cb044264f7c\\})|(\\{096e84e1-36c8-48fe-b642-03c91c1ef14f\\})|(\\{a11e0be8-9b45-4d69-9aad-339d3220147c\\})|(\\{fb754f2e-c021-4190-96fd-7142cbcb985e\\})|(\\{69187792-e951-4ea9-ad26-378f25efee81\\})|(\\{6468fac9-e37b-43e9-9895-36143902e431\\})|(\\{ff1db01d-4e0d-4917-b487-7f3c28d7f5da\\})|(\\{1adb7040-3a78-4270-b4d3-b926819d4c72\\})|(\\{9b3d09a1-2134-48e3-bf0c-a6dc659aad93\\})|(\\{bfa38150-f24e-4443-9d07-875b21ff479e\\})|(\\{05c8e9b0-0b8a-4da2-9e44-a215e691302c\\})|(\\{e4868162-b7f2-40dc-9101-4eab9858876b\\})|(\\{541118b3-1905-4d4a-9059-3ac745b0b043\\})|(\\{d38507bd-22ee-4839-be07-cae4806ac227\\})|(\\{dbef35f1-fc95-42e3-a4a6-b94a970b8a7a\\})|(\\{f34d7289-64c7-4720-90e9-6a6cad0ddc9c\\})|(\\{96b900d3-784a-4e93-8b9f-5f7885424117\\})|(\\{a6ccaf93-4d0a-4bcd-b574-b6d1417bdb0d\\})|(\\{186d942f-cc7f-4054-9673-067f9aaae190\\})|(\\{f7c0f615-d406-4cf8-b5de-bde347f7d9f9\\})|(\\{ddf49e42-2db2-448f-9717-96a93bdb078d\\})|(\\{add62eb4-d1c1-4217-920c-dfb462e955aa\\})|(\\{71ef4372-6321-4e99-937a-0a4a03476348\\})|(\\{053307b4-d841-4d42-8fda-881aa7f7777d\\})|(\\{458e497b-8e5c-4901-82be-1e33832bdac2\\})|(\\{3bf07b01-bc56-4b28-acb4-7d56bb6f5fc8\\})|(\\{7e50978d-cf4e-429b-8482-946c86991bfd\\})|(\\{7691a931-7d5e-4daf-ad20-14539572c215\\})|(\\{c1d8d622-aa7b-4e36-9d5e-e1de1b1044da\\})|(\\{f1b85cd5-bc61-4994-96fb-74df9c62d385\\})|(\\{e26cab94-6216-47e7-b725-948613f2a08c\\})|(\\{2278a05e-3b98-4fe4-83f3-f90d42ce0870\\})|(\\{85240094-c94c-40b7-84ac-6dcf1d50cee5\\})|(\\{7e5f5b7a-ef9d-41a8-8137-da6399afdd5d\\})|(\\{85d01efb-0331-4f7c-9ef2-f5f35c0df0a9\\})|(\\{f455923d-856d-4f2e-8e1b-8cffb0b7a4a4\\})|(\\{cd4fab1b-03b4-42eb-9800-1664c4de06c2\\})|(\\{44ad1c10-baa5-4efe-96bf-743d6a86079e\\})|(\\{73cf3b22-f479-495e-8bab-54ca07e3341f\\})|(\\{c4e856fe-5594-4484-9463-a139eb6071e5\\})|(\\{6f110a84-aa60-4849-8408-9ee70c868e8e\\})|(\\{67d91fb9-3da3-481c-a426-a350788764f2\\})|(\\{eb7699e3-9886-40d5-863e-0bf6862f2f98\\})|(\\{e18709ec-f4d2-40de-8e88-ef7a6f1d4fef\\})|(\\{f83cd650-3411-454d-aadd-79bbd82f3793\\})|(\\{edb44a75-f988-4ce7-ad83-1b7cfe3da54b\\})|(\\{b328a40e-52a7-48df-8960-8f79927bfd35\\})|(\\{f8681a1c-062e-4934-a1ff-6479f179aa97\\})|(\\{f052ef52-7b0e-4a99-9490-892b515d7ace\\}))$/", + "prefs": [], + "schema": 1560714091988, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1559772", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Adobe Flash and clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "34b4e88a-ca61-48fc-a6c7-7e08aedf7887", + "last_modified": 1560874615921 + }, + { + "guid": "/^((\\{c5ab9361-f8d8-454a-b268-a4b355b37a83\\})|(\\{f7bbdf7b-7f4e-4319-b9c6-ba62f38e1d5c\\})|(\\{97154c2a-2a3d-4ddb-814d-5451e3c35103\\})|(\\{1665bb10-e8d5-44ea-8cd3-531b6ebfaef9\\})|(\\{f3b474a6-b76c-4a9e-ae57-df0a3992d8f1\\}))$/", + "prefs": [], + "schema": 1560430131572, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1559330", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "436f1b52-16b8-4c70-b67d-ad6c39f6bd39", + "last_modified": 1560502059138 + }, + { + "guid": "/^((de\\.firefoxextension12345@asdf\\.pl)|(deex1@de\\.com)|(esex1@ese\\.com)|(estrellach@protonmail\\.com)|(fifi312@protonmail\\.com)|(finex1@fin\\.com)|(firefoxextension123@asdf\\.pl)|(firefoxextension1234@asdf\\.pl)|(firefoxextension12345@asdf\\.pl)|(firefoxextension123456@asdf\\.pl)|(frexff1@frexff1\\.com)|(frexff2@frexff2\\.com)|(frexff3@frexff3\\.com)|(ind@niepodam\\.pl)|(jacob4311@protonmail\\.com)|(javonnu144@protonmail\\.com)|(keellon33-ff@protonmail\\.com)|(keellon33@protonmail\\.com)|(masetoo4113@protonmail\\.com)|(mikecosenti11@protonmail\\.com)|(paigecho@protonmail\\.com)|(salooo12@protonmail\\.com)|(swex1@swe\\.com)|(swex2@swe\\.com)|(swex3@swe\\.com)|(willburpoor@protonmail\\.com)|(williamhibburn@protonmail\\.com))$/", + "prefs": [], + "schema": 1560426922081, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1483769", + "why": "Malware targeting Facebook", + "name": "Facebook malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "202fbae4-e904-430a-a244-63b0fb04385f", + "last_modified": 1560429929334 + }, + { + "guid": "/^((\\{50c31b2c-af26-4238-bbbc-f11218d19682\\})|(\\{7c244b9b-4058-4362-9c3f-6f553c75d051\\})|(\\{bc90f38a-c295-45c4-a09f-7038ed6139a7\\})|(\\{d55311d8-83ce-4320-b30f-a7acac9224ac\\})|(\\{39d2838c-8b0d-453d-b685-71b2af4f3ff5\\})|(\\{95fdb905-de6c-4499-b7f2-372910a44405\\}))$/", + "prefs": [], + "schema": 1560371525142, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1558791", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5bcce386-ce8e-4e59-838d-a02d35de154b", + "last_modified": 1560417609081 + }, + { + "guid": "/^((\\{615a653c-240f-4dac-a42b-66751cfe7405\\})|(\\{3119fa71-b8b7-430e-ba44-4c25d5d2a4e8\\})|(\\{98075bd1-491f-4506-9e0a-262b08042e5b\\})|(\\{6102d260-a977-46a0-8635-7cdf3f3b72b3\\})|(\\{593356cd-e02b-4529-9d14-c5e4740fe749\\})|(\\{15b2bf6b-274d-477e-8456-3efc218f9fe4\\})|(\\{78311b1c-ffbe-413a-86c2-86b136aacd17\\})|(\\{98f992f6-d311-4248-939c-05f5db60ee78\\})|(\\{b1f0bcd0-0bf2-43c5-b61c-ee1fdf7f88fe\\})|(\\{7113cc05-ebbf-4c1a-9c6e-a9f959817851\\})|(\\{5325d52b-99b5-4a98-8625-24bbc8098b7f\\})|(\\{b6e3b23b-adfc-4f23-986b-4e62faedf402\\})|(\\{fd000b48-0259-4356-9c5e-2ff22d8784b7\\})|(\\{7fae693e-a917-40eb-9881-769c85f64ab3\\})|(\\{c0b9ae3d-d604-4327-95a7-67733d00bc89\\})|(\\{bc6b6fa2-ef8d-41f8-99de-ad838ad09594\\})|(\\{9dfbbcab-e7d0-4483-85ca-ca71ba03b769\\})|(\\{1d4ef484-d567-436c-ba0b-9cc0fb224708\\})|(\\{9874efa4-3a66-459a-aa77-ecf9ac8d1fe4\\})|(\\{47faaf0b-ec1e-46d3-ad59-bb44345b86d2\\})|(\\{b4b433dd-adcd-4491-8f80-ecdfb4788dc4\\}))$/", + "prefs": [], + "schema": 1560340682934, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1558791", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c016d195-ae3c-45d9-ab13-1010f0711b88", + "last_modified": 1560340840541 + }, + { + "guid": "/^((\\{43c62192-abed-474e-a619-9d486383f5d2\\})|(\\{9506f10a-e5c1-4a0a-83e7-8e83b3fb77d2\\})|(\\{a4fd5625-6912-4cc3-861e-f5338e4c36f8\\})|(\\{2ff20211-5540-4c13-8c06-6769902b0e15\\})|(\\{e15e3074-88a0-4a6d-bd1f-a7149eff3a6a\\})|(\\{e5800ff7-8cf3-410c-91a6-4b61838e4486\\})|(\\{22754446-97f4-473f-8da2-3ffbd44abdc3\\})|(\\{eea1ab8b-a4bd-4905-aa02-80874a452fff\\})|(\\{b0bfaa8c-702e-47ea-84ff-6b20ef979385\\})|(\\{f13622f7-470e-4ad3-85a5-25a0fd43f9fa\\})|(\\{f75cba29-48aa-4c02-8d7e-704532de1aa6\\})|(\\{43325b83-0f60-4d46-9b43-f9be2e91682c\\}))$/", + "prefs": [], + "schema": 1560251614258, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1558543", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Various fake clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2399177f-4b21-4c46-ae74-ab7db1edb879", + "last_modified": 1560339940028 + }, + { + "guid": "{e1bb4a20-9e0d-443a-b171-4d3b71f27211}", + "prefs": [], + "schema": 1560247588269, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1557680", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Biis" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f42edc78-6eda-4dfc-9f7b-fbaaa869852e", + "last_modified": 1560248603844 + }, + { + "guid": "/^((\\{f7b6b9aa-4564-443b-9da0-a194492cecb9\\})|(\\{f20526fe-45cb-441b-96e1-6d8aad20b97f\\})|(\\{2a7ada19-27c1-4f16-b67d-64344e85565f\\})|(\\{7faf6c45-9883-49c8-9d90-c70fd4fb72bd\\}))$/", + "prefs": [], + "schema": 1559835473927, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1557381", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Adobe Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3f781593-e000-464e-b46e-204201f42003", + "last_modified": 1559836209248 + }, + { + "guid": "/^((\\{4580e89a-d987-4a54-acd5-103eb91374df\\})|(\\{04767859-e649-473a-9ff8-1491f9217a2f\\})|(add-on@fdvoyzaxvni)|(add-on@mxdshahek)|(add-on@plgargwikihoves)|(add-on@plgbrhovtwitpor)|(add-on@plgeshovwikies)|(add-on@plgfrhovinstafr)|(add-on@plgfrscroltwofr)|(add-on@plgnlscroltwonl)|(add-on@plgsngicosnsac)|(app@clzohispatolas)|(app@plgarghovtwitt)|(app@plgargtydownesp)|(app@plgautriczoomde)|(app@plgcohovtwites)|(app@plgdehotwittde)|(app@plgdeudownlde)|(app@plgeshovtwittes)|(app@plgindhovtwiten)|(app@plgitscroltwoit)|(app@plgnlhovtwittnl)|(app@plgphscroltwoen)|(app@plgsajjhamzooph)|(app@plgsingchewmve)|(appapp@plgindzoplshind)|(application@arzoplgirasta)|(application@blibluk)|(application@breplgdownporbr)|(application@es9hsgaedr)|(application@es10gfjqzma)|(application@gerluk)|(application@grasow)|(application@plgbehovtwittnl)|(application@plgbelgdownflam)|(application@plgbescroltwonl)|(application@plgbrhovwikibr)|(application@plgdescroltwode)|(application@plgfihovinstafi)|(application@plgindiendownen)|(application@plgindzoplshind)|(application@plgphhovtwiten)|(application@twexispolavieda)|(ext@es8dffdsghe)|(ext@plgagscroltwoes)|(ext@plgbehovtwittfr)|(ext@plgbelgizompnl)|(ext@plgbgzowomawfr)|(ext@plgesscroltwoes)|(ext@plgithovinstait)|(ext@plgsescroltwose)|(ext@plgukscroltwoen)|(ext@sgdlpictomagi)|(ext@uksfdahdhsc)|(extension@es5dssdsj)|(extension@itsfahqiaxb)|(extension@plgauthovtwitde)|(extension@plgbrscroltwopt)|(extension@plgdehoverwikde)|(extension@plgfrdownlnewfr)|(extension@plgfrhoverwikfr)|(extension@plgfrsearchfr)|(plg@defdgajbisl)|(plg@es6fdhfec)|(plg@es7fdsfddqa)|(plg@esfdhalmbwn)|(plg@frhadiadsk)|(plg@indplgomenawc)|(plg@plgbescroltwofr)|(plg@plgbrhovinstapt)|(plg@plgbrsearchpt)|(plg@plgesdownopenew)|(plg@plgitadownaudit)|(plg@plgithovertwitt)|(plg@plgnlhovwikinl)|(plg@plgnlscroltwonl)|(plg@plgnorvegzoom)|(plg@plgsuhovtwittde)|(plg@plgukhovwikien)|(plg@singplganowong)|(plugin@frmdehpzamdoas)|(plugin@pldinddowninen)|(plugin@plgdahovinstada)|(plugin@plgdesearchde)|(plugin@plghowtwifr)|(plugin@plgitsearchit)|(plugin@plgitwikihoveit)|(plugin@plgnohovtwitno)|(plugin@plgpaysbasdownl)|(plugin@plgukendowauden)|(plugin@plgukhovtwitten)|(plugin@ptgouloumette)|(plugin@sgpongextejmk))$/", + "prefs": [], + "schema": 1559815025832, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1557258", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "67ab85b2-4241-4f2a-8589-801b4221b79d", + "last_modified": 1559835396847 + }, + { + "guid": "{ca6a76c4-1831-4e90-9ed8-2a3768114563}", + "prefs": [], + "schema": 1559739635606, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1556974", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Xtif" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5c7614a2-793c-454d-967e-35b06de2cbd1", + "last_modified": 1559814857242 + }, + { + "guid": "/^((\\{8dcc43e0-bd17-44db-a0a1-4cf179f4a97f\\})|(\\{a4ef8092-6d9b-4ca2-bc6a-021fc9e8e156\\})|(\\{9fb5bfd6-27d6-4321-8af9-47b6ceacac3f\\})|(\\{3c9052cd-10e7-4ccf-9dda-8524cccf66a7\\})|(\\{da554ce8-5c96-4511-93f2-4e00a85aeb20\\}))$/", + "prefs": [], + "schema": 1559677299211, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1557027", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Keyloggers" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a7bc4a56-2f54-4791-b644-0ed2403bb9db", + "last_modified": 1559739185033 + }, + { + "guid": "/^((__MSG_name__@crx-to-xpi)|(\\{3fedc417-0513-4d15-a7e5-978d044cc9b9\\})|(\\{5c0f4cf6-b6f5-4485-a941-6c25f65c54eb\\})|(\\{5e1948ce-2889-4504-9e53-170753e731f6\\})|(\\{5fbf70b1-6dd8-4cd3-83fd-fbc53c59b7f7\\})|(\\{6ed4cb78-09ec-4a85-ad8c-d29382beb6d2\\})|(\\{6f7b3f15-30ab-40ce-9424-9a03a4969bea\\})|(\\{7d516686-2f77-4f2f-8a24-2441f9661187\\})|(\\{8c50589a-419d-40b5-8edb-4027a509725a\\})|(\\{8cf8a461-47f0-4eef-966c-faf0ae9593f8\\})|(\\{9ae1dd88-5e97-4ff4-b6a6-907689b6289b\\})|(\\{17a98bce-6b45-4c91-82ac-2916264e79ad\\})|(\\{50bf8563-c1e1-4c6d-80f8-281cd0d9f453\\})|(\\{51c03d30-c160-409f-be23-a576bf6ee2a6\\})|(\\{76a4a4cd-0bbb-4c6b-b285-d3aa4b55a99e\\})|(\\{94c8b1f5-7675-48fb-8c2b-3a8c7faa059e\\})|(\\{110b4b76-4314-4c63-9ae7-01e0e30f995f\\})|(\\{204d5f5f-d2bf-4892-a35d-e02d7436a410\\})|(\\{358af48c-726e-4853-a941-a19f9f4bcf8a\\})|(\\{02110a55-b817-49e7-bddd-5ce06d7f66e3\\})|(\\{9521b96b-f232-4689-81eb-907ba68872fb\\})|(\\{83034ff5-c83d-4560-8e6e-646862d2f405\\})|(\\{167357db-2afa-43a2-90df-ca2c6527ed78\\})|(\\{08429889-d4dd-4dc0-a607-5e26d976f376\\})|(\\{20289793-a4b4-48de-b640-672e6be44f5e\\})|(\\{a1be3447-d87d-409b-8721-d895935f65b8\\})|(\\{bf599b22-fc5d-43ac-ae24-343016cf6a94\\})|(\\{c8faeded-34a7-4eb6-8e21-407c82040ca1\\})|(\\{d8b5848f-b314-483b-8fd3-9f919f0f3b8b\\})|(\\{d768a761-3df5-4676-8cbe-5e3f4f426f6d\\})|(\\{e5d45ac7-1a6d-4ec8-89ae-d75ff07b89b3\\})|(\\{e5705290-3609-4b11-a062-6c55fd074d80\\})|(\\{ec92f712-38a6-4d81-80fb-529990a5b83d\\})|(\\{f14fbd1c-1df9-4ab3-a983-a8889f88fec2\\})|(\\{fcdb43b5-add6-49f2-a102-761147cb88fe\\})|(\\{fddfb568-5055-4dc7-ac3c-5eabc69c3c75\\})|(anhjddeakbabimdgmonfbnpbainknbfa@chrome-store-foxified--782543786)|(anhjddeakbabimdgmonfbnpbainknbfa@chrome-store-foxified-875652714)|(darktheme@addon\\.com))$/", + "prefs": [], + "schema": 1559660128156, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1556759", + "why": "This add-on violates Mozilla's add-on policies by changing request setting without user control or consent.", + "name": "Malicious request manipulation add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6016087e-a255-4d70-8ffa-d7095af42e00", + "last_modified": 1559660502221 + }, + { + "guid": "{31dea008-38a8-44c5-8404-d1b110f47ab5}", + "prefs": [], + "schema": 1559648801945, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555768", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "FastProxy - Украина" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.2", + "minVersion": "0" + } + ], + "id": "dc8887b5-eae3-4cc9-937f-229b8830ba83", + "last_modified": 1559649064547 + }, + { + "guid": "{4bf7c817-a8ab-4d98-b84d-65f79f05415d}", + "prefs": [], + "schema": 1559647486096, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555768", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "FastProxy - Россия" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.0.14", + "minVersion": "0" + } + ], + "id": "96dbcd8a-d40a-4949-957b-b6eda96d0b95", + "last_modified": 1559648801932 + }, + { + "guid": "/^((\\{366bbde3-553b-4587-99d1-ce34f6b52e1b\\})|(\\{af48b3a0-a899-4722-a6b0-72ea823c9c57\\})|(\\{bc4dba63-23da-47ce-9ed5-574859c80ae2\\})|(\\{e390c529-7e38-4191-9cee-7b6902c9d833\\}))$/", + "prefs": [], + "schema": 1559647399044, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1556700", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Keyloggers" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0561368d-755a-498b-a3ba-55274d9bc925", + "last_modified": 1559647486083 + }, + { + "guid": "{5a3477ec-7abe-4efc-b44a-7ede3cc02217}", + "prefs": [], + "schema": 1559563381261, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1556698", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "youtube video download pro" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "94404f29-bf39-473a-a299-57d922f51c6c", + "last_modified": 1559647399030 + }, + { + "guid": "/^((jid1-93CWPmRaVPjRQA@jetpack)|(\\{3c78ca35-67af-4917-a24b-7699456ca61e\\})|(\\{8a8f48ed-3daf-41b9-88f4-73730459aaf6\\})|(\\{f906b9c7-576c-45ad-8f48-358271e15e5b\\})|(\\{de4f3cf2-cd35-4a2f-a405-33a4a29918a2\\})|(\\{7a365978-94e2-49d4-b5f7-6178dc80ed69\\})|(\\{dbc00301-7930-4421-91d8-23bf463228c2\\})|(\\{4f42055b-fd40-4b98-b71f-a58725d729d1\\})|(\\{8b6ae26a-4479-4727-bd89-46aef6f6e5d6\\})|(\\{b6a95353-bf1f-4c03-b4e5-43eca107ecbf\\})|(\\{933d68b5-80f4-44ba-bceb-2a6338c0ac7e\\})|(\\{cc9eb257-0f4e-423a-b124-318c59271a3b\\})|(\\{d0dc10cb-6b80-4e88-aec9-605d4e75ae59\\})|(\\{8dc1a829-e888-4ce5-a19e-614277de4d7a\\})|(\\{81ad5a42-34cb-4c35-b354-2306dbe418fb\\})|(\\{2fe8edfa-61ab-4257-848b-59def269a511\\})|(\\{55de894d-cfd0-4654-af30-719020792149\\})|(\\{4b9ffc29-0e02-4583-89d9-495084f96b43\\})|(\\{aa94dd9b-7d23-4776-ac2e-9939a3cf7bb0\\})|(\\{b43a0fd7-b010-42ae-a613-db7e792c995f\\})|(\\{af57625d-4b59-4028-874d-767a37bc9ebd\\})|(\\{84387d47-147e-4d50-8a3a-6e2bb4b522e5\\}))$/", + "prefs": [], + "schema": 1559562690935, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555945", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Adobe Flash Players and related add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a9526a76-d420-4adf-9a57-7096172d6077", + "last_modified": 1559563142131 + }, + { + "guid": "/^((\\{e914f532-5b99-4c2c-a6d9-56c6f9e07f09\\})|(\\{d7904110-ef0b-4daf-8fe1-1627c9cd14b9\\})|(\\{091d1ebc-55f9-4af0-871a-b3b383b70241\\})|(\\{eaf253cb-0418-4994-98cd-4fcfb5b827c1\\}))$/", + "prefs": [], + "schema": 1559562473865, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555848", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "WFot and others" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "740446f0-e16d-484d-a946-873f92d78c79", + "last_modified": 1559562690921 + }, + { + "guid": "/^((\\{3d35fa06-3f8e-457c-9ffa-a115f33e8e7b\\})|(\\{fd834065-2938-4522-ae8c-55592bf7e870\\})|(\\{d52afc01-3504-45eb-aa06-3fa55d2dc7cd\\})|(\\{2a34eba4-2a55-4863-91a6-c389d8e108c9\\})|(\\{45082e42-ceef-4df8-9f27-b5adf766ed8d\\})|(\\{8f77cfd1-6eee-4489-93d0-ae8627d8211d\\}))$/", + "prefs": [], + "schema": 1559562343779, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555481", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "BatchSerialized and others" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "10b029a4-72af-4e6a-b53a-492b94d1813f", + "last_modified": 1559562473850 + }, + { + "guid": "/^((\\{0c955fab-3d51-4989-aca5-d806cf0f2ea4\\})|(\\{1b382a66-46e8-4d33-a8a4-a857dc03cf12\\})|(\\{3f41aff5-f248-4b99-9c76-b7d192d9e979\\})|(\\{5c3d28a2-b778-4b48-9d71-da6fc2c37ccd\\})|(\\{8ef9fda9-1384-4efa-bbac-02f76f9a75dc\\})|(\\{8fd2e113-76cc-45cd-b4b0-cbef46166d76\\})|(\\{9c914f01-3e9f-45af-8d53-618ce7659d96\\})|(\\{70d353ca-2edb-4196-9294-d80cbca9ae3a\\})|(\\{79fba572-6a81-42be-abf8-92037bd908db\\})|(\\{106c67c5-a780-4076-af33-94945b16d4f1\\})|(\\{177bb2d7-15a6-475a-a34a-94f964b22731\\})|(\\{248d26ad-c8d7-4a1c-8d7e-22a36226f52b\\})|(\\{679b892e-c913-4993-bc18-1b737ac5de8d\\})|(\\{877e11b1-0725-41f9-b0e0-1b89cd88097a\\})|(\\{902b9979-3273-44cd-a717-8c55aec563fe\\})|(\\{3515e081-cd1d-482c-898a-62ade3007f8d\\})|(\\{4080f1c9-b07f-408e-9678-bdda4822f536\\})|(\\{7631efe5-237b-44e3-9193-205346744e31\\})|(\\{8157ad65-a32e-41f3-b99f-5c6f60a82d3e\\})|(\\{9434fbdd-d45c-43ee-98ac-a1c794f89c43\\})|(\\{19976dc8-c059-4058-a7b4-ba734cde798f\\})|(\\{32678e4c-b8df-4a19-91d8-3294b84ce78a\\})|(\\{773450d4-d2d7-48c2-9378-5affb64c4575\\})|(\\{894136b3-8133-432a-b46d-6f528608aa49\\})|(\\{7804268c-4d4f-44c1-a53c-2680e61b6687\\})|(\\{a12fdc00-7623-459d-8188-8e954b6f6eb7\\})|(\\{a46f0ac4-bd96-475d-bba6-2ab01575e06f\\})|(\\{c7dfadc5-8d54-4f00-bc2f-c1ba1483eb41\\})|(\\{cc89ddb5-7f2d-4b9c-bcf6-fc8057869838\\})|(\\{d7c4c15f-e91d-496d-8f78-79809c114a57\\})|(\\{d2925b02-9e1e-4b81-9a72-714a772be945\\})|(\\{dc59a997-f35a-4fa6-9ac0-40c1dce4829b\\})|(\\{def69fda-720f-4e2b-8783-93491939d9a1\\})|(\\{dfbe87b9-0fb6-41fb-a143-f8e0a47452f7\\})|(\\{f778e9f0-ea54-4264-83fb-6783906bce17\\}))$/", + "prefs": [], + "schema": 1559504489486, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1556339", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Keylogger Add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e57383a8-d67b-40b9-8df0-ac8a8a6321fb", + "last_modified": 1559555647852 + }, + { + "guid": "@yt-adblock", + "prefs": [], + "schema": 1559145199457, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555403", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "YouTube Adblock" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c34988ff-b712-4123-ad14-11d24d2d3a18", + "last_modified": 1559147777278 + }, + { + "guid": "/^((\\{6f35f413-21ef-4d85-96f7-64b7cccb947b\\})|(\\{7ec94ecf-7215-4cd1-a193-402c3b6e8474\\})|(\\{767e59b0-41da-4cf5-bfe1-56c8402c1ca2\\})|(\\{0770eaac-f694-49a9-bac0-39933e62862a\\})|(\\{5319ec46-b72f-4b1c-90a7-67b1b392af05\\})|(\\{7765a798-ae6b-4ea9-920d-fe7f6d07043a\\})|(\\{93927b7e-0c83-4ce8-b66e-36bb88e2551f\\})|(\\{bd6bd2fb-8614-4302-a67d-bf4f7da55e20\\}))$/", + "prefs": [], + "schema": 1559136970465, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555357", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Add-ons exfiltrating user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a8ca8b1b-1897-45b6-b12a-b64c45d8959e", + "last_modified": 1559139447403 + }, + { + "guid": "{f6bca217-8cdc-4c85-b8ae-30da228dbd71}", + "prefs": [], + "schema": 1559127564301, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555026", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Gmail checker plus" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5a110749-4dbf-46c9-a6a8-56c19d6eb346", + "last_modified": 1559136956255 + }, + { + "guid": "/^((\\{cc9f487a-618e-4c48-b0a9-65f25d53c887\\})|(\\{8fcc31d3-f865-40fc-9f31-a38ab9973e9a\\}))$/", + "prefs": [], + "schema": 1559124226003, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555282", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Remote script add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c3025513-0716-4a0b-8a1b-9d5636c90f79", + "last_modified": 1559124311799 + }, + { + "guid": "/^((\\{207df7ea-6d21-4fdb-b4fb-566ae3666245\\})|(\\{e622e0a7-d0c6-4747-bf5f-fe0321da85e6\\})|(\\{8ea990c1-ea81-4aa8-8f0b-ab6ddb888bbc\\})|(\\{1f0f312f-85ae-4603-9761-4dea6a699227\\})|(\\{f9b0e524-7ccc-4392-9130-09a7c84f9730\\})|(\\{f9567a86-accf-4710-bf33-d5ff890416af\\}))$/", + "prefs": [], + "schema": 1559123848301, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555280", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Keylogger add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "32891270-8ebe-41b1-b89c-9caf80944c89", + "last_modified": 1559124225988 + }, + { + "guid": "/^((\\{8fad4bef-56e9-4879-8780-ca7c18aa1171\\})|(\\{a6ce9b9f-cea4-44b6-ada5-a96c6bbf6d83\\})|(\\{a14b3807-8409-4b4d-bb16-5d1996492672\\}))$/", + "prefs": [], + "schema": 1559122494410, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1555273", + "why": "This add-on violates Mozilla's add-on policies by collecting ancillary user data.", + "name": "Keylogger malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "57a3e7c7-3cf9-46a5-88ac-3f87029e4b5a", + "last_modified": 1559122823674 + }, + { + "guid": "/^((\\{5d840f65-c476-479b-a2e3-5a19b7a0a853\\})|(\\{021b8ac0-4a36-4294-8261-662d947a83d4\\})|(\\{038b50d5-8590-4479-93d2-4c07b619f402\\})|(\\{882bf6a6-47d7-47c3-8bbf-4f8fb259358d\\})|(\\{0918fee5-aee0-4e84-9613-a8b1e59dfcff\\})|(\\{90854ba5-e748-4f74-b8c2-9a6aa409894c\\})|(\\{a22263da-63d6-44fc-bdb8-381ba7e3c36a\\})|(\\{ec50f1de-0bae-4bfa-b665-59254094089e\\})|(\\{f48a449e-54f7-44fd-90f2-34a9526d5766\\}))$/", + "prefs": [], + "schema": 1559046153546, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554962", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Keylogger add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6c8516bd-c427-4a7a-9f52-10c942e083a2", + "last_modified": 1559046242210 + }, + { + "guid": "/^((\\{a43383bc-1d39-4d94-b353-ceaf942c7d52\\})|(\\{0a727455-07c0-4c8b-a0e2-7853347e13e4\\})|(\\{3810f596-bf50-74e2-a47c-9894ebdc5179\\})|(\\{03ce6944-6491-4f7c-ae42-3f2a9c3f9c99\\})|(\\{3523e8cd-09e2-4442-ae13-e1d1575d4b27\\})|(\\{a08f79e0-c70f-4021-80e9-44614d5f8b5a\\})|(\\{e43b625a-f337-4cd6-b3d1-6763b5213223\\})|(\\{744c464d-4cc3-4303-b3d1-5b756144cd5e\\})|(\\{c948603c-496d-426f-a7ff-9af3d7ac1380\\})|(\\{ceaea029-439b-4d0d-99ca-a261de44d0dd\\}))$/", + "prefs": [], + "schema": 1559039806318, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554942", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Keylogger add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "940f09a9-e0ba-4b98-8dd1-edb78573b62f", + "last_modified": 1559039956017 + }, + { + "guid": "/^((\\{d227e621-ef26-4b4d-b724-0138e5bb03dc\\})|(\\{ec418296-8754-46fa-a265-9856f1706f8d\\})|(\\{e64aeb61-251e-46ed-863c-b9a7c4849cfd\\}))$/", + "prefs": [], + "schema": 1559039652168, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554941", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Service Processor and others" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2ea28df4-7fdc-4571-95ba-10b61e270c72", + "last_modified": 1559039806304 + }, + { + "guid": "/^((\\{e7f0881e-39e9-4569-85b5-71b925294de3\\})|(\\{4d19382a-c81d-488a-98ac-f73484a6dd2b\\})|(\\{6d68222e-1982-4b74-9fd1-52b6a4b4c2a5\\}))$/", + "prefs": [], + "schema": 1558986099090, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554675", + "why": "These add-ons violate Mozilla’s add-on policies by executing remote code.", + "name": "Microsoft .NET Framework Helper" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2b5e298d-9dfd-4c66-ae18-1671001f4cf2", + "last_modified": 1559039652155 + }, + { + "guid": "/^((\\{59feb34b-bb64-4063-933b-d5af131da847\\})|(\\{9d84eba6-c1cb-4ec2-8b4b-b6be1ecd902a\\})|(\\{7c98044a-2ab6-481b-bc75-e2e5df6c4de8\\})|(\\{62457714-ded1-44fd-b107-b14da63a2850\\}))$/", + "prefs": [], + "schema": 1558975224053, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554740", + "why": "These add-ons violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Youtube Downloader" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6a5c8e26-d7c8-4c45-91cc-d86bd1aaa0f3", + "last_modified": 1558976967835 + }, + { + "guid": "/^((\\{6f2cec94-2f78-4812-9898-1bf98d7ccbfe\\})|(\\{e8cff71e-5c43-4fd3-b63b-7b9f6c29d54c\\}))$/", + "prefs": [], + "schema": 1558959050284, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554676", + "why": "This add-on violates Mozilla's add-on policies by using a deceiving name and exfiltrating user data.", + "name": "Data exfiltration malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "98aa70f7-d993-490b-ab07-108cc5a8f38c", + "last_modified": 1558967785430 + }, + { + "guid": "/^((\\{b3c1723b-6e69-4a3d-b3f6-90793bbd6500\\})|(\\{ba38e6b7-7480-4e58-9f5b-162e83c6ece8\\})|(\\{ff08056a-a34e-44b1-bcae-805d3770738f\\})|(\\{29806031-d49c-4ef3-a37a-32ee29e5fc0b\\})|(\\{541e33f8-ec74-4311-b7a3-8baa185aeb7e\\})|(\\{d8196867-5419-450c-aee4-1e349b4b0b5f\\})|(\\{ebd7a4e7-056e-4009-bb5e-753c1d1eed93\\})|(\\{01935a63-d565-478a-9f91-9ff9aa49ce61\\})|(\\{d0e7ce73-9829-4a10-b5f2-83f8bf2c730b\\})|(\\{b70f302a-84ad-4f10-8af3-f6ea4aa327fb\\})|(\\{e5f1a2e5-798b-4915-b109-5ebbe2b57821\\})|(\\{7921669d-959a-4992-857d-f47761b5b4ac\\})|(\\{80808d17-bf74-4b91-8fa5-694c3107950d\\})|(\\{84984575-1b73-4b96-ba1c-d9959393e742\\})|(\\{20587256-b018-41c2-91fc-5a278a2837f2\\})|(\\{577f8c9b-c791-4999-9c39-1e4584f4e9d6\\})|(\\{3691584f-c3a3-4fde-8817-b2501802ef54\\})|(\\{e1680c37-e6ff-4280-8f97-943d77e63022\\})|(\\{013ae092-188d-4e95-9138-2b4d98dda7cd\\})|(\\{05e3806c-00e6-40c7-8503-9c30f3db8e63\\})|(\\{9552ab33-c43e-4338-a020-80dc3636f577\\})|(\\{8fd3c74f-57d7-4e1b-9e52-6c4517ef89f0\\})|(\\{9b0ad6aa-7c54-4655-aca5-78e973b0ebd4\\})|(\\{e648ecf7-4858-40f8-9d85-5cc5f68eae6c\\})|(\\{9430fbaf-aa5d-4492-92c2-0f1354c5c860\\})|(\\{d58bd1fd-6451-42d5-b066-4baf7d4271f9\\})|(\\{948790d7-57d3-4db1-8fc7-7ccee4abf047\\})|(\\{1b8d5392-1641-43c1-a6d6-d1429e9d4109\\})|(\\{3ae072ea-3ffc-4395-8f3c-ebe92f355c3d\\})|(\\{32f9b8a8-505a-4621-979b-b6919633f787\\})|(\\{e939e079-1e15-4491-95b3-6fb8a836e80b\\}))$/", + "prefs": [], + "schema": 1558954910531, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554635", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name while providing unwanted functionality. This is not a legitimate Flash Player add-on.", + "name": "Adobe Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e454fe70-d5d7-40c0-a571-e9253d1361d5", + "last_modified": 1558955090783 + }, + { + "guid": "{19ed30e8-28ad-405a-a7e4-18a8c78b1078}", + "prefs": [], + "schema": 1558951086426, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554624", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "OpTurs" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3a3b8837-605f-48dd-8b40-a66777f45108", + "last_modified": 1558951513292 + }, + { + "guid": "{9834ff7f-e3ea-485a-b861-801a2e33f822}", + "prefs": [], + "schema": 1558813299527, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554465", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "LinkT" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b9496c5f-b37d-4b8e-abaf-509b980ab553", + "last_modified": 1558868765210 + }, + { + "guid": "/^((\\{0e9ab3be-c296-4825-aecd-3923ded051f6\\})|(\\{9f737295-e8d2-4e70-b226-8809f6b135c9\\})|(\\{68e1d557-8fc1-40e0-b197-43f8f3d36239\\})|(\\{90221614-a0b9-4301-b141-3f8a23fb4835\\})|(\\{d3255cb0-bf30-43b0-afd3-db97bfeeede4\\})|(\\{b4498268-c0d0-435c-944e-8dd6e8518654\\})|(\\{93d90a45-a10e-47df-a106-2ffeefe3052a\\})|(\\{d7b04034-ea8b-4219-ad1c-ffa061a2e0cb\\})|(\\{391772ba-a23c-4892-b30d-45d2a935be3c\\})|(\\{0b2aaa98-1f4b-483a-815f-3f864711a737\\})|(\\{2564ed8f-305b-4ade-a787-6fae696c14ab\\})|(\\{fc2fe0a7-9886-4a7e-9850-cccc2879b0e7\\}))$/", + "prefs": [], + "schema": 1558712940017, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554227", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "768bde60-2086-487f-b374-ca3fa6e712fd", + "last_modified": 1558713145915 + }, + { + "guid": "{4ee078c0-ded1-4f82-9bb1-de645e778924}", + "prefs": [], + "schema": 1558712019540, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554097", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Switch" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a0508904-1f0b-4352-a5e6-a33f8fa26ce8", + "last_modified": 1558712846698 + }, + { + "guid": "/^((spp@avast\\.com)|(\\{056790bb-9676-40fb-845a-feae6dedfbee\\})|(\\{15d51e39-3ccf-4ce2-a434-dbbf1785e867\\})|(\\{2bbdf86f-3c6b-48d6-9934-9051ce5f5976\\})|(\\{2f6d1519-33b5-4970-a7ec-561f5e067ba0\\})|(\\{2fd10339-a9db-4846-bdd7-ee41cea97312\\})|(\\{31390faf-ef95-4f4b-a1a4-3c3a09dd7b5a\\})|(\\{411bfbf9-646d-401c-b87d-e77d812a68ce\\})|(\\{44e4b2cf-77ba-4f76-aca7-f3fcbc2dda2f\\})|(\\{5422d0cd-3b45-4fcd-9886-463be7e1a05f\\})|(\\{5ae5a1f8-a994-4e61-8c99-54a9fe58a9c4\\})|(\\{5d4c1f36-196d-4e9a-909b-8ad138546f79\\})|(\\{7150cd87-1b5f-41ea-b659-5cae4b753e2d\\})|(\\{78a02646-2bf6-417e-9498-32f29a4ef89a\\})|(\\{7bdac7a1-be1d-4ecd-8cf1-a1db64adfaaf\\})|(\\{80686e70-c06a-4ab3-b7bf-fd4c05985c1b\\})|(\\{83830f14-c5d0-4546-af99-cbaba3ab832d\\})|(\\{869a5e06-732e-4635-8da3-90a2802f9c80\\})|(\\{87ea875a-396a-4c7b-b202-cecd5a4fe0d4\\})|(\\{94847025-c5a9-4dd7-83df-54c17b79eeb8\\})|(\\{992e4d3d-f56b-4f71-b826-0dd976681228\\})|(\\{a259d36e-9c24-4216-8b28-d3e83c07a832\\})|(\\{a669b31a-3a2b-4c75-838c-a8542f77c79f\\})|(\\{af35bf73-7d25-4286-9be6-fa822818ac82\\})|(\\{b01f0319-b398-4a6e-b9c9-e59e2d99eee7\\})|(\\{c516baf9-a911-453e-be0e-26389cfb33ac\\})|(\\{c88fc74d-31b5-40d4-bb8a-008f2d7a1ea0\\})|(\\{ca6b87f3-2d8b-49ea-9627-95e900c5f108\\})|(\\{cdc01730-6108-4581-b5da-36f7fa8e3d2e\\})|(\\{cfbbd54d-26dd-4f20-b0c9-26b2d920bc04\\})|(\\{d384c2ef-9e42-4dfa-bba5-73b9b6ad2e61\\})|(\\{d7ef08b6-ef77-43b6-ad60-74ea67495674\\})|(\\{dec788dd-9a21-416d-91c7-bf79250cab04\\})|(\\{fb182266-3336-4dcb-8898-859affe73e7f\\})|(\\{fe17e98b-1ed8-45fe-a6e5-8280902d2500\\})|(\\{febfdee8-5724-4aea-8b70-6be9e22248fc\\})|(\\{ff471567-6ff5-48d9-8db6-d2c9134f0aed\\}))$/", + "prefs": [], + "schema": 1558674107244, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1554004", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Keyloggers and Fake Anti-Virus or VPN add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3b36e078-7c88-411c-9366-033ac185c66d", + "last_modified": 1558711200727 + }, + { + "guid": "{e256d52b-d9ae-4709-aa9f-ba4d1eb1b284}", + "prefs": [], + "schema": 1558637428118, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553531", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Reading Cursors" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4b943d8a-828f-45d2-b8e7-f16e6c3f860c", + "last_modified": 1558637573461 + }, + { + "guid": "{7d3c46ed-b9f7-497e-bccc-e6d878032d14}", + "prefs": [], + "schema": 1558636256156, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553746", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Zoom" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1282ea00-9aa1-47c6-9a93-4fc17aa4fcc4", + "last_modified": 1558637428108 + }, + { + "guid": "{cc02a70f-0610-456c-bc5e-5eefb6716904}", + "prefs": [], + "schema": 1558636068339, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553761", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "headingsMap" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "37c5c7d6-e0ce-4c6b-8c89-49d94b6fe159", + "last_modified": 1558636256148 + }, + { + "guid": "/^((\\{5768d1b3-4e2d-4088-bd65-c7a62353ea3a\\})|(\\{65b99c4e-a9bb-4bb9-913d-503fa9bcdc46\\})|(\\{31ebd11b-bb60-403b-94a9-e09a3bf7d64f\\})|(\\{571339cd-9f45-47be-9476-767a62cb6c97\\})|(\\{ed4f9665-1851-4398-ab15-46c5e3ab8fac\\})|(\\{972319b8-8dd8-4ed0-8de2-9bc6582f0560\\})|(\\{4a0d8618-3e21-4bb8-8ae3-d04316b55a1e\\})|(devlopper61@avast\\.com)|(\\{8df3e056-6d4f-42fa-b0ad-40ee9a79d9c4\\})|(\\{e7e68020-07de-4f9f-9aec-6df29e9f64b8\\}))$/", + "prefs": [], + "schema": 1558635731472, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553857", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Easy Screenshot, Youtube Download*" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7f8e7584-6c6c-4579-882a-6f6ed21766dd", + "last_modified": 1558636068330 + }, + { + "guid": "{b19d065e-4460-4714-9208-3f2b38907522}", + "prefs": [], + "schema": 1558537447980, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553521", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "QxSearch" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "60311c2e-044e-4e24-9abe-6ee75d7f5467", + "last_modified": 1558537465968 + }, + { + "guid": "addonfx@horoscope-france.com", + "prefs": [], + "schema": 1558537035045, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553518", + "why": "This add-on violates Mozilla's add-on policies by overriding search setting without user's consent or control and executing remote code.", + "name": "Horoscope France" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ec32be61-2646-4686-9829-7cff21f5d1f8", + "last_modified": 1558537447957 + }, + { + "guid": "{65dc18e1-109f-4039-929b-f8a7a29be090}", + "prefs": [], + "schema": 1558536906311, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553517", + "why": "This add-on violates Mozilla's add-on policies by overriding search setting without user's consent or control.", + "name": "Magnif)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "bf7a8bf0-e60e-4516-9e93-777c19509ef6", + "last_modified": 1558537035028 + }, + { + "guid": "{3fc1db2b-e7db-4512-b24e-1faf4d3a1b4d}", + "prefs": [], + "schema": 1558536030188, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553479", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "quikaxes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "43966df2-e95c-415b-bffc-13814e1d2b11", + "last_modified": 1558536765550 + }, + { + "guid": "{a37a7625-b64e-45f3-8b79-f71634f14438}", + "prefs": [], + "schema": 1558467699805, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553326", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Lift" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7e86024e-5621-4ded-bc16-184f94fa2e29", + "last_modified": 1558528706505 + }, + { + "guid": "/^((\\{3e20d1e2-a7ee-4ce2-ab9c-51c8300a8ff6\\})|(\\{30906bbc-0942-445b-89c8-f74dac0edb8f\\}))$/", + "prefs": [], + "schema": 1558382009200, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1553071", + "why": "This add-on violates Mozilla's add-on policy by executing remote code.", + "name": "Amiri" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9c5f5681-8547-4e65-9c05-5796e483b8e1", + "last_modified": 1558434518612 + }, + { + "guid": "/^((\\{ec19994c-c5a5-46d9-bd4d-0fc417c6f4b8\\})|(\\{a0be7e8d-b0a3-460b-8a52-429c79e49ee2\\})|(\\{1814dd58-4147-4cca-a0a3-c5aa35966d9c\\}))$/", + "prefs": [], + "schema": 1558381075651, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551937", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Inspiring Quotes + Daily Quote Tab + Pug Extension" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "efecef61-549b-4c13-8a52-394c636dd24b", + "last_modified": 1558381320379 + }, + { + "guid": "{dc6176c4-a192-4a92-849f-ad13abe889ad}", + "prefs": [], + "schema": 1558379927394, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551934", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Easy Speedtest" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a5fa8f77-7761-4996-a11d-d8cf723103da", + "last_modified": 1558381015030 + }, + { + "guid": "{ac4be7d1-4db6-4b4c-bf48-e345350bcb59}", + "prefs": [], + "schema": 1558379796996, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552830", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code and exfiltrating user data.", + "name": "Browser type hider" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "41137e55-8a11-4259-a009-42c29daadf17", + "last_modified": 1558379927382 + }, + { + "guid": "{da993d54-9605-42f7-a32f-9f565245070c}", + "prefs": [], + "schema": 1558362750147, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552834", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Fake Adblocker" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2212748f-ad60-497f-af7b-50d20b326e40", + "last_modified": 1558379796981 + }, + { + "guid": "/^((\\{2b3eed60-8f6e-4afc-99f1-38d12f4253da\\})|(\\{3a6f9dac-3a93-4f6f-8b74-9ebc0f501306\\})|(\\{46bba8e9-7a75-4dd6-932e-bdd74141cb87\\})|(\\{4b480ab6-b63a-43f8-b4f4-d312972ab086\\})|(\\{6106687e-ca0c-4d7e-93bc-115929e4d299\\})|(\\{717ce133-3c0a-448e-b6ed-fc7d22b76534\\})|(\\{7224f1ae-c342-4bb5-8441-d324a8951624\\})|(\\{768e9638-2eba-42e4-a13a-4f3f1df391a2\\})|(\\{7b655f43-c871-46d2-8f6d-31989e8ee939\\})|(\\{7e46c692-9488-4671-8c39-7830f92628b0\\})|(\\{83bc6b56-545f-4ba1-a80b-f82d55cc0f68\\})|(\\{970a774e-b7a7-498f-b3f2-d88b14b4dab1\\})|(\\{9d2e8731-3287-46eb-9c19-ece63fe693c7\\})|(\\{a37ccd20-e04f-4949-b055-98ca58639606\\})|(\\{af85f82c-3e8f-4ee5-ab53-b8d3aaac34ec\\})|(\\{b35c6d47-8b07-4d49-89a9-dfe8c10f58f6\\})|(\\{c2485579-368c-4593-a1cd-985b2fa0b990\\})|(\\{c85c16ba-78b4-41b3-9201-f80fa662c52f\\})|(\\{c97e5535-6f2e-4d34-a5a3-0e6e07f7fd13\\})|(\\{ce7db166-9564-482f-91d9-3a450ec3216d\\})|(\\{d64a2c73-ff21-4e3e-998f-ec2dc42ad725\\})|(\\{db6d93c3-67a0-410c-b7bd-f72f267f0cec\\})|(\\{e513775f-359f-47aa-a3d9-eddc946aabe0\\})|(\\{f70258e4-643b-4ec2-9c84-de89009eec61\\})|(\\{f8794e87-82b2-4df4-bce6-db207f62c165\\}))$/", + "prefs": [], + "schema": 1558349836861, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552847", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name while providing unwanted functionality. This is not a legitimate Flash Player add-on.", + "name": "Adobe Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0225ae55-626d-42b2-8f48-46ec95ec89f8", + "last_modified": 1558361121954 + }, + { + "guid": "jid1-HfFCNbAsKx6Aow@jetpack", + "prefs": [], + "schema": 1558343683249, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549020", + "why": "Versions up to 2.9.1.0 of this add-on violate Mozilla’s add-on policies by executing remote code through the native messaging host.", + "name": "SConnect" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "2.9.1.0", + "minVersion": "0" + } + ], + "id": "413065ac-176f-440d-b4a7-0f928f3e330d", + "last_modified": 1558349549359 + }, + { + "guid": "/^((\\{78d3cced-d2a7-46e9-9fea-5be7ed61eea8\\})|(\\{3233777f-a1a7-4ea4-8c2c-fba1a0a68383\\})|(\\{0eb43948-2a3d-4490-b32d-7ca37dd83f07\\})|(\\{64fd625d-2977-46a6-96ca-77f81ebfd54d\\})|(\\{6e138cae-1be3-449e-a964-b3c3060d89b9\\})|(\\{f875c255-8b92-4229-95e1-6d9adaf20dd7\\})|(\\{3c62ef7f-ae8f-4baa-9d2d-27a377480b79\\})|(\\{35a91fe5-c255-498b-9f9f-bec506fdb257\\})|(\\{7d3c52e6-2b7f-4ce8-b28b-032306fe32df\\})|(\\{0ecf6f68-d506-4239-bc69-f77de8f03758\\})|(\\{7290f2b1-3d70-4990-a828-40c775c05f82\\})|(\\{50150580-86bc-460f-ae3a-12e51b9d842e\\})|(\\{a1b46cda-8a83-48e0-b355-7eca4250694f\\})|(\\{614d8f88-b5b4-4897-adc0-0207613f4d4f\\})|(\\{ddc259e9-3738-4b18-a00c-9259dad206ae\\})|(\\{5b2bf836-5322-4161-82dd-fcc8ac6e4247\\})|(\\{97a90c0a-5e3d-47bf-aacc-230e4cb1f2d1\\}))$/", + "prefs": [], + "schema": 1558341490879, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552764", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without users consent or control.", + "name": "Search overriding malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4bda902e-fb36-417b-940f-992ab7a18fde", + "last_modified": 1558343652556 + }, + { + "guid": "{3241efcf-4bfe-4405-ba7e-029d3efb03bf}", + "prefs": [], + "schema": 1558341442463, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552681", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without users consent or control.", + "name": "Neat" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "657d264f-23d2-40f9-bac3-1d7c8b5d8453", + "last_modified": 1558341473815 + }, + { + "guid": "{c4da221b-461a-4ed9-b2d2-6ef1842a94bd}", + "prefs": [], + "schema": 1558341410023, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552639", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without users consent or control.", + "name": "Dimensions" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f48c8fe3-70c4-4d9e-84f4-79aeee9970f0", + "last_modified": 1558341442447 + }, + { + "guid": "{72a6bcef-d0ce-49f5-9773-1b78265174a2}", + "prefs": [], + "schema": 1558341352074, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1552306", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without users consent or control.", + "name": "YtDow" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d4ab989c-1517-493b-9486-f0c4a59a1c95", + "last_modified": 1558341410006 + }, + { + "guid": "{ad6f5b9a-c894-4d15-8c65-4b0f5a29503c}", + "prefs": [], + "schema": 1558341285260, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551964", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without users consent or control.", + "name": "Magnif)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f7135e2e-249f-45d4-bfe3-88b834ddb636", + "last_modified": 1558341352061 + }, + { + "guid": "{ad6f5b9a-c894-4d15-8c65-4b0f5a29503c}", + "prefs": [], + "schema": 1558313782700, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551903", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without users consent or control.", + "name": "Top Scroller" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fdc0d028-2915-4130-a19c-c5819b3acc81", + "last_modified": 1558341285245 + }, + { + "guid": "websurf@mizilla.org", + "prefs": [], + "schema": 1557871049351, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1550673", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Websurf" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a2b8447b-ea50-45bb-936a-64d790ebe448", + "last_modified": 1557871228984 + }, + { + "guid": "{d8f707bf-0a35-462f-8e4d-f90205770547}", + "prefs": [], + "schema": 1557870872432, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1550689", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "STPs" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "be42b13c-301b-4304-a1ed-89b4c13d9eb2", + "last_modified": 1557871049337 + }, + { + "guid": "{0b66e692-1991-4b46-89df-c8101925bad1}", + "prefs": [], + "schema": 1557870310029, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551079", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Base6" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a308009d-3320-49af-b0b8-1174f5a25fe6", + "last_modified": 1557870872418 + }, + { + "guid": "/^((\\{198586f6-9fdc-4ce6-8f21-7bdd85eab432\\})|(\\{1cd349e4-1d52-46ec-b648-6da8ba2ef659\\})|(\\{28c26373-1066-4cb5-8e92-1926cb31f83c\\})|(\\{2b99ea1d-9e25-4005-adeb-2fc9fc6700fc\\})|(\\{2be44a2e-f432-4527-a249-f7a6aecc8464\\})|(\\{2eda700f-8674-43fd-842f-73289b6e317f\\})|(\\{3745fc58-1413-4029-aea4-e1aa8a2c0cad\\})|(\\{3c19f6fc-1b86-411c-8d9a-7fdde31600b2\\})|(\\{450f8d34-b065-46a6-bd9f-ee7f614d750b\\})|(\\{498e999a-2d6b-47e7-8da2-97e0f694f6ff\\})|(\\{56862943-b999-45ef-be94-b97211126ba4\\})|(\\{5dc4633d-2c01-4d8d-8980-a90055d0679c\\})|(\\{77101ac4-6fe7-43ed-8362-75ad2a4b3299\\})|(\\{83ae749b-9ab3-41f9-ba8a-f73470399abe\\})|(\\{8ef68e62-a602-477c-95c2-9b861f91e813\\})|(\\{b81c02f0-e563-4794-8fd3-18a65b0f35fe\\})|(\\{c03bf205-6673-4495-abd7-f12556d3d8ce\\})|(\\{d1e8be12-c4e4-481b-9be1-400f54257dfa\\})|(\\{d3f73060-8ca3-4c24-b389-6a896f43f538\\})|(\\{e5e98141-81c0-433d-ade2-4174ea951243\\})|(\\{ec6ff98f-7315-4cfb-88b9-e6a64bb97ef6\\})|(\\{ee765c0e-cf70-426e-ac5d-704c874202af\\})|(\\{f8a4dc88-e967-4c75-acb3-6176ab166bf4\\}))$/", + "prefs": [], + "schema": 1557849229557, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551093", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8436c1bb-43f2-42d6-acec-05145fdbeccf", + "last_modified": 1557870310012 + }, + { + "guid": "{61121092-5257-4607-b16a-12364832f0e4}", + "prefs": [], + "schema": 1557835907235, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551260", + "why": "This add-on violates Mozilla's add-on policy by executing remote code.", + "name": "Page Image Previewer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "dbd3690c-2ce4-474b-b13d-97f9ab2c54c5", + "last_modified": 1557835935826 + }, + { + "guid": "/^((\\{2e510835-3d3c-4995-ba75-2eee6ff203c7\\})|(\\{bc72fefd-ab07-40ce-8555-45f9b23ef8c0\\}))$/", + "prefs": [], + "schema": 1557835858186, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551259", + "why": "This add-on violates Mozilla's add-on policy by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9ced28d2-c4bf-4933-b369-1ded1ca7f6dc", + "last_modified": 1557835907221 + }, + { + "guid": "{4037503e-7401-4ccf-8fc1-af9f8c9fc168}", + "prefs": [], + "schema": 1557835817321, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551243", + "why": "This add-on violates Mozilla's add-on policy by executing remote code.", + "name": "APKCombo" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "abc5acc2-c9ea-4d44-b8ab-3fefb5723194", + "last_modified": 1557835858172 + }, + { + "guid": "{52484281-3051-4c52-9309-83896b989ddf}", + "prefs": [], + "schema": 1557835777799, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551162", + "why": "This add-on violates Mozilla's add-on policy by overriding search settings without user consent or control.", + "name": "Fppl" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "73b95004-eb42-428b-852b-3671edbc3913", + "last_modified": 1557835817308 + }, + { + "guid": "{3555a8e7-6fc1-4ad8-9e35-b09877d94a8c}", + "prefs": [], + "schema": 1557776499103, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1551080", + "why": "This add-on violates Mozilla's add-on policy by overriding search settings without user consent or control and executing remote code.", + "name": "Search" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a941e00d-4ec8-41db-ac90-75041af68a3b", + "last_modified": 1557835777786 + }, + { + "guid": "/^((\\{da61a3e5-5a98-4c47-ae6c-f4db738f1133\\})|(\\{b0e13c2b-c1cd-426b-bed9-905bf9557fbf\\})|(\\{328c22c5-5f1c-4eb7-95a3-148fd4ad429d\\})|(\\{f6cca5fb-5aa1-471f-88f3-e2ffa87281ef\\})|(\\{d342bf37-554e-41c9-b67b-72769e59b82b\\})|(\\{03ec69b5-3e8e-4bb8-8eda-28f12c54bff8\\})|(\\{a8c876cb-af13-4ad9-9a86-fc3c0722b48c\\})|(\\{56136c32-0159-4368-9d28-c1b8b1515c89\\})|(\\{79bf4660-9729-444b-ae03-6c8005869611\\})|(\\{aa7fdaa5-d888-47e2-b27b-4fa4b3225339\\})|(\\{31e0d180-52b1-4c1d-8f84-7e625715edc4\\})|(\\{f7d20549-e5ee-4045-9e8f-9705bb10c104\\})|(\\{303abacb-760b-43c3-9640-5b456d92db78\\})|(\\{debabd67-2e0a-485e-8213-ac081065a027\\})|(\\{971e739b-c528-41b6-a60c-48fc3cdb52d9\\})|(\\{ffb3a485-2723-4a88-b3ad-8b29773759c4\\})|(\\{b076177a-a5c4-4652-9f6d-953f89f9a81a\\})|(\\{66210cb7-6352-45d5-9d22-ad7a0fb5e247\\})|(\\{8053ad7b-5129-4c74-ade9-8166c38e8636\\})|(\\{1a435c36-133e-4163-ac71-8701a147880c\\})|(\\{8c40c6df-7c9d-4876-bcbe-0621734aba45\\})|(\\{40e1e7d9-ae29-4aec-9465-5e0d49859583\\})|(\\{74eab03b-35cd-4950-b436-7afce3876e58\\})|(\\{95839c11-63a7-4b2b-b3d3-eee9d2c5c42d\\})|(\\{bfaa03c3-744e-48eb-8fb6-4ad61791d4d8\\})|(\\{f123e726-9396-4899-822a-172b8bcb2c5f\\})|(\\{157e255b-2053-4140-b95c-ff003b62bf17\\})|(\\{3e49a17b-b58e-417b-9ebb-a7e8c2317893\\})|(\\{4df1d536-e30f-4344-bee6-6ef2def890c2\\})|(\\{f33ce070-63f4-4d2b-823e-d52fc7a30ba7\\})|(\\{2003e2a5-e848-4fc5-8e7d-3af1efe4f992\\})|(\\{ff2157da-6981-40b6-aa60-d8125e73868e\\})|(\\{d89fa1e5-c9d4-4104-ad8e-00b39e5c6d15\\})|(\\{66e45d14-550f-4489-98c6-8a0caed33375\\})|(\\{86e6d45f-1dfe-4e53-bf52-22bf65b9ae6d\\})|(\\{e71407fe-e1ed-4755-af8f-dd64a952ce1a\\})|(\\{b67b3615-d8fe-4961-a41e-391864afde2d\\})|(\\{5785789b-ccba-44a1-9018-1135b56bd37f\\})|(\\{6dfb93d1-2add-471c-bbbc-b6164b4c1d94\\}))$/", + "prefs": [], + "schema": 1557495790401, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1550581", + "why": "I’ve reviewed the add-on and confirmed that it is collecting ancillary user data, violating our policies.", + "name": "Adobe Flash Players" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fee4b92e-146b-437d-9cc0-95cfc800f0e0", + "last_modified": 1557497630665 + }, + { + "guid": "jid1-NIfFY2CA8fy1tg@jetpack", + "prefs": [], + "schema": 1557437285372, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547789", + "why": "The $rewrite filter functionality allows for remote script injection under certain conditions. Please upgrade to the latest version of Adblock for Firefox to resolve this issue.", + "name": "Adblock for Firefox ($rewrite filter)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.28.0", + "minVersion": "3.19.0" + } + ], + "id": "8ff19ad3-e4e0-40e3-8f02-fd80d18f63b5", + "last_modified": 1557437486195 + }, + { + "guid": "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}", + "prefs": [], + "schema": 1557437276676, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547789", + "why": "The $rewrite filter functionality allows for remote script injection under certain conditions. Please upgrade to the latest version of Adblock Plus to resolve this issue.", + "name": "Adblock Plus ($rewrite filter)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.5.1", + "minVersion": "3.2" + } + ], + "id": "f0fc8d21-d0ec-4285-82d7-d482dae772bc", + "last_modified": 1557437285359 + }, + { + "guid": "{2b10c1c8-a11f-4bad-fe9c-1c11e82cac42}", + "prefs": [], + "schema": 1557437241208, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547789", + "why": "The $rewrite filter functionality allows for remote script injection under certain conditions. Please upgrade to the latest version of µBlock to resolve this issue.", + "name": "µBlock ($rewrite filter)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "0.9.5.14", + "minVersion": "0.9.5.11" + } + ], + "id": "12a0c69f-e755-428b-97dc-229bccb8a5b0", + "last_modified": 1557437276663 + }, + { + "guid": "/^((\\{4e84c504-10e8-4e75-8885-dcc0c90999b9\\})|(\\{8ce99d6d-8d0d-4420-bd17-c303bd8a763e\\})|(\\{16de314a-56cd-4175-9baf-bbe0b09dfed3\\}))$/", + "prefs": [], + "schema": 1557434135180, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549744", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Secure Privacy + Trustnave + Fastwebnav" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "04a300c2-04fc-401e-a428-c7c887bf2bff", + "last_modified": 1557434278943 + }, + { + "guid": "{5308dcd8-f3c7-4b85-ad66-54a120243594}", + "prefs": [], + "schema": 1557433916783, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1550428", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "F_Feed" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "312e30b0-0b4c-4a43-8f6c-8b8447a20f6a", + "last_modified": 1557434135166 + }, + { + "guid": "/^((\\{c8d0fea0-d7b7-4f6f-b9bc-9df6722d9d18\\})|(\\{bed8e1f2-b00b-44e3-8cf0-5335080d0003\\}))$/", + "prefs": [], + "schema": 1557433212304, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1550435", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Webplus+Fastnav" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b9686c72-1902-4868-88d1-6587fd24a57c", + "last_modified": 1557433916770 + }, + { + "guid": "/^((\\{d389cdfe-843e-44cb-b127-441492e46e63\\})|(\\{1340c760-3f4c-4428-b2c0-88821a84de2b\\})|(\\{38524a16-a73d-4a8f-8111-f9347bb5266c\\}))$/", + "prefs": [], + "schema": 1557258104673, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549740", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Add-ons executing remote code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2feeb46a-6784-4c6e-8c07-e120bec00b14", + "last_modified": 1557394160484 + }, + { + "guid": "/^((adsmin@vietbacsecurity\\.com)|(\\{efdefbd4-5c30-42c3-ad2b-4c49082ec4cd\\})|(\\{63d83b36-a85c-4b51-8f68-8eb6c0ea6922\\})|(\\{4613a1ed-6cb1-410b-a8b1-3f81f73b6e00\\})|(\\{90b1aef7-7a52-4649-b5ca-91b5e81b5eab\\})|(\\{d6e2e76d-edff-416b-8c04-53052ff9fec7\\})|(\\{43af2e0f-b5ce-409b-9ee6-5360785c9b08\\})|(\\{e45fa96d-8b74-4666-86de-3bbfb774a74f\\})|(\\{4f8332b6-6167-4b7f-a1f9-61d8eb89b102\\})|(cpcnbnofbhmpimepokdpmoomejafefhb@chrome-store-foxified-14654081)|(developios89@gmail\\.com)|(\\{d82da356-1fa8-4550-958a-bd2472972314\\})|(\\{1dfbd1c3-a8ca-4eb3-8747-d30bfd20ecd5\\})|(\\{6f9fa22a-128f-4d1b-8ef5-d20a44d24245\\})|(\\{5f6af572-35c1-44d7-9d0f-dffbb62fcafe\\})|(developper@avast\\.com)|(\\{886a6486-37b3-4bcd-891b-fd0e335e7b1a\\})|(\\{886a6486-37b3-4bcd-891b-fd0e355e7b1a\\})|(\\{d1cd26ff-fde7-46a4-85cc-48e3bb7e9e8d\\})|(\\{ae11d5cc-8efb-43a0-89bf-e5a779b4fa40\\})|(\\{aca140ce-8249-4e6e-8e2c-cd5b1c987441\\})|(\\{f68b2ca7-0d2c-44cc-afc8-a606a896c467\\})|(\\{321db3c3-8cfd-49f1-99de-fcdc3485b379\\}))$/", + "prefs": [], + "schema": 1557222463147, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549558", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "More Keyloggers" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b62c9ee1-d66f-4964-906e-2a9b07e3fdc1", + "last_modified": 1557222511299 + }, + { + "guid": "artur.dubovoy@gmail.com", + "prefs": [], + "schema": 1557162612874, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549444", + "why": "This add-on violates Mozilla's policies by executing remote code.", + "name": "Flash Video Downloader" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "16.3.9", + "minVersion": "16.3.5" + } + ], + "id": "d7ca07b4-9c97-4f49-a304-117c874ff073", + "last_modified": 1557162636319 + }, + { + "guid": "{93d460ee-879f-4d8f-8599-a1c69ed59ec2}", + "prefs": [], + "schema": 1556912498785, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1549069", + "why": "This add-on violates Mozilla’s add-on policies by collecting ancillary user data.", + "name": "Browser Security &Adblock" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "95cfb43b-72c3-4fb3-a0f2-fc975aff398d", + "last_modified": 1556977392946 + }, + { + "guid": "{fc4c96b2-4eaa-4221-86a6-392dc1eb919a}", + "prefs": [], + "schema": 1556797012258, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1548536", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "AspectResolver" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3f6acc8a-f46c-42cb-8220-48c2e5885bd3", + "last_modified": 1556808070195 + }, + { + "guid": "/^((premium-enhancer@ext\\.com)|(notif-rm-unlisted@ext\\.com)|(search-updater@ext\\.com)|(updt-lite-unlisted@ext\\.com)|(coldsearch@ext\\.com)|(reader@ext\\.com)|(local@ext\\.com)|(fptool@ext\\.com)|(gflash@ext\\.com)|(advplayer@ext\\.com)|(yfp@ext\\.com)|(ytbenhancer@ext\\.com)|(yoump@ext\\.com)|(floating@ext\\.com)|(ytbhelper@ext\\.com))$/", + "prefs": [], + "schema": 1556792823258, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547311", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "Various fake player/search add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9efe3274-2bd2-44a3-aa7f-92934581470b", + "last_modified": 1556794746654 + }, + { + "guid": "{a38141d9-ef67-4d4b-a9da-e3e4d0b7ba6a}", + "prefs": [], + "schema": 1556787949626, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1548473", + "why": "This add-on violates Mozilla's add-on policies by changing search behavior without users consent or control.", + "name": "ReStyle" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b0ff609b-c98e-4d29-8323-61c3e064ec9c", + "last_modified": 1556791242742 + }, + { + "guid": "{0beedf0b-dc79-48bd-adff-2ed36acdb806}", + "prefs": [], + "schema": 1556787897696, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547930", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "71475499-ca6f-4b71-8bef-2b95cf59ee30", + "last_modified": 1556787931409 + }, + { + "guid": "{c11adb01-56bc-44d6-ac05-6f364e2afe01}", + "prefs": [], + "schema": 1556787838618, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547934", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e6371474-8681-4498-8e89-421f25fd2e12", + "last_modified": 1556787897686 + }, + { + "guid": "{a1f6fa05-26cd-4399-a97a-7996067d04b0}", + "prefs": [], + "schema": 1556739699995, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1548134", + "why": "This add-on violates Mozilla's add-on policies by changing search behavior without users consent or control.", + "name": "TC" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3659d4a2-3121-45cd-b8b6-5b2c96ebc17f", + "last_modified": 1556787838607 + }, + { + "guid": "{c65b18e1-cd3d-4773-a901-15a0753e7d81}", + "prefs": [], + "schema": 1556224830338, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1546994", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "PrincipalInterceptor" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "af62a050-b136-4d56-ab3a-af85a2510bc4", + "last_modified": 1556224874229 + }, + { + "guid": "{4bf110f8-5f50-4a35-b7fa-64228bfa2d0b}", + "prefs": [], + "schema": 1556224790813, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547048", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Predicate Property" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5968da82-1d7e-407e-bf93-5d2247ce55c1", + "last_modified": 1556224830329 + }, + { + "guid": "{0bd4e0af-664d-4273-a670-7cb3d0b5a4a5}", + "prefs": [], + "schema": 1556224295744, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1547046", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "ConsumerWatcher" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6a6cc6fb-dce1-49cd-b624-7b44afacf157", + "last_modified": 1556224790803 + }, + { + "guid": "{bbddf452-1a72-4a5d-a833-0416ac7fd76f}", + "prefs": [], + "schema": 1556197615318, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1546989", + "why": "This add-on violates Mozilla's add-on policy by executing remote code.", + "name": "AV Scanner (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "20c25379-aed8-4ab4-9e7f-f2d3f5d948a7", + "last_modified": 1556206274610 + }, + { + "guid": "/^((\\{1601769e-0b0d-4c43-97a7-723ce374996b\\})|(\\{d714118b-5cdd-4829-9299-1066fecc0867\\})|(\\{e8671db6-24be-4744-808c-a63fb744ccca\\}))$/", + "prefs": [], + "schema": 1556133515829, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1545834", + "why": "This add-on violates Mozilla’s add-on policies by overriding search behavior without user consent or control.", + "name": "XPC and Tabs" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1b536121-fd82-4639-bc70-30d7060e42d3", + "last_modified": 1556133806451 + }, + { + "guid": "{3f5f741d-a980-4b58-8552-b1ae328841f4}", + "prefs": [], + "schema": 1556099103820, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1546480", + "why": "This add-on violates Mozilla's add-on policy by intentionally weakening website security and adding fraudulent content to web pages.", + "name": "Google Translate (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ae288e5e-78d5-4b49-8b4d-fa9f25c3baa2", + "last_modified": 1556112119390 + }, + { + "guid": "{3fab603e-3ee1-1222-a859-5f85a3441216}", + "prefs": [], + "schema": 1555527937277, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1545213", + "why": "This add-on violates Mozilla's add-on policy by overriding search behavior without user consent or control.", + "name": "Add security (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "74ad8c8b-a65a-470c-aa2f-ca14e1e8774d", + "last_modified": 1555528639011 + }, + { + "guid": "/^((\\{880cacfe-5793-4346-89ce-fbbd368d394c\\})|(\\{f0780038-50b9-11e9-9c72-4ba2d8f2ec9f\\})|(\\{22ffe411-2b0e-11e9-87f9-c329f1f9c8d2\\})|(\\{cf4bae43-026f-4e7e-a85a-952a7ca697a1\\})|(\\{17052516-09be-11e9-a008-03419f6c8bc6\\})|(\\{333fb3de-18a8-18e8-b6d3-e73213911efb\\})|(\\{aa4abac2-1ffa-12aa-bbdd-9305cb2c1254\\})|(\\{72222e70-2fd6-11e9-956b-27f7787b8d2d\\})|(\\{637212d8-3484-11e9-9812-005056b22b42\\})|(\\{4a222e60-31de-1eca-8476-37565daf6afb\\})|(\\{7fc6d222-48d5-11e9-b586-17e94c73a1b1\\})|(\\{e111c358-121b-13fa-bf23-bb57da32d184\\})|(\\{9674445c-8dff-4580-96b2-99442a7ae9af\\}))$/", + "prefs": [], + "schema": 1555525532852, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1544315", + "why": "This add-on violates Mozilla's add-on policy by executing remote code.", + "name": "Various" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cbd7444f-e62a-4639-b172-845548b6d4a7", + "last_modified": 1555527929174 + }, + { + "guid": "{674fff65-6cd0-488a-9453-fb91fc3d7397}", + "prefs": [], + "schema": 1555406446874, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543924", + "why": "This add-on violates Mozilla’s add-on policies by executing remote code.", + "name": "Assistant" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8aff4cb0-4d5f-4e74-8db7-b04f616c3b60", + "last_modified": 1555503879816 + }, + { + "guid": "{40cd7fd2-a3e4-43f9-9d86-0e0a70376203}", + "prefs": [], + "schema": 1555184501005, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1544268", + "why": "This add-on maliciously injects remote code for execution.", + "name": "Scan Tech" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "881d3476-f18a-4560-b065-cded406783d2", + "last_modified": 1555228377222 + }, + { + "guid": "{8ee8602c-aba6-4e2a-9faa-1724c3f4f9ba}", + "prefs": [], + "schema": 1555102738661, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1544205", + "why": "The add-on is maliciously loading remote code for execution.", + "name": "Service Proccesor" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "94621e2f-28a0-4b18-97c6-5f6203f5912e", + "last_modified": 1555161086175 + }, + { + "guid": "{d8b03707-e39f-4b17-8e56-56354fb14af5}", + "prefs": [], + "schema": 1555100104657, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1544103", + "why": "This add-on maliciously injects scripts, violating our policies.", + "name": "Interruptible Observer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3a921aa8-d44a-4272-be63-0fd102577f59", + "last_modified": 1555100575898 + }, + { + "guid": "{132cb2fd-a6ae-45d2-84cf-b48d591f037d}", + "prefs": [], + "schema": 1555099951278, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543922", + "why": "This add-on injects remote scripts and overrides search behavior without user consent or control. It also masks as an add-on with a different purpose.", + "name": "Player" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "371796e4-387a-4dd0-9ddc-47ba1dd85be7", + "last_modified": 1555100104648 + }, + { + "guid": "H.264.Addon.Test@firefox.com", + "prefs": [], + "schema": 1555099832659, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543919", + "why": "This add-on exfiltrates user data without consent or control and may be misleading the user into believing this is an add-on by Firefox.", + "name": "H.264 Video Codec Fake" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "53ef1aad-7bdb-4f4e-8d46-55d6ec2d78ab", + "last_modified": 1555099951269 + }, + { + "guid": "{608f71eb-5bd6-45d8-bc93-b9e812cf17b7}", + "prefs": [], + "schema": 1555099501294, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543801", + "why": "This add-on maliciously injects remote scripts, violating our policies.", + "name": "Consumer Tech" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ea00841e-8dc2-4e11-9119-7a599e47d800", + "last_modified": 1555099832650 + }, + { + "guid": "/^((\\{8220ccaf-15a4-4f47-a670-a4119a4296a4\\})|(\\{9da72c11-44d7-423c-b19c-c75cd6188c3e\\})|(\\{99d0e4d0-c5ef-4567-b74c-80c5ed21ad99\\}))$/", + "prefs": [], + "schema": 1555011689511, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543848", + "why": "This add-on violates Mozilla's add-on policy by overriding search behavior without user's consent or control.", + "name": "Search overriding add-on" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "03554696-58fe-4b90-89d1-72b72f88f82e", + "last_modified": 1555069748234 + }, + { + "guid": "/^((\\{b37f383f-e60f-4eb1-ac0f-9147e0e8f2f7\\})|(\\{8ad567d2-3fe2-446b-bce9-a3acbe878dba\\}))$/", + "prefs": [], + "schema": 1554997740201, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543673", + "why": "These add-ons are overriding search behavior without user's consent or control.", + "name": "Hash" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "036e2e7d-5403-49be-92cf-b5187ceef7ec", + "last_modified": 1554997910212 + }, + { + "guid": "{be6ab6a9-7004-4c5c-8df9-8d36122d8b14}", + "prefs": [], + "schema": 1554997613727, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543705", + "why": "This add-on injects remote scripts with malicious intent.", + "name": "asin" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6fd3ab94-8e38-47f3-b129-3ca8396d0a22", + "last_modified": 1554997740187 + }, + { + "guid": "/^((\\{61f433aa-45fd-42a9-9c90-c1d7820661d5\\})|(\\{86cd46b6-433a-439c-bff2-722846709f44\\})|(\\{98e126a4-4e70-4300-b893-3b2cca19bc9b\\})|(\\{8f42dc3a-1c46-4fc2-8c7f-dd76a63b1cf7\\})|(\\{a24d3582-2fc2-475c-8440-335736e17c6e\\})|(\\{cf0b5280-cd08-465d-ad7d-70308538f30a\\})|(\\{936f3c79-5dc9-4694-bca8-47932de3357a\\})|(\\{e48d5888-8736-4858-83ba-e816378ffef8\\})|(\\{5305f89c-ceec-4217-8bae-c9c376c7462b\\})|(\\{a2ac47e5-d225-4718-9b57-18a932f87610\\})|(\\{79f60f07-6aee-42cd-9105-c0a52f401316\\}))$/", + "prefs": [], + "schema": 1554981266268, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543664", + "why": "These add-ons exfiltrate user data while masking as a legit add-on.", + "name": "Adobe Flash Player Fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fc38e782-d55b-4fb7-8f9c-374aa18af09c", + "last_modified": 1554997613713 + }, + { + "guid": "/^((\\{0913599d-3094-44a7-8cc2-b8467d5afc7c\\})|(\\{7bee7f1b-d8ad-424d-807d-e69e6634988e\\}))$/", + "prefs": [], + "schema": 1554898779160, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543462", + "why": "This add-on violates Mozilla's policies by exfiltrating user data without their consent or control.", + "name": "Malware exfiltrating user data" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0d86ba71-7baa-4cb3-b3b8-da4ccdfa36b9", + "last_modified": 1554976164809 + }, + { + "guid": "/^((\\{9946bf2f-0aef-4040-bc57-cdae2bde196a\\})|(\\{d511784e-d088-4fce-b77c-14c186f08641\\})|(\\{fe59312a-97bd-4ca7-bce3-b0db95b1e251\\}))$/", + "prefs": [], + "schema": 1554897771015, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543118", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without user consent or control.", + "name": "Invert (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ed01b7e5-73d1-42a6-9fc8-af2d83879854", + "last_modified": 1554898652923 + }, + { + "guid": "{c75432cb-980d-4e64-98c8-d7947b382a2c}", + "prefs": [], + "schema": 1554897109129, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543255", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Concrete Tech" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7fd0eb9c-9f6c-40ea-ba39-645cafb1d5a0", + "last_modified": 1554897390260 + }, + { + "guid": "/^((\\{6a99a9ec-f149-4ad3-b644-15e44290d00c\\})|(\\{cd9d1582-13dc-4ce1-9c83-4aaa31c6bc36\\})|(\\{3414a827-ee54-4331-85eb-736a824bb7e0\\}))$/", + "prefs": [], + "schema": 1554896836686, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543181", + "why": "This add-on violates Mozilla's add-on policies by overriding search behavior without users' consent or control.", + "name": "Drag (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a4f0ac78-ba85-4b5a-9d1f-f3f2c6ae4f7c", + "last_modified": 1554897104624 + }, + { + "guid": "/^((\\{4b6e66db-ee0b-4fc3-abe6-b97cb4798faf\\})|(\\{8aff5f41-86f8-40f1-896d-954eae7fb670\\}))$/", + "prefs": [], + "schema": 1554817097951, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543049", + "why": "Search hijacking", + "name": "Fit" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8a7efe6b-8670-4714-b4b2-08ce5f202ee7", + "last_modified": 1554818136441 + }, + { + "guid": "{81425b21-cc8c-42d0-98e8-69844bcb7404}", + "prefs": [], + "schema": 1554811583185, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543048", + "why": "Remote Script Injection", + "name": "Tech Chip" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "dafcc63d-37e0-42e2-b439-59727cf9de48", + "last_modified": 1554811754967 + }, + { + "guid": "{d85aa6ef-639b-43a1-8560-ddeb59935d10}", + "prefs": [], + "schema": 1554803024628, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1543047", + "why": "Remote Script Injection", + "name": "BatchSerialize" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "204b7b05-d8e1-4de3-86f9-fcf95edb40c0", + "last_modified": 1554811583171 + }, + { + "guid": "/^((\\{bf163ed1-e9f9-4c98-ae4b-8391133472d1\\})|(\\{ec283482-2d66-49b2-9dc5-0d03bcbffe65\\})|(\\{0a19856e-4168-4765-a8ab-a3a34fa88ec1\\})|(\\{e2019dd1-4591-42e2-864a-535a99972b1a\\})|(\\{88a65f0c-f952-41f0-8868-f22fa12597b3\\}))$/", + "prefs": [], + "schema": 1554754247858, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540998", + "why": "Exfiltrating user data to a remote site while masking as a different add-on", + "name": "More Flash Player Clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ddd97fae-7040-4758-811f-1dd53116e6ab", + "last_modified": 1554755273775 + }, + { + "guid": "/^((\\{743fc0de-1085-4078-8611-848024989608\\})|(\\{02acc043-f402-4e48-aa15-56ee1364e17c\\})|(\\{1b7892ab-6dbe-49d1-9c71-bbb70458c830\\})|(\\{25b3b5bc-b77a-49d1-ab56-c0e760fe02ff\\})|(\\{2c78aa89-8cdd-4a96-821a-e35628aea2fb\\})|(\\{2f154b99-05c2-4629-b687-f2aa031d9f65\\})|(\\{36df6f78-16c4-42c2-a6b8-9210a2953739\\})|(\\{40ada62f-72a8-46b7-8e50-4153f660ce34\\})|(\\{49f58462-fc24-472c-b85a-4a3dbbf48741\\})|(\\{4a8cb3fd-0400-47b3-a799-9f2964229bfa\\})|(\\{5429f6da-d7fe-4f1b-a85e-6dc721ec0037\\})|(\\{74480b2f-1198-45b3-86b3-ca0778482216\\})|(\\{777f1169-a824-459d-8a2d-ca2ffaf59424\\})|(\\{81e610be-656a-4a71-866d-dd94b5096c60\\})|(\\{81ee3e70-b6e4-44d0-b5c2-94ded26bb5ac\\})|(\\{881c71c1-6800-4e8b-89de-0d14ef67d588\\})|(\\{9b5d7f59-be9c-4f1e-bf0c-31f085c17726\\})|(\\{9eff0ead-25a4-429c-b4b2-280ba3c6f2d9\\})|(\\{ad1b7e87-e260-4aee-a602-ef234743443e\\})|(\\{b54a530a-cacc-4c76-a7c3-feafd4ce0b13\\})|(\\{bd0d7634-770e-4d9f-8309-d264a5fbbfa9\\})|(\\{bdf16cc8-3da6-4317-a1eb-2ab8adce6b66\\})|(\\{bf484a7f-49fd-4681-afa5-8a063d010a14\\})|(\\{c7cf6d86-207b-4231-a96a-bbfdc9fe59aa\\})|(\\{d33f83c2-dbc6-41d2-a8b9-28fdaa96985e\\})|(\\{d71757ee-edc7-44d5-b536-cb0370d7d9f6\\})|(\\{daf86db4-7dd4-47d4-a1d1-7c31f6b9bbe3\\})|(\\{e27060a0-5fb5-4844-b446-d2655d7681fa\\})|(\\{fae0d133-05dd-44e6-88e1-e218ca2b2caf\\})|(\\{fbf69005-55d8-4360-a562-255a8c285fea\\})|(\\{fd6d1b53-89f5-4d91-9234-fb3e1b067c1b\\}))$/", + "prefs": [], + "schema": 1554753973658, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540998", + "why": "Exfiltrating user data to a remote site while masking as a different add-on", + "name": "Adobe Flash fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e12a97c7-2c83-4e1c-a2c3-66a653bc6048", + "last_modified": 1554754247845 + }, + { + "guid": "{ea173fdc-f27a-482a-8a0a-61fd1aa2ee2e}", + "prefs": [], + "schema": 1554744706554, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1541005", + "why": "Masks as a legit add-on and includes a remote script that is against our policies", + "name": "InterpreterInvocation" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8742ec6a-2e51-4e94-bc6a-653dac08521b", + "last_modified": 1554753973644 + }, + { + "guid": "{16768af9-4120-4566-95cf-c4234effa084}", + "prefs": [], + "schema": 1554733900697, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540999", + "why": "This add-on changes the search settings in a way that is not allowed per our policies", + "name": "Unknown search hijacking add-on" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3fb0ed8e-6e5d-489e-8c9d-b6f48705a742", + "last_modified": 1554736392840 + }, + { + "guid": "{35b9640e-ebbb-44b7-85af-d9ec3af3c6a6}", + "prefs": [], + "schema": 1554730607333, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540690", + "why": "Malicious remote script injection", + "name": "Patagonia Bit" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0eb3a151-ca6b-4dbb-81b3-c10635660c84", + "last_modified": 1554733900683 + }, + { + "guid": "{d574e1f8-537d-4b6c-97bb-9f7a138f4d67}", + "prefs": [], + "schema": 1554728269766, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540686", + "why": "Injects remote scripts and masks as a different add-on", + "name": "DistributedConsumer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a158055b-3387-4961-a4a3-a820d9299e15", + "last_modified": 1554730607318 + }, + { + "guid": "one-search@mozzilla.xpi", + "prefs": [], + "schema": 1554666099983, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1542208", + "why": "The add-on violates Mozilla's add-on policies by overriding search behavior without user consent or control.", + "name": "One Search" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1344c583-9593-412f-a565-c6cc96a07c28", + "last_modified": 1554718843138 + }, + { + "guid": "/^((\\{00b4b65b-79d9-4e92-bc1e-2b926918b91c\\})|(\\{0cb66591-e935-47e4-95c2-3063786f6555\\})|(\\{6cf25884-f86d-4a4e-a924-d95282ce5b71\\})|(\\{22cce9c6-a1de-457f-8938-c981b976b6f4\\})|(\\{89d99d4c-e7c4-4601-91a8-216e597a826b\\})|(\\{998d3ac7-b475-410e-ad3d-2eeb526c1853\\})|(\\{9423e8df-6200-45c0-877a-479c46e91b30\\})|(\\{64937e0b-6e00-4d5f-bf19-190d6614aae2\\})|(\\{91507dc4-c005-4534-80af-d8fbdeac29ed\\})|(\\{a2247e60-7b89-4857-a2fa-0eaee1cad460\\})|(\\{c9c28751-5865-449f-8e45-b3363edf9fb7\\})|(\\{cdfd004f-cddc-4ad7-8e2d-a58457e42b1f\\})|(\\{d3e7f35d-7d9f-4d38-9a2b-1581f6b3e870\\})|(\\{df574ffe-cce0-42db-857b-627cb164a4d4\\})|(\\{e06afe6e-ed52-40f8-82bf-d070a37387fb\\})|(\\{e7e7fb96-cfab-4a5b-85fe-20f621e1bc2e\\})|(\\{e12e5afd-bd1e-43c6-9288-321dc485cb1c\\})|(\\{e92d8545-0396-4808-96de-830c61c0d1b3\\})|(\\{e883b012-1987-4f37-8053-02e59e20c242\\})|(\\{ed3386c6-76c6-4786-a37b-9816d5f2a260\\}))$/", + "prefs": [], + "schema": 1554462951082, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1541662", + "why": "These add-ons violate Mozilla's add-on policies by overriding search preferences without user control or consent.", + "name": "Search overriding malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a1c376fe-20c5-4da3-9126-3fe95b874dce", + "last_modified": 1554463075420 + }, + { + "guid": "/^((\\{6ab41e83-2a91-4c2a-babb-86107a1d1f75\\})|(\\{d84a9d0d-7a31-459e-b45a-2ad111884d1f\\}))$/", + "prefs": [], + "schema": 1554293659259, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1541405", + "why": "This add-on violates Mozilla's add-on policies by overriding search settings without user control or notice.", + "name": "PopWin (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "04b2954e-4f83-4557-968e-2139a277bf1c", + "last_modified": 1554301860877 + }, + { + "guid": "/^((@searchlock-staging-automation)|(@searchlock-automation)|(@searchlock-fx)|(@searchlock-staging)|(jid1-vRJA7N8VwBoiXw@jetpack))$/", + "prefs": [], + "schema": 1554293340413, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540868", + "why": "This add-on violates Mozilla's add-on policies by executing remote code and overriding search preferences.", + "name": "SearchLock" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "aacb25e1-71c5-4bee-ad16-e39e732210ba", + "last_modified": 1554293606641 + }, + { + "guid": "{03dfffe0-509f-11e9-aa00-e7e13d49f3de}", + "prefs": [], + "schema": 1554290590697, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540113", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Addon (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9c75fe89-7011-47ad-b213-57f5a81a4c89", + "last_modified": 1554290693618 + }, + { + "guid": "{e555c358-121b-13fa-bf23-bb57da32d184}", + "prefs": [], + "schema": 1554290541557, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540111", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Security (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1809ea7a-8155-4ae7-8c83-ee7c749d30f5", + "last_modified": 1554290590689 + }, + { + "guid": "{a9c33302-4c97-11e9-9a9d-af400df725e1}", + "prefs": [], + "schema": 1554147700324, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1539514", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Security (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5b4e4e75-cc96-4ca9-aa9f-6a2d2f6cd96a", + "last_modified": 1554290541548 + }, + { + "guid": "{a3f765c3-8dde-4467-ad6e-fd70c3333e50}", + "prefs": [], + "schema": 1554119395186, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538143", + "why": "Remote script injection", + "name": "Angelic Bit" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "93dc42cc-4ff3-460d-a8f2-12f1d947b530", + "last_modified": 1554119427564 + }, + { + "guid": "{91f77263-866e-4acb-a569-f66ac47889f8}", + "prefs": [], + "schema": 1553974898434, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1539226", + "why": "Remote script injection", + "name": "Bit Apex" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c528f48a-9b2c-48ca-8b4a-eac442cc0bd0", + "last_modified": 1554119395177 + }, + { + "guid": "{2256fabf-19f1-4e12-9951-5d126dd9e928}", + "prefs": [], + "schema": 1553899022464, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540086", + "why": "Search hijacking", + "name": "Twit" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7c705f73-9d1d-4ee9-ad11-347d18729adb", + "last_modified": 1553900528324 + }, + { + "guid": "/^((\\{00f77164-eca9-4353-916d-8ea493a54c8d\\})|(\\{0716b2a5-8181-45b8-b675-915e38903761\\})|(\\{26124967-7e32-4577-b998-7296c68d3eb9\\})|(\\{273052bc-fc67-4fc1-a6fd-e62acc3ddad1\\})|(\\{4b5f53ac-36ac-4018-80cb-f1106f60ef96\\})|(\\{61065f61-51aa-462c-aca0-f1addbfa202b\\})|(\\{63383006-d617-4a00-9ca7-30a6864782ee\\})|(\\{7629c987-59ea-4e2f-bcde-b55646ecd268\\})|(\\{78e8c8fa-32ce-432b-9a40-b615bff7cd96\\})|(\\{8e9c05df-e0f5-479f-abb9-858650cb471e\\})|(\\{947f1ac0-09f2-4016-a345-dad0d2ee8f57\\})|(\\{9b9f8124-47bc-4699-8557-45573995b0af\\})|(\\{ab159932-d1dd-4d16-9332-8302a01e0ced\\})|(\\{b7340504-f6ba-43cb-8bd6-5ead88d29898\\})|(\\{bcb9265f-20fd-4311-a33f-212c2d08043a\\})|(\\{f8701822-2814-4d5d-af01-cf7fde4fd510\\}))$/", + "prefs": [], + "schema": 1553898687988, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1539910", + "why": "Data exfiltration, is not actually a flash player", + "name": "Adobe Flash Player fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "64fc6369-b504-4920-abab-f2cf3cc5424a", + "last_modified": 1553899022456 + }, + { + "guid": "/^((\\{7dd03112-82a0-4c82-9957-117dedaac14a\\})|(\\{59fd3cac-1a4d-4f0f-a129-c241b203eb51\\}))$/", + "prefs": [], + "schema": 1553897736388, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540287", + "why": "Search hijacking", + "name": "Song" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b32b14f5-0024-48fb-a4b6-1496add1dac0", + "last_modified": 1553898687980 + }, + { + "guid": "/^((\\{70c2cef0-6cc6-41b8-ad6b-bbd11182a101\\})|(\\{a0366612-376e-47e3-b5fa-b805c7176088\\}))$/", + "prefs": [], + "schema": 1553810805293, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1540055", + "why": "Search hijacking, masking as legit add-on", + "name": "Pix" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d78262b0-ecfc-475e-9759-f7319451cb43", + "last_modified": 1553847044919 + }, + { + "guid": "/^((\\{10f1b84d-79ca-41d0-97f6-abb53cec0765\\})|(\\{7891c029-0071-4534-b7f0-7288f14ee0ad\\}))$/", + "prefs": [], + "schema": 1553810612956, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538476", + "why": "Remote script injection, search hijacking", + "name": "FX" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "50197dbd-71bc-492f-a0f1-6658ec454df4", + "last_modified": 1553810696456 + }, + { + "guid": "/^((\\{058769c7-784e-47a9-a2c4-dfd81bbf6c8c\\})|(\\{2a58598a-f951-4fb0-af2b-12fb7482bf33\\}))$/", + "prefs": [], + "schema": 1553810234714, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1539910", + "why": "Keylogger", + "name": "Fake adobe flash player" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e1355888-e1cd-4d21-9652-c3000662ed88", + "last_modified": 1553810612947 + }, + { + "guid": "/^((\\{54c7e57f-8ef0-48d5-92a0-6e95d193a12c\\})|(\\{32d262da-e3cd-4300-aa0b-c284eb4e17bf\\}))$/", + "prefs": [], + "schema": 1553802101395, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538536", + "why": "Search hijacking, masking as legit add-on", + "name": "TxP" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fa6c7cdb-e477-4478-8dd4-3e1106be6aa3", + "last_modified": 1553810234705 + }, + { + "guid": "/^((\\{36261798-4c2a-4206-89cc-6c28932b2e98\\})|(\\{b2b9bb64-78d5-43ca-b0cf-a9ee8460521b\\}))$/", + "prefs": [], + "schema": 1553616425198, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538234", + "why": "Clone of Feedbro with added remote scripts and search hijacking", + "name": "Feedbro Fake" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e00b2639-8a4f-427a-80d8-7c4937c58f31", + "last_modified": 1553620435398 + }, + { + "guid": "new-tab-search@mozzilla.xpi", + "prefs": [], + "schema": 1553616311575, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538500", + "why": "Search hijacking", + "name": "New Tab Search" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a4dca16a-9fa1-4b55-899c-0f8d5eda1a57", + "last_modified": 1553616386570 + }, + { + "guid": "{a9c33302-4c97-11e9-9a9d-af400df725e3}", + "prefs": [], + "schema": 1553616259023, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538141", + "why": "remote code execution", + "name": "Fake Security add-on" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0c09f067-6e5f-4ee0-9040-08b4297ebe02", + "last_modified": 1553616311567 + }, + { + "guid": "{7ab5c514-4ebe-22e9-a925-9b7c7317c373}", + "prefs": [], + "schema": 1553548654429, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538266", + "why": "remote code injection", + "name": "Eval" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a1f04f09-f4d5-4875-b4b1-a2c772178e8e", + "last_modified": 1553616158755 + }, + { + "guid": "{bc919484-f20e-48e2-a7c8-6642e111abce}", + "prefs": [], + "schema": 1553519202849, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1538962", + "why": "Inserting monetization iframes and masking as a legit add-on. Contains patterns for known malicious add-ons.", + "name": "Pinterest Save Button clone" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7abbecfb-5512-47d1-ba9b-96d6a61b85ee", + "last_modified": 1553548325261 + }, + { + "guid": "/^((\\{157cd8f9-48f0-43a1-9bcf-c4316753e087\\})|(\\{157cd8f9-48f0-43a1-9bcf-c4316753e086\\})|(\\{157cd8f9-48f0-43a1-9bcf-c4316753e088\\}))$/", + "prefs": [], + "schema": 1553186907495, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1537895", + "why": "This add-on violates Mozilla's add-on policies by overriding search settings.", + "name": "SD App (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e263fbec-7155-442f-aa82-cdf218f9e3d7", + "last_modified": 1553193746700 + }, + { + "guid": "/^((\\{1c94bc8a-3ac1-12e1-aae7-0b314772229c\\})|(\\{8a22255c-4737-11e9-a86b-0bb66337cb31\\})|(\\{3fab603e-3ee1-1222-a859-5f85a3441216\\}))$/", + "prefs": [], + "schema": 1553166786114, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535655", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "'Security' add-ons (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6cf1b676-f0b8-4fea-8a5f-64957650dc2e", + "last_modified": 1553172061896 + }, + { + "guid": "{28ac81f1-b04d-448f-94be-1b8cc8fbd58d}", + "prefs": [], + "schema": 1553079961735, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1536513", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "UtilsBridge (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f60b3eec-b8b8-4bd7-8d2b-3f7912c3451f", + "last_modified": 1553080348264 + }, + { + "guid": "/^((\\{9332d73d-7991-46bf-8b67-6db1a21f0167\\})|(\\{b33715d3-eff8-4186-a252-0af5094b8644\\})|(\\{eb7aff78-6145-4a31-a7f5-f3c353ddb312\\})|(\\{6c5cd693-2919-4458-b776-2ac5b6ab1cb0\\})|(\\{daacefee-aaba-4f10-8d4d-059904d8a153\\})|(\\{94d8d504-838c-4392-9971-cd2f6e21ca21\\})|(\\{6574bb31-c971-454f-b08c-a75bfee00855\\})|(\\{1688ecb0-e382-481f-8c70-541d70bdd2e9\\})|(\\{f7b9f777-7b01-4f73-8eb8-f2ad85d4da1c\\})|(\\{598d7ac6-1789-4573-ae6a-5798ed7f6d83\\})|(\\{c0eb4d03-d18e-40bf-b21b-8237ee1bed76\\})|(\\{d0513185-1f20-4045-a232-f3a4252af379\\})|(\\{9ae8269f-eea1-4097-87fd-b7d2f102184d\\})|(\\{5683f95b-2517-4ca7-9d19-83d7f309b62a\\})|(\\{013d3691-0dd6-471b-bf0d-2750d7406a22\\})|(\\{ae73a262-1a27-4d1d-9be7-4b41a84dfd23\\})|(\\{1d92fc5d-5353-401f-8c5f-373b3b6dae67\\})|(\\{e8a81b54-3728-4a9c-8c63-18ef803ef9be\\})|(\\{d604961b-3a3d-4f60-87ae-35977c10b787\\})|(\\{cbe9b620-fac0-407a-b3be-b0a61b319ef8\\})|(\\{1cdb403e-11c7-421b-9c87-0c0d90263626\\})|(\\{f5fa0bfe-a981-48ff-b809-8faa3126f0bc\\})|(\\{7dc6d0d2-b2f0-4334-979d-6ebeff77785a\\})|(\\{13623b47-de82-4226-85f8-d3ae343e619b\\})|(\\{db7b6ea7-2605-44c7-807b-2419d7eec531\\})|(\\{b9298a4a-acca-446d-aa72-d37f5e1576cd\\})|(\\{2e689bc0-735f-445c-bcc7-2cc495f5eb40\\})|(\\{04acd977-4c2b-4162-af33-8c585bea90c5\\})|(\\{2436dde0-3230-4933-9020-c15b3b9e693b\\})|(\\{dcb556aa-ef6e-4778-9f60-c5ae18a72cfb\\})|(\\{5a24385f-ada4-455d-95ad-62cb6256360d\\})|(\\{97f88a13-5b79-4345-a85e-2560d54f577c\\})|(\\{12f4cde8-7d1c-4a9e-9ef7-431f5ecd53a4\\})|(\\{18a93813-7deb-40cf-b3a6-402369e6d817\\})|(\\{9cee5c92-eb1e-4892-86ff-d2d1c627f5b9\\})|(\\{cb1c544e-d444-4c85-8224-64aa26e82230\\})|(\\{1c3b247f-2ef4-4483-93a6-0a3da7bc3548\\})|(\\{1f6913f2-dead-4f96-bf96-0e64affd46ae\\})|(\\{109adc7d-f308-43a5-aa0e-07ccdc5dad2c\\})|(\\{7170e23c-c706-48a7-919f-c1c22548dbfb\\})|(\\{6aa47f05-1f3f-4798-908a-0ed01b2361e0\\})|(\\{33ef0e7b-15ea-4b08-a779-173731ac20b3\\})|(\\{a0361564-9461-4da0-8ec0-7dc6f418f707\\})|(\\{c12631ed-993a-4c2e-9bf0-37867ae40491\\})|(\\{00b79649-3f0e-4b12-a8f0-376a7b2db716\\})|(\\{89096e44-c8b4-4ce5-aad2-f5bac765f608\\})|(\\{6f4eff89-0e32-42bd-a5c1-354adc8417fd\\})|(\\{482c54ae-e080-4596-bf7c-ae972fdff9a3\\})|(\\{04868575-532f-4b43-9325-7e707c109c25\\})|(\\{042c3065-1291-4409-bae5-8d11f3c268e2\\})|(\\{126e7fc4-bf2d-4467-88b1-f3b17bc10da4\\})|(\\{cea21739-b9ce-46c7-ad3e-3607b1ff6538\\})|(\\{06eea1e7-a8be-4794-8cd5-ed12e5f86161\\})|(\\{50993bc5-011c-4322-b522-41e6f3997163\\})|(\\{219a2146-5d9b-472a-8630-4c96a0994bec\\})|(\\{1db94c2f-d957-4b12-a1dc-903bb28f5ff5\\})|(\\{2f7d887c-7d56-41fa-bf9b-eadf6e500427\\})|(\\{2b16f4ab-a2a9-43fd-8fd6-ad6f354b0d28\\})|(\\{034d2b14-29e6-42ad-b2db-2c31286f3fb7\\})|(\\{77058195-5ae1-440c-8f86-c60a96d12ca9\\})|(\\{8082ff2f-2151-4281-8134-1423e5961ca1\\})|(\\{9fa797e8-d7d4-4851-b7e9-76b61ecf046f\\})|(\\{87178fbe-17a5-4d8d-b5ed-48d17179b101\\})|(\\{f010d9e9-0878-4c83-af45-df966cbe8d26\\})|(\\{2aa8c5cd-18fa-4991-b354-d6f459efeca9\\})|(\\{4021839d-3f4a-4866-94fb-9fa809c5245b\\}))$/", + "prefs": [], + "schema": 1553024292304, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535088", + "why": "This add-on violates Mozilla's add-on policies by exfiltration user data and tracking online activities.", + "name": "Search overriding malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "478d4acd-3c01-4dd5-b784-4e06b69d1c05", + "last_modified": 1553079818962 + }, + { + "guid": "{781b89d4-fa53-45a1-bea4-151dd4c8b288}", + "prefs": [], + "schema": 1553013598703, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535280", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Drop Tech (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "81759002-967e-4856-9f55-61d7c30cdb3b", + "last_modified": 1553013656506 + }, + { + "guid": "{3b52063a-0683-4de2-b6e1-6192c78b6ba3}", + "prefs": [], + "schema": 1553013551744, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1536042", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Project Tech (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6a7f932a-3911-4884-8cb9-d282d282c0cc", + "last_modified": 1553013598695 + }, + { + "guid": "{47610aad-982f-4822-93ca-8c27dc96a13b}", + "prefs": [], + "schema": 1552938092086, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534773", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Tech Hand (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "12874e4d-28b5-4e98-8c33-b6cf5eb032bf", + "last_modified": 1553013551736 + }, + { + "guid": "amqp-dwn-all-vd@artur.brown", + "prefs": [], + "schema": 1552916969263, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1536052", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Video Downloader Plus (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a3f5ce2f-d8ef-4dae-9fce-1d7fb69d2b37", + "last_modified": 1552917123606 + }, + { + "guid": "/^((\\{1d758385-fddd-478e-85a0-13eef59f60e5\\})|(\\{1ec3e92a-fd3c-4e16-82e2-56d44bd7bdf4\\})|(\\{3dadda0d-d67c-4452-9475-246c33198192\\})|(\\{4a48f6a8-c9d6-4ae2-8513-a7e9fe785a56\\})|(\\{4d2da172-b160-42b5-9fea-0ede63e0ab52\\})|(\\{5bcd4feb-ce52-4e6f-9da6-eef2a75a4f70\\})|(\\{5eb45d74-0f46-4269-bc0e-8a2a49d64267\\})|(\\{7e8c27c0-b94c-4026-8068-2d152143f073\\})|(\\{9ede19b2-bb97-4d1c-abab-b1d72e7d4c74\\})|(\\{19abb7a0-fb4d-41ff-97d4-65f1680c1348\\})|(\\{25efbdeb-04fa-4998-a9f8-99c1293c7b7f\\})|(\\{0049a401-f02d-4d16-8b5e-5933e5855a4c\\})|(\\{65b91ca5-cd06-42a6-9637-8ecde3a69fd6\\})|(\\{146ec14e-f623-4cb2-88ed-7d3bb8101090\\})|(\\{790d2797-82f3-4bc3-8759-c00d426bbf2f\\})|(\\{865f42b5-e073-4a36-84b1-47d09096b48b\\})|(\\{90055a5b-45a8-45c1-b0a0-979ab2a9064f\\})|(\\{a4f5c163-7b64-46c4-bfd3-348ecc99873a\\})|(\\{a8c40ee7-a376-417b-8022-40909a10181b\\})|(\\{a1031346-14d3-464f-9e50-c30dfd88ad92\\})|(\\{abd16535-2fa8-4bfd-b84e-ed09c9c60e53\\})|(\\{b5ee8c58-b5e5-4ba0-a899-9a54a2f0e386\\})|(\\{b246bb42-577e-4587-adf2-7274b378b0b4\\})|(\\{bb43765b-fe06-4d50-9802-0c6742b220aa\\})|(\\{bf3f628d-9e52-4727-b940-054c65a5a304\\})|(\\{c6bc710d-8cc8-4224-9287-44ecfa452a81\\})|(\\{c232edce-83c9-4184-9782-22df800f65e2\\})|(\\{c5397be4-b756-45b8-a247-339846fada52\\})|(\\{c6675bc5-f7ea-4a11-8252-1152d3783ae3\\})|(\\{cc6a088b-5a84-4e48-8de8-d2f6be3abae7\\})|(\\{e6c12219-f67e-4ea0-a9c3-2c541febeff1\\})|(\\{eb3a7ef7-a4d0-49a4-8b21-2a91c1758100\\})|(\\{ec34588b-86b4-4de3-a3bf-f4d1d8386475\\})|(\\{f4fd8825-648f-4b63-a499-3fd702d42149\\})|(\\{fc4f31f6-c5ed-4afd-8c19-df96e107ce7d\\})|(\\{fe337ef5-bb69-44bf-82a8-ee5c13406165\\})|(\\{ff285a1c-5672-44c3-890e-6c4f25976b83\\}))$/", + "prefs": [], + "schema": 1552908996320, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535421", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "25f18cc5-6ecc-419f-b093-b79e9f261062", + "last_modified": 1552916969252 + }, + { + "guid": "{a4491aab-e273-4bc3-b45e-a7b9b9414a5f}", + "prefs": [], + "schema": 1552695264438, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534792", + "why": "Search takeover not according to policies, masking as a different add-on", + "name": "FFCop" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d2da9c45-59f8-4257-9d7e-07c4fa5de958", + "last_modified": 1552695747900 + }, + { + "guid": "/^((\\{0f6b717d-1625-4434-b770-5ae33eb64b16\\})|(\\{6d092be7-0bad-46af-9489-04e4b3988644\\})|(\\{7f6049d6-e8b0-4c42-8028-204d1458ddb6\\})|(\\{12b75028-c038-40bd-be5b-2809b7d18d78\\})|(\\{46f35a01-faaf-4fab-95e6-7dfc8b6d8b73\\})|(\\{55d2c6f7-62fa-4091-988b-7f4c4b3c1bff\\})|(\\{75aeaeec-d415-404d-84ba-bd70bcc5e70c\\})|(\\{76b8cf24-227d-4e2b-af4c-39ec5b47babf\\})|(\\{77b725cc-5d0e-4b82-88f0-ec6961acd697\\})|(\\{90fc8426-06ba-43ab-8110-7478ff86f531\\})|(\\{90fc8426-06ba-43ab-8110-7478ff86f539\\})|(\\{157cd8f9-48f0-43a1-9bcf-c4316753e084\\})|(\\{157cd8f9-48f0-43a1-9bcf-c4316753e085\\})|(\\{201ec7f7-57b1-48dd-945c-b1ea7489195d\\})|(\\{280fc0f5-6dfb-4a3c-92ae-acb2d5352175\\})|(\\{388f6d65-4a1b-43ac-b791-387882c30599\\})|(\\{0575cabd-38f3-4964-bdc3-0141a2f062e9\\})|(\\{927e4189-4f56-437e-a0d4-5e232612b5c7\\})|(\\{7277d7cf-c598-420b-ab6e-ab066e1e2fdd\\})|(\\{67775ec2-c879-438b-9409-89fba7ffc684\\})|(\\{397386d2-bb76-4b69-8121-86fad15c5216\\})|(\\{bd7f03dc-b362-4744-b118-43ab916205f9\\})|(\\{c133fb29-c967-4aec-953a-4974e8cbdb26\\})|(\\{cc94c8a7-efa3-435c-91fe-ca305f70e39d\\})|(\\{cfd2ff68-6579-4448-8a26-561bdb63877c\\})|(\\{d00f0050-a66c-49fc-9236-1498d4d29f67\\})|(\\{daa287a2-5916-413e-9b61-52c00b5aa061\\})|(\\{dcfac76f-2fd2-4477-9a60-22d167cabcb4\\})|(\\{dd1bbcf4-bff3-4f15-8a2c-3d52ce987f70\\})|(\\{ddb546b5-6490-4af5-8813-8e701bc06e26\\})|(\\{ead6848b-4bd6-4f9a-93bd-b8460c6f6973\\})|(\\{eb8f7a97-ffb0-40f1-9321-5ab1de884f1c\\})|(\\{ec3e8a3d-df39-4f84-ab31-dae369a225e4\\})|(\\{ef986f55-2dc9-4e39-8c87-618cf4fe5e69\\})|(\\{f8b4b601-7917-40c1-94ec-8efbbf125a46\\})|(\\{f8bc456c-0fb4-4d5d-a85f-dfeb25459e76\\})|(\\{f0458469-cc09-407e-a891-be8606553341\\})|(\\{fa73622c-8b41-45b8-9d93-6d66e7633765\\})|(@loveroms)|(loveroms-ash1280@jetpack)|(searchdimension@gmail\\.com))$/", + "prefs": [], + "schema": 1552655172725, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535402", + "why": " This add-on violates Mozilla add-on policies by including abusive search behavior.", + "name": "Add-ons including abusive search behavior" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f9cd41dd-9e52-4506-bb58-a31e189f4ab9", + "last_modified": 1552655392045 + }, + { + "guid": "{b6f5f2d0-1aa3-4e43-b536-6db1b1bf7d1c}", + "prefs": [], + "schema": 1552592498693, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535601", + "why": "This add-on violates Mozilla's add-on policies by exfiltrating user data.", + "name": "FFcanu (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5b807d5f-a192-450a-a0b3-98113c4beff1", + "last_modified": 1552655172717 + }, + { + "guid": "{e19fed8c-637a-42e3-b62a-3a6c4040ded8}", + "prefs": [], + "schema": 1552570939014, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535345", + "why": "This add-on violates Mozilla's add-policies by executing remote code.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5027a1c1-e050-434f-ba77-56417bc2d7cf", + "last_modified": 1552589019976 + }, + { + "guid": "{fb62e856-f09b-4cbc-ba07-642ab55f6cb4}", + "prefs": [], + "schema": 1552567880022, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534781", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "EncDNA module (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ce66baec-1237-481c-87db-ccc1bcf0359d", + "last_modified": 1552567941331 + }, + { + "guid": "{54fc344c-e8ba-462a-a6d9-9ce1b794ce46}", + "prefs": [], + "schema": 1552567837850, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534817", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4bec4aaf-dd5b-4754-bd01-461fdc7ea5ca", + "last_modified": 1552567880014 + }, + { + "guid": "{7b6def45-d585-431a-a479-5bb2badf2506}", + "prefs": [], + "schema": 1552567781490, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1535055", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "PredicitionR (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "e9227d28-b627-48b8-8392-e9fb5a00d9b6", + "last_modified": 1552567837842 + }, + { + "guid": "{6fb28b6b-abf2-4937-af28-340851faa971}", + "prefs": [], + "schema": 1552567721181, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534769", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "metamedian (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ed853ce8-83e0-42b7-8d93-7f48041d4987", + "last_modified": 1552567781482 + }, + { + "guid": "{ae5b30dd-b29d-4ae6-844b-5d7bfc3d7915}", + "prefs": [], + "schema": 1552567676370, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534807", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "Crypto Valuator (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3cfd9af5-a7d0-49d3-971b-7af5e2eab78f", + "last_modified": 1552567721173 + }, + { + "guid": "web-private@ext.com", + "prefs": [], + "schema": 1552567616148, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534828", + "why": "This add-on violates Mozilla's add-on policies by overriding search preferences.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3d52fe32-71e5-47bd-8eda-d98fa0c489e9", + "last_modified": 1552567676362 + }, + { + "guid": "/^((ads@firefox\\.pl)|(adsfinland@firefox\\.pl)|(adsfrance@firefox\\.pl)|(dodateknowy@o2\\.pl)|(dodateksuper1@firefox\\.pl)|(dodateksuper2@firefox\\.pl)|(dodateksuper3@firefox\\.pl)|(dodateksuper5@firefox\\.pl)|(dodateksuper6@firefox\\.pl)|(dodateksuper@firefox\\.pl)|(test_b@iext\\.pro)|(\\{697be03c-cdd2-430e-b6cf-0f9b5f0556ee\\})|(\\{c9ced03f-a5cf-4dbf-b5ba-67673e442590\\})|(\\{cbe59f66-a23a-45c1-81ac-d0cbedf9ea4e\\})|(\\{dbf0a186-d41c-40ae-8841-e9d8a6b49d8d\\}))$/", + "prefs": [], + "schema": 1552493457658, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534940", + "why": "This add-on violates Mozilla's add-on policies by using a deceiving name and exfiltrating user data.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "67025e3b-860c-4680-949f-ec472cd72fae", + "last_modified": 1552567437766 + }, + { + "guid": "/^((\\{86c18738-11ed-4c16-af92-786aa036c83c\\})|(\\{d0fee16a-f4eb-4dc1-9961-82b913e5943d\\})|(\\{1c4937a1-c678-4607-8665-a16384ee302e\\})|(\\{22caeb02-38a3-415d-b168-64fadccbb4a4\\})|(\\{1c9372e7-5f0e-4541-99cf-dfbf2ab00b01\\})|(\\{9fe66994-8ed1-4317-a20a-1d0544ca062f\\})|(\\{6df222d8-97c7-42bf-9683-1cf8119c1e9e\\})|(\\{4c2dda03-bad0-4160-a8a1-6d089200420e\\})|(\\{7aae7d4f-55b9-42eb-b683-932591265e17\\})|(\\{e6f8ab99-3c96-410c-95d1-267ad48ed3e2\\})|(\\{6d8c5068-d0cb-47a5-af5e-3f23064f4608\\})|(\\{90481f38-d06a-465e-a54c-206bbb1ee9ae\\})|(\\{4b75aeb8-f14a-4ef3-b1ad-09733b40dac3\\})|(\\{3a8ca495-f5ab-4320-b070-4f44266fe3d1\\})|(\\{84f8914f-0dec-48ed-a0fd-4a7712c06793\\})|(\\{aa613fce-603c-41df-bf49-9b09614cebe6\\})|(\\{30314350-199a-4951-9c05-c3537a946492\\})|(\\{a2edce1d-10ab-483d-8c01-5e5fe0c82902\\})|(\\{ec91a3d4-8311-4700-aa15-b3941f21a052\\})|(\\{e9049687-164a-4cf3-be1f-1291cfb0f44a\\})|(\\{2be73925-ebaf-43ca-8b26-bd820887f591\\})|(\\{840eadea-1c68-411f-b4e9-08d9f236385d\\})|(\\{0a89d040-5fb1-46d7-bf81-43b55e83695d\\})|(\\{6a1e76ed-4ac2-4a0c-8beb-43ae63558b36\\})|(\\{1b90c930-e7d7-486a-9085-8b57129489c7\\})|(\\{eab649ca-af76-4de9-95b0-8036e35a66cc\\})|(\\{0628e652-98f4-4e58-9ecb-ad996b061aef\\})|(elfr@geckoaddon\\.org)|(else@geckoaddon\\.org)|(fr_b@iext\\.pro)|(it_b@iext\\.pro)|(sv_b@iext\\.pro)|(no_b1@iext\\.pro)|(fi_b@iext\\.pro)|(au_b@iext\\.pro)|(elfr12@geckoaddon\\.org)|(test@informations\\.to)|(se_pop@informations\\.to)|(it@spongebog\\.funny-ok\\.com)|(it@tsunami\\.funny-ok\\.com)|(fi@spongebog\\.funny-ok\\.com)|(fi@tsunami\\.funny-ok\\.com)|(no@spongebog\\.funny-ok\\.com)|(no@tsunami\\.funny-ok\\.com)|(fr@tsunami\\.funny-ok\\.com)|(fr@spongebog\\.funny-ok\\.com)|(se@tsunami\\.funny-ok\\.com)|(se@spongebog\\.funny-ok\\.com)|(au@spongebog\\.funny-ok\\.com)|(au@tsunami\\.funny-ok\\.com)|(nz@spongebog\\.funny-ok\\.com)|(nz@tsunami\\.funny-ok\\.com)|(gr@spongebog\\.funny-ok\\.com)|(gr@tsunami\\.funny-ok\\.com)|(nz_fnew@tsunami\\.funny-ok\\.com))$/", + "prefs": [], + "schema": 1552320039514, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1534103", + "why": "Stealing cookies, browsing history and other information", + "name": "Rogue Updater add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "b57d9505-21bf-4a24-accb-05ceac50dadc", + "last_modified": 1552323475989 + }, + { + "guid": "{c04d9d7d-1c8c-4eab-a51a-828c47e1b8b7}", + "prefs": [], + "schema": 1552246898392, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1533780", + "why": "This add-on violates Mozilla's add-on policies by executing remote code.", + "name": "asin (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2c739daa-ffee-48d9-a825-e53c8fd2bb3c", + "last_modified": 1552300402314 + }, + { + "guid": "/^((\\{ee2d725e-9726-43ac-8040-60ce9ff2831b\\})|(\\{55417a80-e6f7-4d77-8d73-f59045e5e890\\}))$/", + "prefs": [], + "schema": 1551728497880, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1532269", + "why": "This add-on violates Mozilla's add-on policies by using a deceptive name.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "aca80fb4-760e-4cd4-9fec-649fb38b2947", + "last_modified": 1551794995188 + }, + { + "guid": "/^((\\{5084f455-bc8f-483c-b145-91245bcbfd64\\})|(\\{bd69d5d0-4b2f-48cb-bab5-dcf1e0f9c63b\\}))$/", + "prefs": [], + "schema": 1551398716775, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1531416", + "why": "Maliciously collecting form data and cookie modification", + "name": "Adblock/Adobe Flash fakes" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7718be46-8e84-4bc7-a5a9-4c5de18378ee", + "last_modified": 1551399019543 + }, + { + "guid": "/^((\\{6745ccb4-833d-497e-b582-d98a5e790e8c\\})|(\\{cd205ddb-b106-4d2a-a965-5d1c610b5072\\})|(\\{218ec82e-2839-42da-acaa-e527454f4237\\})|(\\{7af25a3d-1caf-49f8-8be9-8ae6065db7c5\\}))$/", + "prefs": [], + "schema": 1551397746059, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530911", + "why": "Search hijacking, remote scripts", + "name": "Emoj1 clones" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "77a32be3-94ce-49c2-b129-fa2562a7f47b", + "last_modified": 1551398716765 + }, + { + "guid": "Youtube-video@Myaddons.com", + "prefs": [], + "schema": 1551397521494, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1529887", + "why": "Remote script injection and data exfiltration", + "name": "YouTube Video and MP3 Downloader" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a30f9f2a-aa68-48b7-88cc-8a582405b385", + "last_modified": 1551397746049 + }, + { + "guid": "Youtube-downloader@Myaddons.com", + "prefs": [], + "schema": 1551396963937, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1529873", + "why": "Remote script injection, data exfiltration", + "name": "YouTube MP3 Converter" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1d596a69-157f-4743-9465-f86d6452206b", + "last_modified": 1551397521482 + }, + { + "guid": "{ba74c7ee-32b1-11e9-ade5-1f2222a4f325}", + "prefs": [], + "schema": 1551382900634, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1529573", + "why": "Remote script injection and user data exfiltration", + "name": "GET Security" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "01825fea-8c5c-4d76-bd06-e1019c188056", + "last_modified": 1551396963926 + }, + { + "guid": "/^((\\{e0686c32-99b4-44d8-972f-88bf08b68f88\\})|(\\{b2225e4c-9d1d-472b-8aeb-5ff203bcff9a\\}))$/", + "prefs": [], + "schema": 1551210091992, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530932", + "why": "This add-on is distributed violates Mozilla's add-on policies by being distributed through a fake Firefox update page.", + "name": "Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "37252271-9e38-46a9-b23a-2b6d7048c0db", + "last_modified": 1551250023025 + }, + { + "guid": "{9d7cfde2-39ae-11e9-bde0-02427e2eba50}", + "prefs": [], + "schema": 1551104404768, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1530640", + "why": "This add-on violates Mozilla's add-on policies by including abusive remote functionality, negatively affecting security and performance.", + "name": "Unnamed malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "7bb234b0-cfda-4a23-bf02-9c82fb3500a3", + "last_modified": 1551204284998 + }, + { + "guid": "{4603d01d-ae80-4653-9288-d5ef98b99a17}", + "prefs": [], + "schema": 1551099702949, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1529924", + "why": "This add-on violates Mozilla add-on policies by including abusive remote code.", + "name": "IMmailgun (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "32227de6-a7bf-454c-bf44-4478ddd96abe", + "last_modified": 1551099805258 + }, + { + "guid": "{157cd8f9-48f0-43a1-9bcf-c4316753e083}", + "prefs": [], + "schema": 1551037300209, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1529926", + "why": "This add-on violates Mozilla add-on policies by including abusive search behavior.", + "name": "SD App (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5b3fe8de-6d05-4d95-a6d2-cd5695f1b0c0", + "last_modified": 1551099426266 + }, + { + "guid": "{5288d05d-253f-4675-be3b-152bf02aa3ec}", + "prefs": [], + "schema": 1550683849233, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1529328", + "why": "Remote script injection along with deceptive code to hide the fact", + "name": "Unicode & Emoji Support" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9600b4cd-da02-4947-a4f5-c56c657ba197", + "last_modified": 1550684288501 + }, + { + "guid": "restore.old@youtube.com", + "prefs": [], + "schema": 1550580649249, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1529126", + "why": "This add-ons does remote script injection in a way that attempts to hide the injection. The code being executed runs on all pages which is contrary to what the add-on is described to do.", + "name": "Restore Old Youtube" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "27917953-17fb-4ffc-bcf1-5fc2727174b4", + "last_modified": 1550617105595 + }, + { + "guid": "/^((pohhpifegcbplaijeeonlbkjgocfgjbf@chrome-store-foxified-18573537)|(pohhpifegcbplaijeeonlbkjgocfgjbf@chrome-store-foxified-570932499)|(aackamlchlgmalkmcphbhhcjebbpnfdf@chrome-store-foxified-233164420)|(pohhpifegcbplaijeeonlbkjgocfgjbf@chrome-store-foxified-1808417494)|(nnfhjlgginbfdejaajhbboeemajpjgfp@chrome-store-foxified-699139867)|(dilkapnoekabmkkhhdlpnpfgjcmddlia@chrome-store-foxified-1808417494)|(dilkapnoekabmkkhhdlpnpfgjcmddlia@chrome-store-foxified-1190639722)|(nnfhjlgginbfdejaajhbboeemajpjgfp@chrome-store-foxified-2745518014)|(fibaefnljghpmdibfkhnlaniblfkkndi@chrome-store-foxified-1955364993)|(dilkapnoekabmkkhhdlpnpfgjcmddlia@chrome-store-foxified-1516694386)|(generated-rk4dtanssk56goquir78sz@chrome-store-foxified--1594555229)|(nnfhjlgginbfdejaajhbboeemajpjgfp@chrome-store-foxified-1388315457)|(oaoebpgbkehlcdggaeeohgfpopdhjell@chrome-store-foxified-1823310541)|(fibaefnljghpmdibfkhnlaniblfkkndi@chrome-store-foxified--1031675095)|(ancjheohbkbnkgcmfaldcaepoeeplkgh@chrome-store-foxified-1823310541)|(generated-lwcbpuj27wcknyyl6pkap7@chrome-store-foxified-1823310541)|(generated-bmtr2hbgikv399aj6aeycd@chrome-store-foxified-1823310541)|(generated-8w9odie82h2ugbsrofooj3@chrome-store-foxified-1823310541)|(\\{4170faaa-ee87-4a0e-b57a-1aec49282887\\})|(\\{b66dd4bc-7f5e-41d9-bc43-84c5736d0c87\\})|(\\{507ad1fb-08ae-43ac-a596-fd5b6e7dea9a\\})|(\\{8bf85207-66df-4eb7-b913-ac162498c687\\})|(\\{b063895a-8077-452d-b049-ebc20a39f882\\})|(\\{5776bd90-3d09-43b5-a769-8da373e9820f\\})|(\\{f6bdce44-3d5a-4f88-9a64-e39fcb4f0717\\})|(\\{1c22e687-6a02-440c-a6d5-c1ccaf520e10\\})|(\\{f7be824f-7287-466a-8590-2f48007a223b\\})|(\\{89ffc0b4-97f7-447c-bd6f-9c519b0188bd\\})|(\\{3a67084b-c231-4b4b-a924-ffa741f90921\\})|(\\{fac8b1d0-f321-491d-9234-c40d89b3660d\\})|(\\{a8053a83-8d1a-4f0e-9f88-d31cfe00cf83\\})|(\\{d10fa57e-37d3-43d3-39f8-e6d5b2a7759d\\})|(savetube_downloader@savetube\\.co)|(\\{95a8a08c-53a8-7e1d-5a80-f1a5cd4751bf\\})|(\\{5037bd74-c23b-4bbf-8865-6b5a09e07342\\})|(\\{830c558c-70f0-4b07-95f1-8e06ad34ee2c\\})|(\\{e4d93c37-1299-452f-9b60-adee15ad3802\\})|(googlemaps@search))$/", + "prefs": [], + "schema": 1550502645324, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1528761", + "why": "Malicious add-ons injecting remote scripts and exfiltrating data for monetization purposes. Some of the add-ons are masking themselves as legit add-ons and using various obfuscation tactics to hide their injections.", + "name": "Rogue Youtube downloaders and related add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f2483d7d-1895-4ae6-9901-7321e59062a6", + "last_modified": 1550502978653 + }, + { + "guid": "/^((((generated-e0yf8zkhypow3jjifkqzwo)|(iiinclagpealgnaglbmkdbfbgchjjbpg)|(jaehkpjddfdgiiefcnhahapilbejohhj))@chrome-store-foxified--?\\d+)|(jid1-9ETkKdBARv7Iww@jetpack)|(\\{813fe658-5a29-4dcc-ba6c-3941676e4f19\\}))$/", + "prefs": [], + "schema": 1550254444783, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1528308", + "why": "This add-on violates Mozilla's add-on policies by tracking user behavior and including remote code.", + "name": "BuyHatke Best Price Comparison, Graph, Coupons (and similar)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d6b2dbad-31e9-4984-b649-19036cd38e1d", + "last_modified": 1550265430182 + }, + { + "guid": "/^((odnenajaeicndaeiapagpglohiklndhe@chrome-store-foxified-2174031944)|(akdibckdjeocfbcjaikipkhhbggcbgkg@chrome-store-foxified-699805590)|(\\{bd9f5830-bf8c-4e38-933d-09f85b24d744\\})|(youtube-downloader@addoncrop\\.com)|(dailymotion-downloader@addoncrop\\.com)|(youtube-mp3-iframe-downloader@addoncrop\\.com)|(youtube-downloader-lite@addoncrop\\.com)|(dailymotion-downloader-lite@addoncrop\\.com)|(generated-p5qj07ef5ceqfktp5v8whw@chrome-store-foxified--505607522)|(generated-tuag2j5gwq73xzp8fbw3j8@chrome-store-foxified--505607522)|(generated-uj53d5j612ap5pz11hwgv2@chrome-store-foxified--505607522)|(\\{f51f0244-bfb0-40ce-bf9d-af49f5949e61\\})|(\\{81751484-2dc9-47bf-aec3-b8e556237178\\})|(\\{d89d1e20-57af-4164-82cc-650af45cf0a5\\})|(\\{a5e28713-14c3-4967-befe-2ec253e317cd\\})|(\\{335ac35b-6c16-4304-93f0-64a418e5bf81\\})|(\\{d3fdb429-ef84-40a4-b160-010745ee0098\\})|(\\{d66db057-7d38-4df4-a0ba-63c272be25ee\\})|(\\{ff7f7151-09e3-4e35-9052-b21e23e7e2c1\\})|(\\{1479da02-e759-4a6f-8f62-c08096583806\\})|(\\{42d59cda-c117-459e-b244-1b850c27ccc9\\})|(\\{bec48f33-7869-4158-8cbc-5cf1606f9afa\\})|(\\{08ca656c-4973-46a8-85a9-3d771399c86e\\})|(\\{dc5336c8-5624-4f74-a794-bb15f3760f34\\})|(\\{6a3d97de-1b42-4624-880a-1a68bc917142\\})|(\\{ad965166-3a34-449d-8230-a636fb5cae57\\})|(video-view@vv\\.us)|(video-view@excoe\\.com)|(xeco@rama\\.com)|(ghj@brem\\.com)|(fgtr@rembgr\\.com)|(xero@saltam\\.com)|(sora@remjem\\.com)|(repo@saram\\.com)|(tora@empoytr\\.net)|(saving@ropbart\\.com)|(japa@vbgt\\.com)|(rtya@nop\\.net)|(asan@khazan\\.com)|(xerb@tangot\\.com)|(audiotools@ramdeen\\.com)|(webat@extrolm\\.com)|(media@medplus\\.com)|(audio@audequl\\.com)|(control@medcontrols\\.com)|(sabqo@rimjim\\.com)|(repto@secroa\\.com)|(meters@videobf\\.com)|(betro@diskra\\.com)|(codeca@vxmrop\\.com)|(revoca@feronx\\.com)|(brota@vidbrghta\\.com))$/", + "prefs": [], + "schema": 1550173301925, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1527603", + "why": "This add-on violates Mozilla's add-on policy by including malicious remote code.", + "name": "Various add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d94f7e0f-7088-43c9-a8da-eae102781079", + "last_modified": 1550253583075 + }, + { + "guid": "superzoom@funnerapps.com", + "prefs": [], + "schema": 1549578756953, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1525405", + "why": "This add-on includes abusive functionality including violating the user's security and privacy.", + "name": "SuperZoom (Abusive)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f7c4b329-7a54-48d3-9494-81783fe99d90", + "last_modified": 1549627400713 + }, + { + "guid": "/^((addon@firefox-updater\\.com)|(downloader@youtube-download\\.org)|(downloader2@youtube-download\\.org)|(downloader1@youtube-download\\.org))$/", + "prefs": [], + "schema": 1549369087422, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1525153", + "why": "These add-ons include abusive behavior.", + "name": "\"FF Manual Update (Required)\" and \"Easy Youtube Video Downloader Express\" (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6b72e942-af54-469c-b9f2-9e436ad2c0d1", + "last_modified": 1549373215201 + }, + { + "guid": "/^((\\{0b347362-773f-4527-a006-f96e9db437e5\\})|(\\{9edb10ad-67af-4ad0-af45-efe452479321\\})|(\\{202e2671-6153-450d-bc66-5e67cee3603f\\}))$/", + "prefs": [], + "schema": 1548963700621, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1524357", + "why": "This add-on includes hidden abusive functionality.", + "name": "Video download add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "680a99e9-c972-4a14-9b15-e56eeeed75eb", + "last_modified": 1549037404012 + }, + { + "guid": "/^((\\{a9bc520e-68ab-49c2-a8df-75a0349d54fd\\})|(\\{bfc5d009-c6bd-4526-92ce-a9d27102f7f2\\}))$/", + "prefs": [], + "schema": 1548699141208, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1522959", + "why": "Add-ons that contain abusive functionality.", + "name": "Unnamed (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "97c4ee31-4009-40de-ae02-f1b349c87d01", + "last_modified": 1548699177099 + }, + { + "guid": "{4e47160d-ec83-417c-ab01-cda978378d9e}", + "prefs": [], + "schema": 1548699076839, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1522970", + "why": "The add-on contains abusive functionality.", + "name": "mlflow (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "62c54642-73ab-4641-b5c2-47e4ae29bbc5", + "last_modified": 1548699141199 + }, + { + "guid": "/^((\\{feff5ea4-ed4a-46a3-9331-12fec01d52a9\\})|(\\{8ffc339c-0ca1-46e3-acb3-3bfd889f9a21\\})|(\\{1fe481b5-baad-44e9-b410-082cf0f2acbb\\})|(\\{92c85192-b325-4599-82e2-a110f193eae6\\})|(\\{5bc21266-26f1-469a-bc1a-a49d7b70ecb9\\})|(\\{dc2dd143-f2e7-4f46-a8ff-4dc1a985e889\\})|(\\{c1233bb6-31a9-4c7d-8469-f8f44adee9ba\\})|(\\{d0d48905-1065-43dc-ab96-434d100602ed\\})|(\\{11deae99-2675-4d5e-86cd-7bd55b88daf2\\})|(\\{a7014e8e-eacf-4ba0-9047-c897c4ed3387\\})|(\\{b9c545a5-0ffa-490a-8071-2fee09478cfe\\})|(\\{39e8eebb-4d9e-4a03-93a8-4468fdd7a186\\})|(\\{8ccc00b1-2010-4155-a07c-f4d7c4d6dec2\\})|(\\{a1056511-4919-43b7-a9e5-ac2b770de810\\})|(\\{90a6da19-9165-44c1-819c-e3b53409f9c9\\})|(\\{e3862078-8f9f-4f8e-99dc-55ba558f0619\\})|(\\{d89fcf34-2615-4efc-a267-1e83ab6a19d0\\})|(\\{588151ce-eab4-4acf-83a7-bb5ccaf4d867\\})|(\\{6ab6312d-5fd4-42a9-ab10-08b954e53f9d\\})|(\\{24a6cbde-be68-4b7d-9f1b-d4d5dfd178a3\\})|(\\{55ae1a08-105f-4f7f-9d4e-e448b517db2b\\})|(\\{74fe9d83-df17-4812-bd3f-27b84b0086b7\\})|(\\{21140e51-713a-4bf8-865b-e2ee07282409\\})|(\\{24f39610-2958-4dc8-a73b-75cc9665bffa\\})|(\\{c50a45a5-efa4-44af-8946-6f20e4748d47\\})|(\\{41d0b7e0-0d93-4f67-bed9-da6d7688ad16\\})|(\\{c2bee222-2163-4c0f-89f5-4ac502f06810\\})|(\\{4be4602e-2b20-473f-8f2b-85e8c53d17dc\\})|(\\{dec2e9b5-e787-4fb5-a7bc-5894f80f7367\\})|(\\{179526e1-1824-49f7-afb3-49fdaadaa503\\})|(\\{4f07d826-ca9e-4370-a508-b984f54758de\\})|(\\{d0558df2-714f-4793-9d85-d2d648de4f2e\\})|(\\{daf1a69b-f47b-4936-bd25-5ac21f4e27ec\\}))$/", + "prefs": [], + "schema": 1548099697813, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1521975", + "why": "Remote script injection and deceptive tactics to hide the fact", + "name": "ext-affiliate add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "87c17ce6-aaef-4d47-a662-588efff34041", + "last_modified": 1548198338831 + }, + { + "guid": "hlper@xero.com", + "prefs": [], + "schema": 1548076840649, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1521533", + "why": "This add-on executes abusive remote code.", + "name": "Av Player Helper (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1e2ae4c0-66cd-40bc-9cf6-5ca0ce9548f7", + "last_modified": 1548084072622 + }, + { + "guid": "/^((xtera@soravem\\.net)|(nozl@ssave\\.net))$/", + "prefs": [], + "schema": 1547926889113, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1521429", + "why": "This add-on injects abusive remote code.", + "name": "Video Assist (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c69997df-0b61-4de5-a351-b640123a9c3b", + "last_modified": 1548073537411 + }, + { + "guid": "/^((\\{94f608b3-76b6-4b7b-8cef-7360df22a930\\})|(\\{9648b74f-35ea-4218-acf0-ec2191f509f6\\}))$/", + "prefs": [], + "schema": 1547754101798, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1520813", + "why": "Add-ons that leak private user data.", + "name": "Instagram Register and Google Register (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a15e8d7e-0726-4190-8187-c75e2b46d429", + "last_modified": 1547810271416 + }, + { + "guid": "reopen@closedtab.com", + "prefs": [], + "schema": 1547067530457, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1518847", + "why": "This add-on contains unwanted abusive behavior unrelated to the add-ons functionality.", + "name": "Reopen Closed Tabs (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "451d950f-ca89-491b-87e7-45123e4f5ab4", + "last_modified": 1547206877909 + }, + { + "guid": "{43ecded1-f7cb-4bb6-a03d-4bec23b9f22d}", + "prefs": [], + "schema": 1547025691087, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1518580", + "why": "This add-on violates Mozilla's policy and user's security/privacy by loading abusive code from remote.", + "name": "lamme (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "06c6868e-055f-4e7d-aa8f-5ba577f43e85", + "last_modified": 1547027153061 + }, + { + "guid": "youtube_downloader@addon.partners", + "prefs": [], + "schema": 1546890104853, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1518032", + "why": "This add-on contains unwanted abusive functionality.", + "name": "YouTube Download Tool HD (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5a42c5bb-9cb4-4d96-b978-8d9f816322e6", + "last_modified": 1547025691078 + }, + { + "guid": "/^((\\{00cf6ee0-14f3-4e35-a4fd-d2160fe2f05e\\})|(\\{0da583da-e623-41f2-b2d2-0ac61b493171\\})|(\\{105c14a6-8b6f-49ef-b0d6-41bad99ad5e8\\})|(\\{10a15a74-271f-4098-a662-bd827db4f8bc\\})|(\\{13e02b9b-2797-4100-8144-65b73c4145c4\\})|(\\{1eb56568-8a30-42b1-a646-ad9f485f60fe\\})|(\\{1eb8a08c-82a8-4d1d-8b80-f7b5cd4751bf\\})|(\\{2f8220a8-b2a7-4277-ba6b-bdcb6958f669\\})|(\\{33f39a5d-137c-4757-9f9d-e86395c8bf20\\})|(\\{347ca189-9e63-43d2-8a2f-5d5141598bdc\\})|(\\{396056fc-1697-4954-b535-06de8d62fe1b\\})|(\\{3d530918-dbe8-442c-8faf-1f4ca7ca8ab9\\})|(\\{3e283c2e-cde3-4baa-8076-226ca8fb90ef\\})|(\\{591468f8-ebbd-497a-92f1-fa0a1206adb4\\})|(\\{5f6c3eb8-aa32-489a-bb01-b12b23d2001a\\})|(\\{6cbb397a-d865-42b2-8454-25a75b710dff\\})|(\\{7ae2bde0-f7ea-4bf3-a055-06953f9fcf31\\})|(\\{7b402578-ddec-4eee-9c8b-98e4e8db0059\\})|(\\{7fb00cf7-40d3-4415-a0c8-a48d3fbe847f\\})|(\\{87a8a08c-82a8-4d1d-8b80-f7b5cd4751bf\\})|(\\{888220a8-b2a7-4277-ba6b-bdcb6958f669\\})|(\\{8b1dd8f3-224b-4975-bda2-cb2dd184d4d8\\})|(\\{8bcdc9cc-f6be-4203-ae43-a9d281f0bcdb\\})|(\\{8cda9ce6-7893-4f47-ac70-a65215cec288\\})|(\\{8dc9b946-0bb9-4264-9c76-fd9ff1e159a2\\})|(\\{942e0fec-19f2-4ebc-8a74-009da7fa625d\\})|(\\{b2a720a8-b2a7-4277-aa6b-bdeb6958f669\\})|(\\{bdcf953b-d2aa-4e7a-8176-aeb1e95a0caf\\})|(\\{cae82615-f7be-4aff-875d-33da1bc93923\\})|(\\{d2aa953b-bdcf-4f7a-8476-ddb1e9500caf\\})|(\\{da0fa57e-17d3-40d3-99f8-e9d5b2a7759d\\})|(\\{da1237ca-e35d-4653-b2b5-d98043f33781\\})|(\\{e164563a-1512-4b81-99ff-95f2644c4075\\})|(\\{e2a720a8-b3a7-1277-aa2b-bdeb2958f669\\})|(\\{e6a90490-6ef7-407d-863a-7dd120f6f7dc\\})|(\\{f15cfa53-fa9b-43cf-84e8-16ece6695922\\})|(\\{f722c845-2d8b-4a0a-b518-4f39af703e79\\})|(\\{ff1c4e62-7c11-4ea7-b734-3462417ceeb5\\})|(\\{ffa0a57e-17d2-41d3-96f8-e8d5b2a0759d\\}))$/", + "prefs": [], + "schema": 1546378806655, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1517154", + "why": "Executing remote code containing coin mining and other undisclosed monetization", + "name": "Various remote iframe add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "53c5fb08-1001-471e-87ce-31185a84bcbc", + "last_modified": 1546439268437 + }, + { + "guid": "{02267dc4-36f2-4c22-8103-9e26477b48ef}", + "prefs": [], + "schema": 1545922885656, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1516539", + "why": "Google Search hijacking and affiliate injection", + "name": "Markdown" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "6dd73da4-cb2f-4eb8-8850-890e80c8d57b", + "last_modified": 1545926512914 + }, + { + "guid": "{d064563a-1542-4b8b-99ff-95f1644c4075}", + "prefs": [], + "schema": 1545921144932, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1516488", + "why": "Obfuscated remote script injection stealing data", + "name": "PDF Print and Save" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "875bc1c6-257e-438a-8624-3bfe963747b0", + "last_modified": 1545922824252 + }, + { + "guid": "/^((\\{83768008-e10c-48c0-b303-5a0f1de763a1\\})|(\\{430b0612-bfad-463b-8783-cf2e32801513\\})|(\\{519adb83-4abb-4a66-8e94-243754b8adce\\})|(\\{228a707d-55c1-465b-a759-a2129eb6602e\\}))$/", + "prefs": [], + "schema": 1545853297809, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1516131", + "why": "Remote script injection and data exfiltration", + "name": "Various malicious remote script injection add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c0cb1a85-c6c6-453e-b126-0e6e26ceaf88", + "last_modified": 1545870108716 + }, + { + "guid": "/^((\\{4c4ceb83-f3f1-ad73-bfe0-259a371ed872\\})|(\\{a941b5ab-8894-41e1-a2ca-c5a6e2769c5f\\})|(\\{56488007-bd74-4702-9b6d-aee8f6cc05ea\\})|(\\{9eebac07-ac86-4be7-928f-e1015f858eee\\})|(\\{5a993517-5be7-480e-a86c-b8e8109fa774\\})|(\\{309ad78e-efff-43cf-910c-76361c536b20\\})|(\\{cefcf45b-dfec-4072-9ffc-317094c69c28\\})|(\\{5b04980b-25e9-4bc6-b6ea-02c58d86cc5e\\})|(\\{0021a844-360f-480e-ac53-47391b7b17b4\\})|(\\{2bed9f51-62a8-4471-b23c-827e6727c794\\})|(\\{7d2130d3-d724-4f58-b6b7-8537a9e09d4c\\})|(\\{ccd3847a-e5ec-4d28-bf98-340230dcbd4d\\})|(\\{83716b9b-6e6e-4471-af76-2d846f5804f3\\})|(\\{5154c03a-4bfc-4b13-86a9-0581a7d8c26d\\})|(\\{24f51c5c-e3f5-4667-bd6c-0be4f6ef5cc2\\})|(\\{73554774-4390-4b00-a5b9-84e8e06d6f3c\\})|(\\{c70cfd12-6dc3-4021-97f2-68057b3b759b\\})|(\\{ef5fe17b-eb6a-4e5e-9c18-9d423525bbbd\\})|(\\{461eb9b4-953c-4412-998e-9452a7cb42e0\\})|(\\{966b00fe-40b0-4d4b-8fde-6deca31c577b\\})|(\\{dab908ac-e1b0-4d7e-bc2e-86a15f37621f\\})|(\\{01a067d3-7bfa-44ac-8da7-2474a0114a7e\\})|(\\{6126261f-d025-4254-a1db-068a48113b11\\})|(\\{6c80453f-05ec-4243-bb71-e1aac5e59cae\\})|(\\{f94ec34b-5590-4518-8546-c1c3a94a5731\\})|(\\{5d4c049e-7433-485a-ac62-dd6e41af1a73\\})|(\\{507f643d-6db8-47fe-af9c-7a7b85a86d83\\})|(\\{5c56eeb4-f97c-4b0d-a72f-8e639fbaf295\\})|(\\{2ef98f55-1e26-40d3-a113-a004618a772e\\})|(\\{77d58874-d516-4b00-b68a-2d987ef83ec5\\})|(\\{7a0755d3-3ba2-4b19-98ce-efcdc36423fc\\})|(\\{47ee3ba1-8974-4f71-b8a4-8033d8c2155f\\})|(\\{a477f774-bc36-4cc8-85bd-99f6b04ea255\\})|(\\{1a2e41e3-4343-4a00-90cd-ce77ac77a8af\\})|(\\{7b180e9a-afd6-4693-94a1-c7b5ed9b46fa\\})|(\\{51f76862-f222-414d-8724-6063f61bbabf\\})|(\\{d47a0c63-ac4c-48ce-8fc7-c5abc81d7f75\\})|(\\{b8adf653-f262-413c-b955-100213b105ad\\})|(\\{ccedf35b-dfd6-417a-80de-fb432948861d\\})|(\\{70e29b0e-7cd8-40df-b560-cf6eb066350d\\})|(\\{9926f8ad-b4c3-4122-a033-1b8a5db416db\\})|(\\{62eefb1c-a2d8-40ba-ab94-9fc2f2d31b2f\\})|(\\{17f14919-00bd-44a4-8c14-78ab9728038f\\})|(\\{20e36a3e-672c-4448-9efb-5750cbffe90c\\})|(\\{6070c95f-6460-4ffd-9846-2bbd7238697f\\})|(\\{1edb8a4e-f105-4623-9e19-e40fb082b132\\})|(\\{223a1503-772d-45eb-8cb8-e2e49563703d\\})|(\\{59e0f01c-1f70-445c-a572-7be5d85549bd\\})|(\\{8ec160b7-1089-4944-a999-a1d6afa71c5d\\})|(\\{d2d111d6-0ea1-4880-ae7b-2e82dff3a719\\})|(\\{cfacacd6-191c-46c4-b78c-8a48289b2829\\})|(\\{1155e72f-2b21-433f-ba9a-5af6ed40c8ee\\})|(\\{583910bd-759f-40f6-b96a-1d678d65650f\\}))$/", + "prefs": [], + "schema": 1545162093238, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1514865", + "why": "Remote script injection and data exfiltration", + "name": "Various add-ons using .cool domains" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "53168513-103a-4ea0-a48f-bc291354cc9f", + "last_modified": 1545232187960 + }, + { + "guid": "{56a1e8d2-3ced-4919-aca5-ddd58e0f31ef}", + "prefs": [], + "schema": 1544470901949, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1491312", + "why": "The add-on introduces unwanted functionality for users.", + "name": "Web Guard (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1dc366d6-c774-4eca-af19-4f9495c2c55e", + "last_modified": 1544544484935 + }, + { + "guid": "/^((\\{a99e680b-4349-42a5-b292-79b349bf4f3d\\})|(\\{f09a2393-1e6d-4ae4-a020-4772e94040ae\\})|(\\{c9ed9184-179f-485f-adb8-8bd8e9b7cee6\\})|(\\{085e53da-25a2-4162-906e-6c158ec977ac\\})|(\\{bd6960ba-7c06-493b-8cc4-0964a9968df5\\})|(\\{6eeec42e-a844-4bfd-a380-cfbfc988bd78\\})|(\\{3bbfb999-1c82-422e-b7a8-9e04649c7c51\\})|(\\{bfd229b6-089d-49e8-a09c-9ad652f056f6\\})|(\\{ab23eb77-1c96-4e20-b381-14dec82ee9b8\\})|(\\{ebcce9f0-6210-4cf3-a521-5c273924f5ba\\})|(\\{574aba9d-0573-4614-aec8-276fbc85741e\\})|(\\{12e75094-10b0-497b-92af-5405c053c73b\\})|(\\{99508271-f8c0-4ca9-a5f8-ee61e4bd6e86\\})|(\\{831beefc-cd8c-4bd5-a581-bba13d374973\\})|(\\{c8fe42db-b7e2-49e6-98c4-14ac369473a4\\})|(\\{f8927cca-e6cb-4faf-941d-928f84eb937f\\})|(\\{17e9f867-9402-4b19-8686-f0c2b02d378f\\})|(\\{f12ac367-199b-4cad-8e5a-0a7a1135cad0\\})|(\\{487003ce-5253-4eab-bf76-684f26365168\\})|(\\{487003ce-5213-2ecb-bf16-684f25365161\\}))$/", + "prefs": [], + "schema": 1543088493623, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1509864", + "why": "Add-ons that track users and load remote code, while pretending to provide cursor customization features.", + "name": "Various cursor and update add-ons (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a8d942b3-779d-4391-a39c-58c746c13b70", + "last_modified": 1543241996691 + }, + { + "guid": "{97f19f1f-dbb0-4e50-8b46-8091318617bc}", + "prefs": [], + "schema": 1542229276053, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1507191", + "why": "Fraudulent Adobe Reader add-on", + "name": "Adobe Reader (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "03120522-ee87-4cf8-891a-acfb248536ff", + "last_modified": 1542272674851 + }, + { + "guid": "/^((video-downloader@vd\\.io)|(image-search-reverse@an\\.br)|(YouTube\\.Downloader@2\\.8)|(eMoji@ems-al\\.io))$/", + "prefs": [], + "schema": 1542023230755, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1506560", + "why": "Add-ons that contain malicious copies of third-party libraries.", + "name": "Malware containing unwanted behavior" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cd079abe-8e8d-476f-a550-63f75ac09fe8", + "last_modified": 1542025588071 + }, + { + "guid": "/^({b384b75c-c978-4c4d-b3cf-62a82d8f8f12})|({b471eba0-dc87-495e-bb4f-dc02c8b1dc39})|({36f623de-750c-4498-a5d3-ac720e6bfea3})$/", + "prefs": [], + "schema": 1541360505662, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1504619", + "why": "Add-ons that contain unwanted behavior.", + "name": "Google Translate (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "aa5eefa7-716a-45a6-870b-4697b023d894", + "last_modified": 1541435973146 + }, + { + "guid": "{80869932-37ba-4dd4-8dfe-2ef30a2067cc}", + "prefs": [], + "schema": 1538941301306, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1497161", + "why": "Malicious page redirection", + "name": "Iridium (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "dd5b0fa4-48fd-4bf6-943d-34de125bf502", + "last_modified": 1538996335645 + }, + { + "guid": "admin@vietbacsecurity.com", + "prefs": [], + "schema": 1537309741764, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1491716", + "why": "Logging and sending keystrokes to a remote server.", + "name": "Vietnamese Input Method (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "89d714f6-9f35-4107-b8af-a16777f66337", + "last_modified": 1537309752952 + }, + { + "guid": "Safe@vietbacsecurity.com", + "prefs": [], + "schema": 1537309684266, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1491717", + "why": "Logging and sending keystrokes to a remote server.", + "name": "SafeKids (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c651780e-c185-4d6c-b509-d34673c158a3", + "last_modified": 1537309741758 + }, + { + "guid": "/^((\\{c9226c62-9948-4038-b247-2b95a921135b\\})|(\\{5de34d4f-b891-4575-b54b-54c53b4e6418\\})|(\\{9f7ac3be-8f1c-47c6-8ebe-655b29eb7f21\\})|(\\{bb33ccaf-e279-4253-8946-cfae19a35aa4\\}))$/", + "prefs": [], + "schema": 1537305338753, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1491298", + "why": "These add-ons inject remote malicious scripts on Google websites.", + "name": "Dll and similar (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "54b3e69a-40ae-4be5-b7cf-cf51c526dcfb", + "last_modified": 1537306138745 + }, + { + "guid": "updater-pro-unlisted@mozilla.com", + "prefs": [], + "schema": 1537305285414, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1491306", + "why": "Redirects search queries.", + "name": "Updater Pro (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3108c151-9f25-4eca-8d80-a2fbb2d9bd07", + "last_modified": 1537305338747 + }, + { + "guid": "/^((\\{686fc9c5-c339-43db-b93a-5181a217f9a6\\})|(\\{eb4b28c8-7f2d-4327-a00c-40de4299ba44\\})|(\\{58d735b4-9d6c-4e37-b146-7b9f7e79e318\\})|(\\{ff608c10-2abc-415c-9fe8-0fdd8e988de8\\})|(\\{5a8145e2-6cbb-4509-a268-f3121429656c\\})|(\\{6d451f29-1d6b-4c34-a510-c1234488b0a3\\})|(\\{de71f09a-3342-48c5-95c1-4b0f17567554\\})|(\\{df106b04-984e-4e27-97b6-3f3150e98a9e\\})|(\\{70DE470A-4DC0-11E6-A074-0C08D310C1A8\\})|(\\{4dcde019-2a1b-499b-a5cd-322828e1279b\\})|(\\{1ec3563f-1567-49a6-bb5c-75d52334b01c\\})|(\\{c140c82e-98e6-49fd-ae17-0627e6f7c5e1\\})|(\\{2581c1f6-5ad9-48d4-8008-4c37dcea1984\\})|(\\{a2bcc6f7-14f7-4083-b4b0-c335edc68612\\})|(\\{4c726bb6-a2af-44ed-b498-794cfd8d8838\\})|(\\{fa6c39a6-cd11-477b-966d-f388f0ba4203\\})|(\\{26c7bd04-18d3-47f5-aeec-bb54e562acf2\\})|(\\{7a961c90-2071-4f94-9d9a-d4e3bbf247c0\\})|(\\{a0481ea2-03f0-4e56-a0e1-030908ecb43e\\})|(\\{c98fb54e-d25f-43f4-bd72-dfaa736391e2\\})|(\\{da57263d-adfc-4768-91f7-b3b076c20d63\\})|(\\{3abb352c-8735-4fb6-9fd6-8117aea3d705\\})|(contactus@unzipper\\.com)|(\\{a1499769-6978-4647-ac0f-78da4652716d\\})|(\\{581D0A4C-1013-11E7-938B-FCD2A0406E17\\})|(\\{68feffe4-bfd8-4fc3-8320-8178a3b7aa67\\})|(\\{823489ae-1bf8-4403-acdd-ea1bdc6431da\\})|(\\{4c0d11c3-ee81-4f73-a63c-da23d8388abd\\})|(\\{dc7d2ecc-9cc3-40d7-93ed-ef6f3219bd6f\\})|(\\{21f29077-6271-46fc-8a79-abaeedb2002b\\})|(\\{55d15d4d-da76-44ab-95a3-639315be5ef8\\})|(\\{edfbec6b-8432-4856-930d-feb334fb69c1\\})|(\\{f81a3bf7-d626-48cf-bd24-64e111ddc580\\})|(\\{4407ab94-60ae-4526-b1ab-2521ffd285c7\\})|(\\{4aa2ba11-f87b-4950-8250-cd977252e556\\})|(\\{646b0c4d-4c6f-429d-9b09-37101b36ed1c\\})|(\\{1b2d76f1-4906-42d2-9643-0ce928505dab\\})|(\\{1869f89d-5f15-4c0d-b993-2fa8f09694fb\\})|(\\{7e4edd36-e3a6-4ddb-9e98-22b4e9eb4721\\})|(\\{e9c9ad8c-84ba-43f2-9ae2-c1448694a2a0\\})|(\\{6b2bb4f0-78ea-47c2-a03a-f4bf8f916eda\\})|(\\{539e1692-5841-4ac6-b0cd-40db15c34738\\}))$/", + "prefs": [], + "schema": 1536183366865, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1488578", + "why": "These add-ons take away user control by redirecting search.", + "name": "Tightrope search add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "81eb67a5-3fdb-448c-aadd-5f4d3b7cf281", + "last_modified": 1536186868443 + }, + { + "guid": "/^((\\{f01a138a-c051-4bc7-a90a-21151ce05755\\})|(\\{50f78250-63ce-4191-b7c3-e0efc6309b64\\})|(\\{3d2b2ff4-126b-4874-a57e-ed7dac670230\\})|(\\{e7c1abd4-ec8e-4519-8f3a-7bd763b8a353\\})|(\\{4d40bf75-fbe2-45f6-a119-b191c2dd33b0\\})|(\\{08df7ff2-dee0-453c-b85e-f3369add18ef\\}))$/", + "prefs": [], + "schema": 1535990752587, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1488248", + "why": "Add-ons that inject malicious remote code.", + "name": "Various Malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "67f72634-e170-4860-a5a3-133f160ebc32", + "last_modified": 1535992146430 + }, + { + "guid": "/^((\\{1cfaec8b-a1cb-4fc5-b139-897a22a71390\\})|(\\{2ed89659-09c1-4280-9dd7-1daf69272a86\\})|(\\{5c82f5cc-31f8-4316-bb7d-45a5c05227e6\\})|(\\{6a98a401-378c-4eac-b93c-da1036a00c6c\\})|(\\{6d83ebde-6396-483c-b078-57c9d445abfa\\})|(\\{07efb887-b09f-4028-8f7f-c0036d0485ea\\})|(\\{36f4882f-ff0b-4865-8674-ef02a937f7da\\})|(\\{61dea9e9-922d-4218-acdd-cfef0fdf85e7\\})|(\\{261be583-9695-48e0-bd93-a4feafaa18e6\\})|(\\{401ae092-6c5c-4771-9a87-a6827be80224\\})|(\\{534b7a84-9fc6-4d7c-9d67-e3365d2ae088\\})|(\\{552a949f-6d0e-402d-903d-1550075541ba\\})|(\\{579b8de8-c461-4301-ab09-695579f9b7c7\\})|(\\{754d3be3-7337-488e-a5bb-86487e495495\\})|(\\{2775f69b-75e4-46cb-a5aa-f819624bd9a6\\})|(\\{41290ec4-b3f0-45ad-b8f3-7bcbca01ed0d\\})|(\\{0159131f-d76f-4365-81cd-d6831549b90a\\})|(\\{01527332-1170-4f20-a65b-376e25438f3d\\})|(\\{760e6ff0-798d-4291-9d5f-12f48ef7658b\\})|(\\{7e31c21c-156a-4783-b1ce-df0274a89c75\\})|(\\{8e247308-a68a-4280-b0e2-a14c2f15180a\\})|(\\{b6d36fe8-eca1-4d85-859e-a4cc74debfed\\})|(\\{bab0e844-2979-407f-9264-c87ebe279e72\\})|(\\{d00f78fe-ee73-4589-b120-5723b9a64aa0\\})|(\\{d59a7294-6c08-4ad5-ba6d-a3bc41851de5\\})|(\\{d145aa5b-6e66-40cb-8a08-d55a53fc7058\\})|(\\{d79962e3-4511-4c44-8a40-aed6d32a53b1\\})|(\\{e3e2a47e-7295-426f-8517-e72c31da3f23\\})|(\\{e6348f01-841d-419f-8298-93d6adb0b022\\})|(\\{eb6f8a22-d96e-4727-9167-be68c7d0a7e9\\})|(\\{fdd72dfe-e10b-468b-8508-4de34f4e95e3\\}))$/", + "prefs": [], + "schema": 1535830899087, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1487472", + "why": "Several add-ons that change forcefully override search settings.", + "name": "Various malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "43f11241-88e3-4139-9f02-ac39489a241f", + "last_modified": 1535990735167 + }, + { + "guid": "/^((\\{35253b0b-8109-437f-b8fa-d7e690d3bde1\\})|(\\{0c8d774c-0447-11e7-a3b1-1b43e3911f03\\})|(\\{c11f85de-0bf8-11e7-9dcd-83433cae2e8e\\})|(\\{f9f072c8-5357-11e7-bb4c-c37ea2335fb4\\})|(\\{b6d09408-a35e-11e7-bc48-f3e9438e081e\\}))$/", + "prefs": [], + "schema": 1535658090284, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1486754", + "why": "Add-ons that execute remote malicious code.", + "name": "Several malicious add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "56bd2f99-57eb-4904-840a-23ca155d93ad", + "last_modified": 1535701073599 + }, + { + "guid": "/^((fireAnalytics\\.download@mozilla\\.com)|(fireabsorb@mozilla\\.com)|(fireaccent@mozilla\\.com)|(fireaccept@mozilla\\.com)|(fireads@mozilla\\.com)|(firealerts@mozilla\\.com)|(fireapi@mozilla\\.com)|(fireapp@mozilla\\.com)|(fireattribution@mozilla\\.com)|(fireauthenticator@mozilla\\.com)|(firecalendar@mozilla\\.com)|(firemail@mozilla\\.com)|(firemarketplace@mozilla\\.com)|(firequestions@mozilla\\.com)|(firescript@mozilla\\.com)|(firesheets@mozilla\\.com)|(firespam@mozilla\\.com)|(firesuite@mozilla\\.com)|(\\{3b6dfc8f-e8ed-4b4c-b616-bdc8c526ac1d\\})|(\\{834f87db-0ff7-4518-89a0-0167a963a869\\})|(\\{4921fe4d-fbe6-4806-8eed-346d7aff7c75\\})|(\\{07809949-bd7d-40a6-a17b-19807448f77d\\})|(\\{68968617-cc8b-4c25-9c38-34646cdbe43e\\})|(\\{b8b2c0e1-f85d-4acd-aeb1-b6308a473874\\})|(\\{bc0b3499-f772-468e-9de6-b4aaf65d2bbb\\}))$/", + "prefs": [], + "schema": 1535555549913, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1486636", + "why": "Add-ons that hijack search settings.", + "name": "Various malicious add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "fcd12629-43df-4751-9654-7cc008f8f7c0", + "last_modified": 1535555562143 + }, + { + "guid": "/^((\\{25211004-63e4-4a94-9c71-bdfeabb72bfe\\})|(\\{cbf23b92-ea55-4ca9-a5ae-f4197e286bc8\\})|(\\{7ac0550e-19cb-4d22-be12-b0b352144b33\\})|(Mada111@mozilla\\.com)|(\\{c71709a9-af59-4958-a587-646c8c314c16\\})|(\\{6ac3f3b4-18db-4f69-a210-7babefd94b1e\\})|(addon@fastsearch\\.me)|(\\{53d152fa-0ae0-47f1-97bf-c97ca3051562\\})|(\\{f9071611-24ee-472b-b106-f5e2f40bbe54\\})|(\\{972920f1-3bfd-4e99-b605-8688a94c3c85\\})|(\\{985afe98-fa74-4932-8026-4bdc880552ac\\})|(\\{d96a82f5-5d3e-46ed-945f-7c62c20b7644\\})|(\\{3a036dc5-c13b-499a-a62d-e18aab59d485\\})|(\\{49574957-56c6-4477-87f1-1ac7fa1b2299\\})|(\\{097006e8-9a95-4f7c-9c2f-59f20c61771c\\})|(\\{8619885d-0380-467a-b3fe-92a115299c32\\})|(\\{aa0587d6-4760-4abe-b3a1-2a5958f46775\\})|(\\{bdada7ae-cf89-46cf-b1fe-f3681f596278\\})|(\\{649bead3-df51-4023-8090-02ceb2f7095a\\})|(\\{097c3142-0b68-416a-9919-9dd576aedc17\\})|(\\{bc3cced8-51f0-4519-89ee-56706b67ea4b\\})|(\\{796da6e3-01c0-4c63-96dd-1737710b2ff6\\}))$/", + "prefs": [], + "schema": 1535485297866, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1487083", + "why": "Add-ons that hijack search settings and contain other unwanted features.", + "name": "Vairous malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "016676cc-c381-4c01-adcf-2d46f48142d0", + "last_modified": 1535550828514 + }, + { + "guid": "/^((Timemetric@tmetric)|(image-fastpicker@eight04.blogspot\\.com)|(textMarkertool@underFlyingBirches\\.org)|(youpanel@jetpack)|({0ff32ce0-dee9-4e7e-9260-65e58373e21d})|({4ca00873-7e8d-4ada-b460-96cad0eb8fa9})|({6b427f73-2ee1-4256-b69d-7dc253ebe030})|({6f13489d-b274-45b6-80fa-e9daa140e1a4})|({40a9d23b-09ef-4c82-ae1d-7fc5c067e987})|({205c2185-ebe4-4106-92ab-0ffa7c4efcbb})|({256ec7b0-57b4-416d-91c1-2bfdf01b2438})|({568db771-c718-4587-bcd0-e3728ee53550})|({5782a0f1-de26-42e5-a5b3-dae9ec05221b})|({9077390b-89a9-41ad-998f-ab973e37f26f})|({8e7269ac-a171-4d9f-9c0a-c504848fd52f})|({3e6586e2-7410-4f10-bba0-914abfc3a0b4})|({b3f06312-93c7-4a4f-a78b-f5defc185d8f})|({c1aee371-4401-4bab-937a-ceb15c2323c1})|({c579191c-6bb8-4795-adca-d1bf180b512d})|({d0aa0ad2-15ed-4415-8ef5-723f303c2a67})|({d8157e0c-bf39-42eb-a0c3-051ff9724a8c})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({ee97f92d-1bfe-4e9d-816c-0dfcd63a6206}))$/", + "prefs": [], + "schema": 1535356061028, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1485145", + "why": "Add-ons that run remote malicious code from websites that trick the user into installing the add-on.", + "name": "Malware running remote malicious code" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a0d44ee3-9492-47d7-ac1c-35f520e819ae", + "last_modified": 1535393877555 + }, + { + "guid": "/^((fastplayer@fastsearch\\.me)|(ff-search-flash-unlisted@mozilla\\.com)|(inspiratiooo-unlisted@mozilla\\.com)|(lite-search-ff-unlisted@mozilla\\.com)|(mysearchprotect-unlisted@mozilla\\.com)|(pdfconverter-unlisted@mozilla\\.com)|(plugin-search-ff-unlisted@mozilla\\.com)|(pro-search-ff-unlisted@mozilla\\.com)|(pro-search-unlisted@mozilla\\.com)|(searchincognito-unlisted@mozilla\\.com)|(socopoco-search@mozilla\\.com)|(socopoco-unlisted@mozilla\\.com)|(\\{08ea1e08-e237-42e7-ad60-811398c21d58\\})|(\\{0a56e2a0-a374-48b6-9afc-976680fab110\\})|(\\{193b040d-2a00-4406-b9ae-e0d345b53201\\})|(\\{1ffa2e79-7cd4-4fbf-8034-20bcb3463d20\\})|(\\{528cbbe2-3cde-4331-9344-e348cb310783\\})|(\\{6f7c2a42-515a-4797-b615-eaa9d78e8c80\\})|(\\{be2a3fba-7ea2-48b9-bbae-dffa7ae45ef8\\})|(\\{c0231a6b-c8c8-4453-abc9-c4a999a863bd\\}))$/", + "prefs": [], + "schema": 1535139689975, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1483854", + "why": "Add-ons overwriting search changes without consent and remote script injection", + "name": "\"Flash Updater\" and search redirectors" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "46779b5a-2369-4007-bff0-857a657626ba", + "last_modified": 1535153064735 + }, + { + "guid": "/^(({aeac6f90-5e17-46fe-8e81-9007264b907d})|({6ee25421-1bd5-4f0c-9924-79eb29a8889d})|({b317fa11-c23d-45b9-9fd8-9df41a094525})|({16ac3e8f-507a-4e04-966b-0247a196c0b4}))$/", + "prefs": [], + "schema": 1534946831027, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1485609", + "why": "Add-ons that take away user control by changing search settings.", + "name": "Search hijacking malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ab029019-0e93-450a-8c11-ac31556c2a77", + "last_modified": 1535020847820 + }, + { + "guid": "@testpilot-addon", + "prefs": [], + "schema": 1534876689555, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1485083", + "why": "Older versions of the TestPilot add-on cause stability issues in Firefox.", + "name": "Testpilot (old, broken versions)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "2.0.8-dev-259fe19", + "minVersion": "0" + } + ], + "id": "ee2d12a4-ea1d-4f3d-9df1-4303e8993f18", + "last_modified": 1534946810180 + }, + { + "guid": "/^((@svuznnqyxinw)|(myprivacytools@besttools\\.com)|(powertools@penprivacy\\.com)|(privacypro@mybestprivacy\\.com)|(realsecure@top10\\.com)|(rlbvpdfrlbgx@scoutee\\.net)|(vfjkurlfijwz@scoutee\\.net))$/", + "prefs": [], + "schema": 1534382102271, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1482322", + "why": "Add-ons that change the default search engine, taking away user control.", + "name": "Search hijacking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "df852b6a-28be-4b10-9285-869f4761f111", + "last_modified": 1534382538298 + }, + { + "guid": "/^(({1a3fb414-0945-405c-a62a-9fe5e1a50c69})|({1a45f6aa-d80a-4317-84d2-0ce43671b08a})|({2d52a462-8bec-4708-9cd1-894b682bdc78})|({3f841cfc-de5a-421f-8bd7-2bf1d943b02a})|({5c7601bf-522b-47e5-b0f0-ea0e706af443})|({7ebe580f-71c9-4ef8-8073-f38deaeb9dfb})|({8b2188fd-1daf-4851-b387-28d964014353})|({8cee42ac-f1fe-40ae-aed6-24e3b76b2f77})|({8d13c4a9-5e8c-47a6-b583-681c83164ac9})|({9b1d775a-1877-45c9-ad48-d6fcfa4fff39})|({9efdbe5f-6e51-4a35-a41b-71dc939e6221})|({23f63efb-156e-440b-a96c-118bebc21057})|({026dfc8c-ecc8-41ba-b45f-70ffbd5cc672})|({34aa433c-27e9-4c87-a662-9f82f99eb9af})|({36f34d69-f22f-47c3-b4cd-4f37b7676107})|({39bd8607-0af4-4d6b-bd69-9a63c1825d3c})|({48c6ad6d-297c-4074-8fef-ca5f07683859})|({54aa688d-9504-481d-ba75-cfee421b98e0})|({59f59748-e6a8-4b41-87b5-9baadd75ddef})|({61d99407-1231-4edc-acc8-ab96cbbcf151})|({68ca8e3a-397a-4135-a3af-b6e4068a1eae})|({71beafd6-779b-4b7d-a78b-18a107277b59})|({83ed90f8-b07e-4c45-ba6b-ba2fe12cebb6})|({231dfb44-98e0-4bc4-b6ee-1dac4a836b08})|({273f0bce-33f4-45f6-ae03-df67df3864c2})|({392f4252-c731-4715-9f8d-d5815f766abb})|({484ec5d0-4cfd-4d96-88d0-a349bfc33780})|({569dbf47-cc10-41c4-8fd5-5f6cf4a833c7})|({578cad7a-57d5-404d-8dda-4d30de33b0c2})|({986b2c3f-e335-4b39-b3ad-46caf809d3aa})|({1091c11f-5983-410e-a715-0968754cff54})|({2330eb8a-e3fe-4b2e-9f17-9ddbfb96e6f5})|({5920b042-0af1-4658-97c1-602315d3b93d})|({6331a47f-8aae-490c-a9ad-eae786b4349f})|({6698b988-c3ef-4e1f-8740-08d52719eab5})|({30516f71-88d4-489b-a27f-d00a63ad459f})|({12089699-5570-4bf6-890f-07e7f674aa6e})|({84887738-92bf-4903-a5e8-695fd078c657})|({8562e48e-3723-412a-9ebd-b33d3d3b29dd})|({6e449795-c545-41be-92c0-5d467c147389})|({1e369c7c-6b61-436e-8978-4640687670d6})|({a03d427a-bd2e-42b6-828f-a57f38fac7b5})|({a77fc9b9-6ebb-418d-b0b6-86311c191158})|({a368025b-9828-43a1-8a5c-f6fab61c9be9})|({b1908b02-410d-4778-8856-7e259fbf471d})|({b9425ace-c2e9-4ec4-b564-4062546f4eca})|({b9845b5d-70c9-419c-a9a5-98ea8ee5cc01})|({ba99fee7-9806-4e32-8257-a33ffc3b8539})|({bdf8767d-ae4c-4d45-8f95-0ba29b910600})|({c6c4a718-cf91-4648-aa9b-170d66163cf2})|({ca0f2988-e1a8-4e83-afde-0dca56a17d5f})|({cac5db09-979b-40e3-8c8e-d96397b0eecb})|({d3b5280b-f8d8-4669-bdf6-91f23ae58042})|({d73d2f6a-ea24-4b1b-8c76-563fce9f786d})|({d77fed37-85c0-4b94-89bb-0d2849472b8d})|({d371abec-84bb-481b-acbf-235639451127})|({de47a3b4-dad1-4f4a-bdd6-8666586e29e8})|({ded6afad-2aaa-446b-b6bd-b12a8a61c945})|({e0c3a1ca-8e21-4d1b-b53b-ea115cf59172})|({e6bbf496-6489-4b48-8e5a-799aad4aa742})|({e63b262a-f9b8-4496-9c4b-9d3cbd6aea90})|({e73c1b5d-20f7-4d86-ad16-9de3c27718e2})|({eb01dc49-688f-4a21-aa8d-49bd88a8f319})|({edc9816b-60b4-493c-a090-01125e0b8018})|({effa2f97-0f07-44c8-99cb-32ac760a0621})|({f6e6fd9b-b89f-4e8d-9257-01405bc139a6})|({ff87977a-fefb-4a9d-b703-4b73dce8853d})|({ffea9e62-e516-4238-88a7-d6f9346f4955}))$/", + "prefs": [], + "schema": 1534335096640, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1483191", + "why": "Add-ons that change the default search engine, taking away user control.", + "name": "Search hijacking add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d9892a76-b22e-40bd-8073-89b0f8110ec7", + "last_modified": 1534336165428 + }, + { + "guid": "/^((Timemetric@tmetric)|(textMarkertool@underFlyingBirches\\.org)|(youpanel@jetpack)|({6f13489d-b274-45b6-80fa-e9daa140e1a4})|({568db771-c718-4587-bcd0-e3728ee53550})|({829827cd-03be-4fed-af96-dd5997806fb4})|({9077390b-89a9-41ad-998f-ab973e37f26f})|({8e7269ac-a171-4d9f-9c0a-c504848fd52f})|({aaaffe20-3306-4c64-9fe5-66986ebb248e})|({bf153de7-cdf2-4554-af46-29dabfb2aa2d})|({c579191c-6bb8-4795-adca-d1bf180b512d})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({ee97f92d-1bfe-4e9d-816c-0dfcd63a6206}))$/", + "prefs": [], + "schema": 1534275699570, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1483206", + "why": "Add-ons that execute malicious remote code", + "name": "Various malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2734325e-143b-4962-98bf-4b18c77407e2", + "last_modified": 1534334500118 + }, + { + "guid": "{5834f62d-6164-4cdd-a0a3-c00c66ec9d13}", + "prefs": [], + "schema": 1532704368947, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1479002", + "why": "This add-on violates our security and user-choice/no surprises policies.", + "name": "Youtube Dark Mode (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d0a401cb-0c70-4784-8288-b06a88b2ae8a", + "last_modified": 1532705151926 + }, + { + "guid": "/^((@asdfjhsdfuhw)|(@asdfsdfwe)|(@asdieieuss)|(@dghfghfgh)|(@difherk)|(@dsfgtftgjhrdf4)|(@fidfueir)|(@fsgergsdqtyy)|(@hjconsnfes)|(@isdifvdkf)|(@iweruewir)|(@oiboijdjfj)|(@safesearchavs)|(@safesearchavsext)|(@safesearchincognito)|(@safesearchscoutee)|(@sdfykhhhfg)|(@sdiosuff)|(@sdklsajd)|(@sduixcjksd)|(@sicognitores)|(@simtabtest)|(@sodiasudi)|(@test13)|(@test131)|(@test131ver)|(@test132)|(@test13s)|(@testmptys)|(\\{ac4e5b0c-13c4-4bfd-a0c3-1e73c81e8bac\\})|(\\{e78785c3-ec49-44d2-8aac-9ec7293f4a8f\\})|(general@filecheckerapp\\.com)|(general@safesearch\\.net))$/", + "prefs": [], + "schema": 1532703832328, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1475330", + "why": "These Add-ons violate our data collection, no surprises and user-choice policies.", + "name": "Safesearch (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "d664412d-ed08-4892-b247-b007a70856ff", + "last_modified": 1532704364007 + }, + { + "guid": "{dd3d7613-0246-469d-bc65-2a3cc1668adc}", + "prefs": [], + "schema": 1532684052432, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1478731", + "why": "This add-on violates data practices outlined in the review policy.", + "name": "BlockSite" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "4.0.3", + "minVersion": "0" + } + ], + "id": "e04f98b5-4480-43a3-881d-e509e4e28cdc", + "last_modified": 1532684085999 + }, + { + "guid": "{bee8b1f2-823a-424c-959c-f8f76c8b2306}", + "prefs": [], + "schema": 1532547689407, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1478731", + "why": "This add-on violates data practices outlined in the review policy.", + "name": "Popup blocker for FireFox" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "4.0.7.3", + "minVersion": "0" + } + ], + "id": "f0713a5e-7208-484e-b3a0-4e6dc6a195be", + "last_modified": 1532684052426 + }, + { + "guid": "/^((\\{39bd8607-0af4-4d6b-bd69-9a63c1825d3c\\})|(\\{273f0bce-33f4-45f6-ae03-df67df3864c2\\})|(\\{a77fc9b9-6ebb-418d-b0b6-86311c191158\\})|(\\{c6c4a718-cf91-4648-aa9b-170d66163cf2\\})|(\\{d371abec-84bb-481b-acbf-235639451127\\})|(\\{e63b262a-f9b8-4496-9c4b-9d3cbd6aea90\\}))$/", + "prefs": [], + "schema": 1532386339902, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1477950", + "why": "Add-ons that contain malicious functionality like search engine redirect.", + "name": "Smash (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c37c7c24-e738-4d06-888c-108b4d63b428", + "last_modified": 1532424286908 + }, + { + "guid": "/^((\\{ac296b47-7c03-486f-a1d6-c48b24419749\\})|(\\{1cab8ccf-deff-4743-925d-a47cbd0a6b56\\})|(\\{5da81d3d-5db1-432a-affc-4a2fe9a70749\\})|(\\{071b9878-a7d3-4ae3-8ef0-2eaee1923403\\})|(\\{261476ea-bd0e-477c-abd7-33cdf626f81f\\})|(\\{224e66d0-6b11-4c4b-9bcf-41180889898a\\})|(\\{1e90cf52-c67c-4bd9-80c3-a2bf521fc981\\})|(\\{09c4799c-00f1-439e-9e60-3827c589b372\\})|(\\{d3d2095a-9faa-466f-82ae-3114179b34d6\\})|(\\{70389ea5-7e4d-4515-835c-fbd047f229dd\\})|(\\{2e8083a5-cd88-4aaa-bb8b-e54e9753f280\\})|(\\{fbf2480b-5c19-478e-bfd0-192ad9f84dc9\\})|(\\{6c7dc694-89f8-477e-88d5-c55af4d6a846\\})|(\\{915c12c6-901a-490d-9bfc-20f00d1ad31d\\})|(\\{d3a4aa3e-f74c-4382-876d-825f592f2976\\})|(\\{0ad91ec1-f7c4-4a39-9244-3310e9fdd169\\})|(\\{9c17aa27-63c5-470a-a678-dc899ab67ed3\\})|(\\{c65efef2-9988-48db-9e0a-9ff8164182b6\\})|(\\{d54c5d25-2d51-446d-8d14-18d859e3e89a\\})|(\\{e458f1f1-a331-4486-b157-81cba19f0993\\})|(\\{d2de7e1f-6e51-41d6-ba8a-937f8a5c92ff\\})|(\\{2b08a649-9bea-4dd4-91c8-f53a84d38e19\\})|(\\{312dd57e-a590-4e19-9b26-90e308cfb103\\})|(\\{82ce595a-f9b6-4db8-9c97-b1f1c933418b\\})|(\\{0a2e64f0-ea5a-4fff-902d-530732308d8e\\})|(\\{5fbdc975-17ab-4b4e-90d7-9a64fd832a08\\})|(\\{28820707-54d8-41f0-93e9-a36ffb2a1da6\\})|(\\{64a2aed1-5dcf-4f2b-aad6-9717d23779ec\\})|(\\{ee54794f-cd16-4f7d-a7dd-515a36086f60\\})|(\\{4d381160-b2d5-4718-9a05-fc54d4b307e7\\})|(\\{60393e0e-f039-4b80-bad4-10189053c2ab\\})|(\\{0997b7b2-52d7-4d14-9aa6-d820b2e26310\\})|(\\{8214cbd6-d008-4d16-9381-3ef1e1415665\\})|(\\{6dec3d8d-0527-49a3-8f12-b05f2a8b95b2\\})|(\\{0c0d8d8f-3ae0-4c98-81ac-06453a316d16\\})|(\\{84d5ef02-a283-484a-80da-7087836c74aa\\})|(\\{24413756-2c44-47c5-8bbf-160cb37776d8\\})|(\\{cf6ac458-06e8-45d0-9cbf-ec7fc0eb1710\\})|(\\{263a5792-933a-4de1-820a-d04198e17120\\})|(\\{b5fd7f37-190d-4c0a-b8dd-8b4850c986ac\\})|(\\{cb5ef07b-c2e7-47a6-be81-2ceff8df4dd5\\})|(\\{311b20bc-b498-493c-a5e1-22ec32b0e83c\\})|(\\{b308aead-8bc1-4f37-9324-834b49903df7\\})|(\\{3a26e767-b781-4e21-aaf8-ac813d9edc9f\\}))$/", + "prefs": [], + "schema": 1532361925873, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1476553", + "why": "Third-party websites try to trick users into installing add-ons that inject remote scripts.", + "name": "Various malicious add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "52842139-3d11-41ac-9d7f-8e51122a3141", + "last_modified": 1532372344457 + }, + { + "guid": "{46551EC9-40F0-4e47-8E18-8E5CF550CFB8}", + "prefs": [], + "schema": 1530711142817, + "blockID": "i1900", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1472948", + "why": "This add-on violates data practices outlined in the review policy.", + "name": "Stylish" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.1.1", + "minVersion": "3.0.0" + } + ], + "id": "c635229f-7aa0-44c5-914f-80c590949071", + "last_modified": 1530716488758 + }, + { + "guid": "/^(contactus@unzipper.com|{72dcff4e-48ce-41d8-a807-823adadbe0c9}|{dc7d2ecc-9cc3-40d7-93ed-ef6f3219bd6f}|{994db3d3-ccfe-449a-81e4-f95e2da76843}|{25aef460-43d5-4bd0-aa3d-0a46a41400e6}|{178e750c-ae27-4868-a229-04951dac57f7})$/", + "prefs": [], + "schema": 1528400492025, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1460331", + "why": "Add-ons change search settings against our policies, affecting core Firefox features. Add-on is also reportedly installed without user consent.", + "name": "SearchWeb" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5afea853-d029-43f3-a387-64ce9980742a", + "last_modified": 1528408770328 + }, + { + "guid": "{38363d75-6591-4e8b-bf01-0270623d1b6c}", + "prefs": [], + "schema": 1526326889114, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1461625", + "why": "This add-on contains abusive functionality.", + "name": "Photobucket Hotlink Fix" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0f0764d5-a290-428b-a5b2-3767e1d72c71", + "last_modified": 1526381862851 + }, + { + "guid": "@vkmad", + "prefs": [], + "schema": 1526154098016, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1461410", + "why": "This add-on includes malicious functionality.", + "name": "VK Universal Downloader (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cbfa5303-c1bf-49c8-87d8-259738a20064", + "last_modified": 1526322954850 + }, + { + "guid": "/((@extcorp\\.[a-z]+)|(@brcorporation\\.com)|(@brmodcorp\\.com)|(@teset\\.com)|(@modext\\.tech)|(@ext?mod\\.net)|(@browcorporation\\.org)|(@omegacorporation\\.org)|(@browmodule\\.com)|(@corpext\\.net)|({6b50ddac-f5e0-4d9e-945b-e4165bfea5d6})|({fab6484f-b8a7-4ba9-a041-0f948518b80c})|({b797035a-7f29-4ff5-bd19-77f1b5e464b1})|({0f612416-5c5a-4ec8-b482-eb546af9cac4}))$/", + "prefs": [], + "schema": 1525290095999, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1458330", + "why": "These are malicious add-ons that inject remote scripts and use deceptive names.", + "name": "\"Table\" add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3ab9f100-e253-4080-b3e5-652f842ddb7a", + "last_modified": 1525377099954 + }, + { + "guid": "/^({b99ae7b1-aabb-4674-ba8f-14ed32d04e76})|({dfa77d38-f67b-4c41-80d5-96470d804d09})$/", + "prefs": [], + "schema": 1524146566650, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1455291", + "why": "These add-ons claim to be the flash plugin.", + "name": "Flash Plugin (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "96b137e6-8cb5-44d6-9a34-4a4a76fb5e38", + "last_modified": 1524147337556 + }, + { + "guid": "/^({6ecb9f49-90f0-43a1-8f8a-e809ea4f732b})|(@googledashboard)|(@smashdashboard)|(@smash_tv)|(@smash_mov)|(@smashmovs)|(@smashtvs)|(@FirefoxUpdate)|({92b9e511-ac81-4d47-9b8f-f92dc872447e})|({3c841114-da8c-44ea-8303-78264edfe60b})|({116a0754-20eb-4fe5-bd35-575867a0b89e})|({6e6ff0fd-4ae4-49ae-ac0c-e2527e12359b})|({f992ac88-79d3-4960-870e-92c342ed3491})|({6ecb9f49-90f0-43a1-8f8a-e809ea4f732b})|({a512297e-4d3a-468c-bd1a-f77bd093f925})|({08c28c16-9fb6-4b32-9868-db37c1668f94})|({b4ab1a1d-e137-4c59-94d5-4f509358a81d})|({feedf4f8-08c1-451f-a717-f08233a64ec9})$/", + "prefs": [], + "schema": 1524139371832, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1454691", + "why": "This malware prevents itself from getting uninstalled ", + "name": "Malware" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "feb2d0d7-1b76-4dba-bf84-42873a92af5f", + "last_modified": 1524141477640 + }, + { + "guid": "{872f20ea-196e-4d11-8835-1cc4c877b1b8}", + "prefs": [], + "schema": 1523734896380, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1454413", + "why": "Extension claims to be Flash Player", + "name": "Flash Player (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "1e5f5cb2-346c-422a-9aaa-29d8760949d2", + "last_modified": 1523897202689 + }, + { + "guid": "adbeaver@adbeaver.org", + "prefs": [], + "schema": 1521630548030, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1445031", + "why": "This add-on generates numerous errors when loading Facebook, caused by ad injection included in it. Users who want to continue using this add-on can enable it in the Add-ons Manager.", + "name": "AdBeaver" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "baf7f735-d6b6-410a-8cc8-25c60f7c57e2", + "last_modified": 1522103097333 + }, + { + "guid": "{44685ba6-68b3-4895-879e-4efa29dfb578}", + "prefs": [], + "schema": 1521565140013, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1447042", + "why": "This add-on impersonates a Flash tool and runs remote code on users' systems.", + "name": "FF Flash Manager" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "547037f2-97ae-435a-863c-efd7532668cd", + "last_modified": 1521630548023 + }, + { + "guid": "/^(addon@fasterweb\\.com|\\{5f398d3f-25db-47f5-b422-aa2364ff6c0b\\}|addon@fasterp\\.com|addon@calculator)$/", + "prefs": [], + "schema": 1520338910918, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1443478", + "why": "These are malicious add-ons that use deceptive names and run remote scripts.", + "name": "FasterWeb add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f58729ec-f93c-41d9-870d-dd9c9fd811b6", + "last_modified": 1520358450708 + }, + { + "guid": "{42baa93e-0cff-4289-b79e-6ae88df668c4}", + "prefs": [], + "schema": 1520336325565, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1443196", + "why": "The add-on claims to be \"Adobe Shockwave Flash Player\"", + "name": "Adobe Shockwave Flash Player (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0cd723fe-d33d-43a0-b84f-7a3cad253212", + "last_modified": 1520338780397 + }, + { + "guid": "{f3c31b34-862c-4bc8-a98f-910cc6314a86}", + "prefs": [], + "schema": 1519242096699, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1440736", + "why": "This is a malicious add-on that is masked as an official Adobe Updater and runs malicious code.", + "name": "Adobe Updater (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "adfd98ef-cebc-406b-b1e0-61bd4c71e4b1", + "last_modified": 1519409417397 + }, + { + "guid": "/^(\\{fd0c36fa-6a29-4246-810b-0bb4800019cb\\}|\\{b9c1e5bf-6585-4766-93fc-26313ac59999\\}|\\{3de25fff-25e8-40e9-9ad9-fdb3b38bb2f4\\})$/", + "prefs": [], + "schema": 1519069296530, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1439432", + "why": "These are malicious add-ons that are masked as an official Adobe Updater and run malicious code.", + "name": "Adobe Updater" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4e28ba5c-af62-4e53-a7a1-d33334571cf8", + "last_modified": 1519078890592 + }, + { + "guid": "/^(\\{01c9a4a4-06dd-426b-9500-2ea6fe841b88\\}|{5e024309-042c-4b9d-a634-5d92cf9c7514\\}|{f4262989-6de0-4604-918f-663b85fad605\\}|{e341ed12-a703-47fe-b8dd-5948c38070e4\\}|{cd89045b-2e06-46bb-9e34-48e8799e5ef2\\}|{ac296b47-7c03-486f-a1d6-c48b24419749\\}|{5da81d3d-5db1-432a-affc-4a2fe9a70749\\}|{df09f268-3c92-49db-8c31-6a25a6643896\\}|{81ac42f3-3d17-4cff-85af-8b7f89c8826b\\}|{09c8fa16-4eec-4f78-b19d-9b24b1b57e1e\\}|{71639610-9cc3-47e0-86ed-d5b99eaa41d5\\}|{83d38ac3-121b-4f28-bf9c-1220bd3c643b\\}|{7f8bc48d-1c7c-41a0-8534-54adc079338f\\})$/", + "prefs": [], + "schema": 1518550894975, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1438028", + "why": "These are malicious add-ons that inject remote scripts into popular websites.", + "name": "Page Marker add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "cc5848e8-23d5-4655-b45c-dc239839b74e", + "last_modified": 1518640450735 + }, + { + "guid": "/^(https|youtube)@vietbacsecurity\\.com$/", + "prefs": [], + "schema": 1517909997354, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1435974", + "why": "These add-ons contain malicious functionality, violating the users privacy and security.", + "name": "HTTPS and Youtube downloader (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "646e2384-f894-41bf-b7fc-8879e0095109", + "last_modified": 1517910100624 + }, + { + "guid": "{ed352072-ddf0-4cb4-9cb6-d8aa3741c2de}", + "prefs": [], + "schema": 1517514097126, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1434893", + "why": "This is a malicious add-on that injects remote scripts into popular pages while pretending to do something else.", + "name": "Image previewer" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2104a522-bb2f-4b04-ad0d-b0c571644552", + "last_modified": 1517577111194 + }, + { + "guid": "/^({be5d0c88-571b-4d01-a27a-cc2d2b75868c})|({3908d078-e1db-40bf-9567-5845aa77b833})|({5b620343-cd69-49b8-a7ba-f9d499ee5d3d})|({6eee2d17-f932-4a43-a254-9e2223be8f32})|({e05ba06a-6d6a-4c51-b8fc-60b461ffecaf})|({a5808da1-5b4f-42f2-b030-161fd11a36f7})|({d355bee9-07f0-47d3-8de6-59b8eecba57b})|({a1f8e136-bce5-4fd3-9ed1-f260703a5582})$/", + "prefs": [], + "schema": 1517260691761, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.\n", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "70f37cc7-9f8a-4d0f-a881-f0c56934fa75", + "last_modified": 1517260722621 + }, + { + "guid": "/^({d78d27f4-9716-4f13-a8b6-842c455d6a46})|({bd5ba448-b096-4bd0-9582-eb7a5c9c0948})|({0b24cf69-02b8-407d-83db-e7af04fc1f3e})|({e08d85c5-4c0f-4ce3-9194-760187ce93ba})|({1c7d6d9e-325a-4260-8213-82d51277fc31})|({8a0699a0-09c3-4cf1-b38d-fec25441650c})|({1e68848a-2bb7-425c-81a2-524ab93763eb})$/", + "prefs": [], + "schema": 1517168490224, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "805ee80e-0929-4c92-93ed-062b98053f28", + "last_modified": 1517260691755 + }, + { + "guid": "/^({abec23c3-478f-4a5b-8a38-68ccd500ec42}|{a83c1cbb-7a41-41e7-a2ae-58efcb4dc2e4}|{62237447-e365-487e-8fc3-64ddf37bdaed}|{b12cfdc7-3c69-43cb-a3fb-38981b68a087}|{1a927d5b-42e7-4407-828a-fdc441d0daae}|{dd1cb0ec-be2a-432b-9c90-d64c824ac371}|{82c8ced2-e08c-4d6c-a12b-3e8227d7fc2a}|{87c552f9-7dbb-421b-8deb-571d4a2d7a21})$/", + "prefs": [], + "schema": 1516828883529, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "c92f2a05-73eb-454e-9583-f6d2382d8bca", + "last_modified": 1516829074251 + }, + { + "guid": "/^({618baeb9-e694-4c7b-9328-69f35b6a8839}|{b91fcda4-88b0-4a10-9015-9365e5340563}|{04150f98-2d7c-4ae2-8979-f5baa198a577}|{4b1050c6-9139-4126-9331-30a836e75db9}|{1e6f5a54-2c4f-4597-aa9e-3e278c617d38}|{e73854da-9503-423b-ab27-fafea2fbf443}|{a2427e23-d349-4b25-b5b8-46960b218079}|{f92c1155-97b3-40f4-9d5b-7efa897524bb}|{c8e14311-4b2d-4eb0-9a6b-062c6912f50e}|{45621564-b408-4c29-8515-4cf1f26e4bc3}|{27380afd-f42a-4c25-b57d-b9012e0d5d48})$/", + "prefs": [], + "schema": 1516828883529, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "2d4fe65b-6c02-4461-baa8-dda52e688cf6", + "last_modified": 1516829040469 + }, + { + "guid": "/^({4dac7c77-e117-4cae-a9f0-6bd89e9e26ab}|{cc689da4-203f-4a0c-a7a6-a00a5abe74c5}|{0eb4672d-58a6-4230-b74c-50ca3716c4b0}|{06a71249-ef35-4f61-b2c8-85c3c6ee5617}|{5280684d-f769-43c9-8eaa-fb04f7de9199}|{c2341a34-a3a0-4234-90cf-74df1db0aa49}|{85e31e7e-3e3a-42d3-9b7b-0a2ff1818b33}|{b5a35d05-fa28-41b5-ae22-db1665f93f6b}|{1bd8ba17-b3ed-412e-88db-35bc4d8771d7}|{a18087bb-4980-4349-898c-ca1b7a0e59cd}|{488e190b-d1f6-4de8-bffb-0c90cc805b62})$/", + "prefs": [], + "schema": 1516828883529, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9a3fd797-0ab8-4286-9a1b-2b6c97f9075b", + "last_modified": 1516829006347 + }, + { + "guid": "/^({f6df4ef7-14bd-43b5-90c9-7bd02943789c}|{ccb7b5d6-a567-40a2-9686-a097a8b583dd}|{9b8df895-fcdd-452a-8c46-da5be345b5bc}|{5cf77367-b141-4ba4-ac2a-5b2ca3728e81}|{ada56fe6-f6df-4517-9ed0-b301686a34cc}|{95c7ae97-c87e-4827-a2b7-7b9934d7d642}|{e7b978ae-ffc2-4998-a99d-0f4e2f24da82}|{115a8321-4414-4f4c-aee6-9f812121b446}|{bf153de7-cdf2-4554-af46-29dabfb2aa2d}|{179710ba-0561-4551-8e8d-1809422cb09f}|{9d592fd5-e655-461a-9b28-9eba85d4c97f})$/", + "prefs": [], + "schema": 1516828883529, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "aae78cd5-6b26-472e-ab2d-db4105911250", + "last_modified": 1516828973824 + }, + { + "guid": "/^({30972e0a-f613-4c46-8c87-2e59878e7180}|{0599211f-6314-4bf9-854b-84cb18da97f8}|{4414af84-1e1f-449b-ac85-b79f812eb69b}|{2a8bec00-0ab0-4b4d-bd3d-4f59eada8fd8}|{bea8866f-01f8-49e9-92cd-61e96c05d288}|{046258c9-75c5-429d-8d5b-386cfbadc39d}|{c5d359ff-ae01-4f67-a4f7-bf234b5afd6e}|{fdc0601f-1fbb-40a5-84e1-8bbe96b22502}|{85349ea6-2b5d-496a-9379-d4be82c2c13d}|{640c40e5-a881-4d16-a4d0-6aa788399dd2}|{d42328e1-9749-46ba-b35c-cce85ddd4ace})$/", + "prefs": [], + "schema": 1516828883529, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "750aa293-3742-46b5-8761-51536afecaef", + "last_modified": 1516828938683 + }, + { + "guid": "/^({d03b6b0f-4d44-4666-a6d6-f16ad9483593}|{767d394a-aa77-40c9-9365-c1916b4a2f84}|{a0ce2605-b5fc-4265-aa65-863354e85058}|{b7f366fa-6c66-46bf-8df2-797c5e52859f}|{4ad16913-e5cb-4292-974c-d557ef5ec5bb}|{3c3ef2a3-0440-4e77-9e3c-1ca8d48f895c}|{543f7503-3620-4f41-8f9e-c258fdff07e9}|{98363f8b-d070-47b6-acc6-65b80acac4f3}|{5af74f5a-652b-4b83-a2a9-f3d21c3c0010}|{484e0ba4-a20b-4404-bb1b-b93473782ae0}|{b99847d6-c932-4b52-9650-af83c9dae649})$/", + "prefs": [], + "schema": 1516828883529, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "a29aed6f-6546-4fa2-8131-df5c9a5427af", + "last_modified": 1516828911059 + }, + { + "guid": "/^({2bb68b03-b528-4133-9fc4-4980fbb4e449}|{231e58ac-0f3c-460b-bb08-0e589360bec7}|{a506c5af-0f95-4107-86f8-3de05e2794c9}|{8886a262-1c25-490b-b797-2e750dd9f36b}|{65072bef-041f-492e-8a51-acca2aaeac70}|{6fa41039-572b-44a4-acd4-01fdaebf608d}|{87ba49bd-daba-4071-aedf-4f32a7e63dbe}|{95d58338-ba6a-40c8-93fd-05a34731dc0e}|{4cbef3f0-4205-4165-8871-2844f9737602}|{1855d130-4893-4c79-b4aa-cbdf6fee86d3}|{87dcb9bf-3a3e-4b93-9c85-ba750a55831a})$/", + "prefs": [], + "schema": 1516822896448, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are malicious add-ons that automatically close the Add-ons Manager.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5c092b0d-7205-43a1-aa75-b7a42372fb52", + "last_modified": 1516828883523 + }, + { + "guid": "/^({fce89242-66d3-4946-9ed0-e66078f172fc})|({0c4df994-4f4a-4646-ae5d-8936be8a4188})|({6cee30bc-a27c-43ea-ac72-302862db62b2})|({e08ebf0b-431d-4ed1-88bb-02e5db8b9443})$/", + "prefs": [], + "schema": 1516650096284, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1432560", + "why": "These are malicious add-ons that make it hard for the user to be removed.", + "name": "FF AntiVir Monitoring" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9dfeee42-e6a8-49e0-8979-0648f7368239", + "last_modified": 1516744119329 + }, + { + "guid": "/^(\\{1490068c-d8b7-4bd2-9621-a648942b312c\\})|(\\{d47ebc8a-c1ea-4a42-9ca3-f723fff034bd\\})|(\\{83d6f65c-7fc0-47d0-9864-a488bfcaa376\\})|(\\{e804fa4c-08e0-4dae-a237-8680074eba07\\})|(\\{ea618d26-780e-4f0f-91fd-2a6911064204\\})|(\\{ce93dcc7-f911-4098-8238-7f023dcdfd0d\\})|(\\{7eaf96aa-d4e7-41b0-9f12-775c2ac7f7c0\\})|(\\{b019c485-2a48-4f5b-be13-a7af94bc1a3e\\})|(\\{9b8a3057-8bf4-4a9e-b94b-867e4e71a50c\\})|(\\{eb3ebb14-6ced-4f60-9800-85c3de3680a4\\})|(\\{01f409a5-d617-47be-a574-d54325fe05d1\\})$/", + "prefs": [], + "schema": 1516394914836, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are a set of malicious add-ons that block the add-ons manager tab from opening so they can't be uninstalled.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5bf72f70-a611-4845-af3f-d4dabe8862b6", + "last_modified": 1516394982586 + }, + { + "guid": "/^(\\{ac06c6b2-3fd6-45ee-9237-6235aa347215\\})|(\\{d461cc1b-8a36-4ff0-b330-1824c148f326\\})|(\\{d1ab5ebd-9505-481d-a6cd-6b9db8d65977\\})|(\\{07953f60-447e-4f53-a5ef-ed060487f616\\})|(\\{2d3c5a5a-8e6f-4762-8aff-b24953fe1cc9\\})|(\\{f82b3ad5-e590-4286-891f-05adf5028d2f\\})|(\\{f96245ad-3bb0-46c5-8ca9-2917d69aa6ca\\})|(\\{2f53e091-4b16-4b60-9cae-69d0c55b2e78\\})|(\\{18868c3a-a209-41a6-855d-f99f782d1606\\})|(\\{47352fbf-80d9-4b70-9398-fb7bffa3da53\\})$/", + "prefs": [], + "schema": 1516311993443, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1431748", + "why": "These are a set of malicious add-ons that block the add-ons manager tab from opening so they can't be uninstalled.", + "name": "FF Tool" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "4ca8206f-bc2a-4428-9439-7f3142dc08db", + "last_modified": 1516394914828 + }, + { + "guid": "{5b0f6d3c-10fd-414c-a135-dffd26d7de0f}", + "prefs": [], + "schema": 1516131689499, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1430577", + "why": "This is a malicious add-on that executes remote scripts, redirects popular search URLs and tracks users.", + "name": "P Birthday" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "8088b39a-3e6d-4a17-a22f-3f95c0464bd6", + "last_modified": 1516303320468 + }, + { + "guid": "{1490068c-d8b7-4bd2-9621-a648942b312c}", + "prefs": [], + "schema": 1515267698296, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1428754", + "why": "This add-on is using a deceptive name and performing unwanted actions on users' systems.", + "name": "FF Safe Helper" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "674b6e19-f087-4706-a91d-1e723ed6f79e", + "last_modified": 1515433728497 + }, + { + "guid": "{dfa727cb-0246-4c5a-843a-e4a8592cc7b9}", + "prefs": [], + "schema": 1514922095288, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1426582", + "why": "Version 2.0.0 shipped with a hidden coin miner, which degrades performance in users who have it enabled. Version 1.2.3 currently available on AMO is not affected.", + "name": "Open With Adobe PDF Reader 2.0.0" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "2.0.0", + "minVersion": "2.0.0" + } + ], + "id": "455772a3-8360-4f5a-9a5f-a45b904d0b51", + "last_modified": 1515007270887 + }, + { + "guid": "{d03b6b0f-4d44-4666-a6d6-f16ad9483593}", + "prefs": [], + "schema": 1513366896461, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1425581", + "why": "This is a malicious add-on posing as a legitimate update.", + "name": "FF Guard Tool (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "10d9ce89-b8d4-4b53-b3d7-ecd192681f4e", + "last_modified": 1513376470395 + }, + { + "guid": "{7e907a15-0a4c-4ff4-b64f-5eeb8f841349}", + "prefs": [], + "schema": 1510083698490, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1411885", + "why": "This is a malicious add-on posing as a legitimate update.", + "name": "Manual Update" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "f7569261-f575-4719-8202-552b20d013b0", + "last_modified": 1510168860382 + }, + { + "guid": "{3602008d-8195-4860-965a-d01ac4f9ca96}", + "prefs": [], + "schema": 1509120801051, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1411885", + "why": "This is a malicious add-on posing as a legitimate antivirus.\n", + "name": "Manual Antivirus" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "28c805a9-e692-4ef8-b3ae-14e085c19ecd", + "last_modified": 1509120934909 + }, + { + "guid": "{87010166-e3d0-4db5-a394-0517917201df}", + "prefs": [], + "schema": 1509120801051, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1411885", + "why": "This is a malicious add-on posing as a legitimate antivirus.\n", + "name": "Manual Antivirus" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "84dd8a02-c879-4477-8ea7-bf2f225b0940", + "last_modified": 1509120881470 + }, + { + "guid": "{8ab60777-e899-475d-9a4f-5f2ee02c7ea4}", + "prefs": [], + "schema": 1509120801051, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1411885", + "why": "This is a malicious add-on posing as a legitimate antivirus.\n", + "name": "Manual Antivirus" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "ccebab59-7190-4258-8faa-a0b752dd5301", + "last_modified": 1509120831329 + }, + { + "guid": "{368eb817-31b4-4be9-a761-b67598faf9fa}", + "prefs": [], + "schema": 1509046897080, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1411885", + "why": "This is a malicious add-on posing as a legitimate antivirus.", + "name": "Manual Antivirus" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "9abc7502-bd6f-40d7-b035-abe721345360", + "last_modified": 1509120801043 + }, + { + "guid": "@68eba425-7a05-4d62-82b1-1d6d5a51716b", + "prefs": [], + "schema": 1505072496256, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1398905", + "why": "Misleads users into thinking this is a security and privacy tool (also distributed on a site that makes it look like an official Mozilla product).", + "name": "SearchAssist Incognito" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "595e0e53-b76b-4188-a160-66f29c636094", + "last_modified": 1505211411253 + }, + { + "guid": "@H99KV4DO-UCCF-9PFO-9ZLK-8RRP4FVOKD9O", + "prefs": [], + "schema": 1502483549048, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1340877", + "why": "This is a malicious add-on that is being installed silently.", + "name": "FF Adr (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "5df16afc-c804-43c9-9de5-f1835403e5fb", + "last_modified": 1502483601731 + }, + { + "guid": "@DA3566E2-F709-11E5-8E87-A604BC8E7F8B", + "prefs": [], + "schema": 1502480491460, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1340877", + "why": "This is a malicious add-on that is being installed silently into users' systems.", + "name": "SimilarWeb (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "0a47a2f7-f07c-489b-bd39-88122a2dfe6a", + "last_modified": 1502483549043 + }, + { + "guid": "/^({95E84BD3-3604-4AAC-B2CA-D9AC3E55B64B}|{E3605470-291B-44EB-8648-745EE356599A}|{95E5E0AD-65F9-4FFC-A2A2-0008DCF6ED25}|{FF20459C-DA6E-41A7-80BC-8F4FEFD9C575}|{6E727987-C8EA-44DA-8749-310C0FBE3C3E}|{12E8A6C2-B125-479F-AB3C-13B8757C7F04}|{EB6628CF-0675-4DAE-95CE-EFFA23169743})$/", + "prefs": [], + "schema": 1494022576295, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1362585", + "why": "All of these add-ons have been identified as malware, and are being installed in Firefox globally, most likely via a malicious application installer.", + "name": "Malicious globally-installed add-ons" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "3fd71895-7fc6-4f3f-aa22-1cbb0c5fd922", + "last_modified": 1494024191520 + }, + { + "guid": "@safesearchscoutee", + "prefs": [], + "schema": 1494013289942, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1362553", + "why": "This add-on intercepts queries sent to search engines and replaces them with its own, without user consent.", + "name": "SafeSearch Incognito (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0" + } + ], + "id": "edad04eb-ea16-42f3-a4a7-20dded33cc37", + "last_modified": 1494022568654 + }, + { + "guid": "msktbird@mcafee.com", + "prefs": [], + "schema": 1493150718059, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1354912", + "why": "These versions of this add-on are known to cause frequent crashes in Thunderbird.", + "name": "McAfee Anti-Spam Thunderbird Extension 2.0 and lower" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "2.0", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{3550f703-e582-4d05-9a08-453d09bdfdc6}", + "maxVersion": "*", + "minVersion": "0" + } + ] + } + ], + "id": "9e86d1ff-727a-45e3-9fb6-17f32666daf2", + "last_modified": 1493332747360 + }, + { + "guid": "/^(\\{11112503-5e91-4299-bf4b-f8c07811aa50\\})|(\\{501815af-725e-45be-b0f2-8f36f5617afc\\})$/", + "prefs": [], + "schema": 1491421290217, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1354045", + "why": "This add-on steals user credentials for popular websites from Facebook.", + "name": "Flash Player Updater (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c142360c-4f93-467e-9717-b638aa085d95", + "last_modified": 1491472107658 + }, + { + "guid": "fr@fbt.ovh", + "prefs": [], + "schema": 1490898754477, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1351689", + "why": "Scam add-on that silently steals user credentials of popular websites", + "name": "Adobe Flash Player (Malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0f8344d0-8211-49a1-81be-c0084b3da9b1", + "last_modified": 1490898787752 + }, + { + "guid": "{95E84BD3-3604-4AAC-B2CA-D9AC3E55B64B}", + "prefs": [], + "schema": 1487179851382, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1338690", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that is silently installed in users' systems.", + "name": "youtube adblock (malware)" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "04b25e3d-a725-493e-be07-cbd74fb37ea7", + "last_modified": 1487288975999 + }, + { + "guid": "ext@alibonus.com", + "prefs": [], + "schema": 1485297431051, + "blockID": "i1524", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1333471", + "who": "All Firefox users who have these versions installed.", + "why": "Versions 1.20.9 and lower of this add-on contain critical security issues.", + "name": "Alibonus 1.20.9 and lower", + "created": "2017-01-24T22:45:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.20.9", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a015d5a4-9184-95db-0c74-9262af2332fa", + "last_modified": 1485301116629 + }, + { + "guid": "/^(ciscowebexstart1@cisco\\.com|ciscowebexstart_test@cisco\\.com|ciscowebexstart@cisco\\.com|ciscowebexgpc@cisco\\.com)$/", + "prefs": [], + "schema": 1485212610474, + "blockID": "i1522", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1333225", + "who": "All Firefox users who have any Cisco WebEx add-ons installed.", + "why": "A critical security vulnerability has been discovered in Cisco WebEx add-ons that enable malicious websites to execute code on the user's system.", + "name": "Cisco WebEx add-ons", + "created": "2017-01-23T22:55:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.0.1", + "minVersion": "1.0.0", + "targetApplication": [] + } + ], + "id": "30368779-1d3b-490a-0a34-253085af7754", + "last_modified": 1485215014902 + }, + { + "guid": "googlotim@gmail.com", + "prefs": [], + "schema": 1483389810787, + "blockID": "i1492", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1328594", + "who": "All users who have Savogram version 1.3.2 installed. Version 1.3.1 doesn't have this problem and can be installed from the add-on page. Note that this is an older version, so affected users won't be automatically updated to it. New versions should correct this problem if they become available.", + "why": "Version 1.3.2 of this add-on loads remote code and performs DOM injection in an unsafe manner.", + "name": "Savogram 1.3.2", + "created": "2017-01-05T19:58:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.3.2", + "minVersion": "1.3.2", + "targetApplication": [] + } + ], + "id": "0756ed76-7bc7-ec1e-aba5-3a9fac2107ba", + "last_modified": 1483646608603 + }, + { + "guid": "support@update-firefox.com", + "prefs": [], + "schema": 1483387107003, + "blockID": "i21", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=629717", + "who": "All users of the add-on in all Mozilla applications.", + "why": "This add-on is adware/spyware masquerading as a Firefox update mechanism.", + "name": "Browser Update (spyware)", + "created": "2011-01-31T16:23:48Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "dfb06be8-3594-28e4-d163-17e27119f15d", + "last_modified": 1483389809169 + }, + { + "guid": "{2224e955-00e9-4613-a844-ce69fccaae91}", + "prefs": [], + "schema": 1483387107003, + "blockID": "i7", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=512406", + "who": "All users of Internet Saving Optimizer for all Mozilla applications.", + "why": "This add-on causes a high volume of Firefox crashes and is considered malware.", + "name": "Internet Saving Optimizer (extension)", + "created": "2011-03-31T16:28:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b9efb796-97c2-6434-d28f-acc83436f8e5", + "last_modified": 1483389809147 + }, + { + "guid": "supportaccessplugin@gmail.com", + "prefs": [], + "schema": 1483387107003, + "blockID": "i43", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=693673", + "who": "All users with Firefox Access Plugin installed", + "why": "This add-on is spyware that reports all visited websites to a third party with no user value.", + "name": "Firefox Access Plugin (spyware)", + "created": "2011-10-11T11:24:05Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1ed230a4-e174-262a-55ab-0c33f93a2529", + "last_modified": 1483389809124 + }, + { + "guid": "{8CE11043-9A15-4207-A565-0C94C42D590D}", + "prefs": [], + "schema": 1483387107003, + "blockID": "i10", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=541302", + "who": "All users of this add-on in all Mozilla applications.", + "why": "This add-on secretly hijacks all search results in most major search engines and masks as a security add-on.", + "name": "Internal security options editor (malware)", + "created": "2011-03-31T16:28:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e2e0ac09-6d68-75f5-2424-140f51904876", + "last_modified": 1483389809102 + }, + { + "guid": "admin@youtubespeedup.com", + "prefs": [], + "schema": 1483387107003, + "blockID": "i48", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=714221", + "who": "All users with any version of Youtube Speed UP! installed on any Mozilla product.", + "why": "This add-on hijacks your Facebook account.", + "name": "Youtube Speed UP! (malware)", + "created": "2011-12-29T19:48:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a93922c4-8a8a-5230-8f76-76fecb0653b6", + "last_modified": 1483389809057 + }, + { + "guid": "pink@rosaplugin.info", + "prefs": [], + "schema": 1482945809444, + "blockID": "i84", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=743484", + "who": "All Firefox users who have this add-on installed", + "why": "Add-on acts like malware and performs user actions on Facebook without their consent.", + "name": "Facebook Rosa (malware)", + "created": "2012-04-09T10:13:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "66ad8de9-311d-076c-7356-87fde6d30d8f", + "last_modified": 1482945810971 + }, + { + "guid": "videoplugin@player.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i90", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=752483", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on is malware disguised as a Flash Player update. It can hijack Google searches and Facebook accounts.", + "name": "FlashPlayer 11 (malware)", + "created": "2012-05-07T08:58:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d25943f1-39ef-b9ec-ab77-baeef3498365", + "last_modified": 1482945810949 + }, + { + "guid": "youtb3@youtb3.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i60", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=723753", + "who": "All Firefox users who have this extension installed.", + "why": "Malicious extension installed under false pretenses.", + "name": "Video extension (malware)", + "created": "2012-02-02T16:38:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "cae3093f-a7b3-5352-a264-01dbfbf347ce", + "last_modified": 1482945810927 + }, + { + "guid": "{8f42fb8b-b6f6-45de-81c0-d6d39f54f971}", + "prefs": [], + "schema": 1482945809444, + "blockID": "i82", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=743012", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on maliciously manipulates Facebook and is installed under false pretenses.", + "name": "Face Plus (malware)", + "created": "2012-04-09T10:04:28Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "09319ab3-55e7-fec1-44e0-84067d014b9b", + "last_modified": 1482945810904 + }, + { + "guid": "{95ff02bc-ffc6-45f0-a5c8-619b8226a9de}", + "prefs": [], + "schema": 1482945809444, + "blockID": "i105", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=763065", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that inserts scripts into Facebook and hijacks the user's session.\r\n", + "name": "Eklenti Dünyası (malware)", + "created": "2012-06-08T14:34:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "afbbc08d-2414-f51e-fdb8-74c0a2d90323", + "last_modified": 1482945810858 + }, + { + "guid": "{fa277cfc-1d75-4949-a1f9-4ac8e41b2dfd}", + "prefs": [], + "schema": 1482945809444, + "blockID": "i77", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=738419", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on is malware that is installed under false pretenses as an Adobe plugin.", + "name": "Adobe Flash (malware)", + "created": "2012-03-22T14:39:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "81753a93-382d-5f9d-a4ca-8a21b679ebb1", + "last_modified": 1482945810835 + }, + { + "guid": "youtube@youtube3.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i57", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=722823", + "who": "All Firefox users that have installed this add-on.", + "why": "Malware installed on false pretenses.", + "name": "Divx 2012 Plugin (malware)", + "created": "2012-01-31T13:54:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4a93a0eb-a513-7272-6199-bc4d6228ff50", + "last_modified": 1482945810811 + }, + { + "guid": "{392e123b-b691-4a5e-b52f-c4c1027e749c}", + "prefs": [], + "schema": 1482945809444, + "blockID": "i109", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=769781", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on pretends to be developed by Facebook and injects scripts that manipulate users' Facebook accounts.", + "name": "Zaman Tuneline Hayır! (malware)", + "created": "2012-06-29T13:20:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b9a805aa-cae7-58d6-5a53-2af4442e4cf6", + "last_modified": 1482945810788 + }, + { + "guid": "msntoolbar@msn.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i18", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=599971", + "who": "Users of Bing Bar 6.0 and older for all versions of Firefox.", + "why": "This add-on has security issues and was blocked at Microsoft's request. For more information, please see this article.", + "name": "Bing Bar", + "created": "2011-03-31T16:28:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "6.*", + "minVersion": " 0", + "targetApplication": [] + } + ], + "id": "9b2f2039-b997-8993-d6dc-d881bc1ca7a1", + "last_modified": 1482945810764 + }, + { + "guid": "yasd@youasdr3.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i104", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=763065", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that inserts scripts into Facebook and hijacks the user's session.\r\n", + "name": "Play Now (malware)", + "created": "2012-06-08T14:33:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8a352dff-d09d-1e78-7feb-45dec7ace5a5", + "last_modified": 1482945810740 + }, + { + "guid": "fdm_ffext@freedownloadmanager.org", + "prefs": [], + "schema": 1482945809444, + "blockID": "i2", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=408445", + "who": "Users of Firefox 3 and later with versions 1.0 through 1.3.1 of Free Download Manager", + "why": "This add-on causes a high volume of crashes.", + "name": "Free Download Manager", + "created": "2011-03-31T16:28:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.3.1", + "minVersion": "1.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "3.0a1" + } + ] + } + ], + "id": "fc46f8e7-0489-b90f-a373-d93109479ca5", + "last_modified": 1482945810393 + }, + { + "guid": "flash@adobe.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i56", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=722526", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on poses as an Adobe Flash update and injects malicious scripts into web pages. It hides itself in the Add-ons Manager.", + "name": "Adobe Flash Update (malware)", + "created": "2012-01-30T15:41:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "696db959-fb0b-8aa4-928e-65f157cdd77a", + "last_modified": 1482945810371 + }, + { + "guid": "youtubeer@youtuber.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i66", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=726787", + "who": "All Firefox users who have installed this add-on.", + "why": "Add-on behaves maliciously, and is installed under false pretenses.", + "name": "Plug VDS (malware)", + "created": "2012-02-13T15:44:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0878ce4e-b476-ffa3-0e06-21a65b7917a1", + "last_modified": 1482945810348 + }, + { + "guid": "flash@adobee.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i83", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=743497", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware installed under false pretenses.", + "name": "FlashPlayer 11 (malware)", + "created": "2012-04-09T10:08:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "09bb4661-331c-f7ba-865b-9e085dc437af", + "last_modified": 1482945810259 + }, + { + "guid": "youtube@2youtube.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i71", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=730399", + "who": "All Firefox users who have installed this add-on.", + "why": "Extension is malware, installed under false pretenses.", + "name": "YouTube extension (malware)", + "created": "2012-02-27T10:23:23Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5d389c1f-b3a0-b06f-6ffb-d1e8aa055e3c", + "last_modified": 1482945810236 + }, + { + "guid": "webmaster@buzzzzvideos.info", + "prefs": [], + "schema": 1482945809444, + "blockID": "i58", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=722844", + "who": "All Firefox users who have installed this add-on.", + "why": "Malware add-on that is installed under false pretenses.", + "name": "Buzz Video (malware)", + "created": "2012-01-31T14:51:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f7aab105-e2c2-42f5-d9be-280eb9c0c8f7", + "last_modified": 1482945810213 + }, + { + "guid": "play5@vide04flash.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i92", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=755443", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on impersonates a Flash Player update (poorly), and inserts malicious scripts into Facebook.", + "name": "Lastest Flash PLayer (malware)", + "created": "2012-05-15T13:27:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "7190860e-fc1f-cd9f-5d25-778e1e9043b2", + "last_modified": 1482945810191 + }, + { + "guid": "support3_en@adobe122.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i97", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=759164", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on is malware disguised as the Flash Player plugin.", + "name": "FlashPlayer 11 (malware)", + "created": "2012-05-28T13:42:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "decf93a1-2bb0-148c-a1a6-10b3757b554b", + "last_modified": 1482945810168 + }, + { + "guid": "a1g0a9g219d@a1.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i73", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=736275", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on is malware disguised as Flash Player. It steals user cookies and sends them to a remote location.", + "name": "Flash Player (malware)", + "created": "2012-03-15T15:03:04Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6dd66b43-897d-874a-2227-54e240b8520f", + "last_modified": 1482945810146 + }, + { + "guid": "ghostviewer@youtube2.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i59", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=723683", + "who": "All Firefox users who have installed this add-on.", + "why": "Malicious add-on that automatically posts to Facebook.", + "name": "Ghost Viewer (malware)", + "created": "2012-02-02T16:32:15Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "06dfe833-8c3d-90ee-3aa8-37c3c28f7c56", + "last_modified": 1482945810123 + }, + { + "guid": "kdrgun@gmail.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i103", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=763065", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that inserts scripts into Facebook and hijacks the user's session.", + "name": "Timeline Kapat (malware)", + "created": "2012-06-08T14:32:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a9a46ab2-2f56-1046-201c-5faa3435e248", + "last_modified": 1482945810078 + }, + { + "guid": "youtube2@youtube2.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i67", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=728476", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on is malware, installed under false pretenses.", + "name": "Youtube Online (malware)", + "created": "2012-02-18T09:10:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "14650ece-295b-a667-f9bc-a3d973e2228c", + "last_modified": 1482945810055 + }, + { + "guid": "admin@youtubeplayer.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i51", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=717165", + "who": "All Firefox users with this extension installed.", + "why": "This add-on is malware, doing nothing more than inserting advertisements into websites through iframes.", + "name": "Youtube player (malware)", + "created": "2012-01-18T14:34:55Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "16b2ce94-88db-0d79-33fc-a93070ceb509", + "last_modified": 1482945809957 + }, + { + "guid": "youtubeee@youtuber3.com", + "prefs": [], + "schema": 1482945809444, + "blockID": "i96", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=758503", + "who": "All Firefox users who have installed this add-on.", + "why": "This is a malicious add-on that is disguised as a DivX plugin.", + "name": "Divx 2012 Plugins (malware)", + "created": "2012-05-25T09:26:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f01be9cb-5cf2-774a-a4d7-e210a24db5b9", + "last_modified": 1482945809912 + }, + { + "guid": "{3252b9ae-c69a-4eaf-9502-dc9c1f6c009e}", + "prefs": [], + "schema": 1482945809444, + "blockID": "i17", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=599971", + "who": "Users of version 2.2 of this add-on in all versions of Firefox.", + "why": "This add-on has security issues and was blocked at Microsoft's request. For more information, please see this article.", + "name": "Default Manager (Microsoft)", + "created": "2011-03-31T16:28:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "2.2", + "minVersion": "2.2", + "targetApplication": [] + } + ], + "id": "38be28ac-2e30-37fa-4332-852a55fafb43", + "last_modified": 1482945809886 + }, + { + "guid": "{68b8676b-99a5-46d1-b390-22411d8bcd61}", + "prefs": [], + "schema": 1482945809444, + "blockID": "i93", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=755635", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that post content on Facebook accounts and steals user data.", + "name": "Zaman Tünelini Kaldır! (malware)", + "created": "2012-05-16T10:44:42Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "733aff15-9b1f-ec04-288f-b78a55165a1c", + "last_modified": 1482945809863 + }, + { + "guid": "applebeegifts@mozilla.doslash.org", + "prefs": [], + "schema": 1482945809444, + "blockID": "i54", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=721562", + "who": "All Firefox users that install this add-on.", + "why": "Add-on is malware installed under false pretenses.", + "name": "Applebees Gift Card (malware)", + "created": "2012-01-26T16:17:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1372c8ab-5452-745a-461a-aa78e3e12c4b", + "last_modified": 1482945809840 + }, + { + "guid": "activity@facebook.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i65", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=726803", + "who": "All Firefox users who have installed this add-on.", + "why": "Add-on behaves maliciously and poses as an official Facebook add-on.", + "name": "Facebook extension (malware)", + "created": "2012-02-13T15:41:02Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "79ad1c9b-0828-7823-4574-dd1cdd46c3d6", + "last_modified": 1482945809437 + }, + { + "guid": "jid0-EcdqvFOgWLKHNJPuqAnawlykCGZ@jetpack", + "prefs": [], + "schema": 1482945112982, + "blockID": "i62", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=724650", + "who": "All Firefox users who have installed this add-on.", + "why": "Add-on is installed under false pretenses and delivers malware.", + "name": "YouTube extension (malware)", + "created": "2012-02-06T14:46:33Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5ae1e642-b53c-54c0-19e7-5562cfdac3a3", + "last_modified": 1482945809415 + }, + { + "guid": "{B7082FAA-CB62-4872-9106-E42DD88EDE45}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i25", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=637542", + "who": "Users of McAfee SiteAdvisor below version 3.3.1 for Firefox 4.\r\n\r\nUsers of McAfee SiteAdvisor 3.3.1 and below for Firefox 5 and higher.", + "why": "This add-on causes a high volume of crashes and is incompatible with certain versions of Firefox.", + "name": "McAfee SiteAdvisor", + "created": "2011-03-14T15:53:07Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.3.0.*", + "minVersion": "0.1", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "3.7a1" + } + ] + } + ], + "id": "c950501b-1f08-2ab2-d817-7c664c0d16fe", + "last_modified": 1482945809393 + }, + { + "guid": "{B7082FAA-CB62-4872-9106-E42DD88EDE45}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i38", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=660111", + "who": "Users of McAfee SiteAdvisor below version 3.3.1 for Firefox 4.\r\n\r\nUsers of McAfee SiteAdvisor 3.3.1 and below for Firefox 5 and higher.", + "why": "This add-on causes a high volume of crashes and is incompatible with certain versions of Firefox.", + "name": "McAfee SiteAdvisor", + "created": "2011-05-27T13:55:02Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "3.3.1", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "5.0a1" + } + ] + } + ], + "id": "f11de388-4511-8d06-1414-95d3b2b122c5", + "last_modified": 1482945809371 + }, + { + "guid": "youtube@youtube7.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i55", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=721646", + "who": "All Firefox users with this add-on installed.", + "why": "This is malware posing as video software.", + "name": "Plugin Video (malware)", + "created": "2012-01-27T09:39:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "08ceedf5-c7c1-f54f-db0c-02f01f0e319a", + "last_modified": 1482945809304 + }, + { + "guid": "crossriderapp3924@crossrider.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i76", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=738282", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on compromises Facebook privacy and security and spams friends lists without user intervention.", + "name": "Fblixx (malware)", + "created": "2012-03-22T10:38:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "39d0a019-62fb-837b-1f1f-6831e56442b5", + "last_modified": 1482945809279 + }, + { + "guid": "{45147e67-4020-47e2-8f7a-55464fb535aa}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i86", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=748993", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on injects scripts into Facebook and performs malicious activity.", + "name": "Mukemmel Face+", + "created": "2012-04-25T16:33:21Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "960443f9-cf48-0b71-1ff2-b8c34a3411ea", + "last_modified": 1482945809255 + }, + { + "guid": "{4B3803EA-5230-4DC3-A7FC-33638F3D3542}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i4", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=441649", + "who": "Users of Firefox 3 and later with version 1.2 of Crawler Toolbar", + "why": "This add-on causes a high volume of crashes.", + "name": "Crawler Toolbar", + "created": "2008-07-08T10:23:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.2", + "minVersion": "1.2", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "3.0a1" + } + ] + } + ], + "id": "a9818d53-3a6a-8673-04dd-2a16f5644215", + "last_modified": 1482945809232 + }, + { + "guid": "flashupdate@adobe.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i68", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=722526", + "who": "All Firefox users who have this add-on installed.", + "why": "Add-on is malware, installed under false pretenses.", + "name": "Flash Update (malware)", + "created": "2012-02-21T13:55:10Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1ba5b46e-790d-5af2-9580-a5f1e6e65522", + "last_modified": 1482945809208 + }, + { + "guid": "plugin@youtubeplayer.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i127", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=783356", + "who": "All users who have this add-on installed.", + "why": "This add-on tries to pass as a YouTube player and runs malicious scripts on webpages.", + "name": "Youtube Facebook Player (malware)", + "created": "2012-08-16T13:03:10Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "17a8bece-e2df-a55d-8a72-95faff028b83", + "last_modified": 1482945809185 + }, + { + "guid": "GifBlock@facebook.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i79", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=739482", + "who": "All Firefox users who have installed this extension.", + "why": "This extension is malicious and is installed under false pretenses.", + "name": "Facebook Essentials (malware)", + "created": "2012-03-27T10:53:33Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "728451e8-1273-d887-37e9-5712b1cc3bff", + "last_modified": 1482945809162 + }, + { + "guid": "ff-ext@youtube", + "prefs": [], + "schema": 1482945112982, + "blockID": "i52", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=719296", + "who": "All Firefox users that have this add-on installed.", + "why": "This add-on poses as a YouTube player while posting spam into Facebook account.", + "name": "Youtube player (malware)", + "created": "2012-01-19T08:26:35Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "cd2dd72a-dd52-6752-a0cd-a4b312fd0b65", + "last_modified": 1482945809138 + }, + { + "guid": "ShopperReports@ShopperReports.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i22", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=630191", + "who": "Users of Shopper Reports version 3.1.22.0 in Firefox 4 and later.", + "why": "This add-on causes a high volume of Firefox crashes.", + "name": "Shopper Reports", + "created": "2011-02-09T17:03:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.1.22.0", + "minVersion": "3.1.22.0", + "targetApplication": [] + } + ], + "id": "f26b049c-d856-750f-f050-996e6bec7cbb", + "last_modified": 1482945809115 + }, + { + "guid": "{27182e60-b5f3-411c-b545-b44205977502}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i16", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=599971", + "who": "Users of version 1.0 of this add-on in all versions of Firefox.", + "why": "This add-on has security issues and was blocked at Microsoft's request. For more information, please see this article.", + "name": "Search Helper Extension (Microsoft)", + "created": "2011-03-31T16:28:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.0", + "minVersion": "1.0", + "targetApplication": [] + } + ], + "id": "2655f230-11f3-fe4c-7c3d-757d37d5f9a5", + "last_modified": 1482945809092 + }, + { + "guid": "{841468a1-d7f4-4bd3-84e6-bb0f13a06c64}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i46", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=712369", + "who": "Users of all versions of Nectar Search Toolbar in Firefox 9.", + "why": "This add-on causes crashes and other stability issues in Firefox.", + "name": "Nectar Search Toolbar", + "created": "2011-12-20T11:38:17Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0.1", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "9.0", + "minVersion": "9.0a1" + } + ] + } + ], + "id": "b660dabd-0dc0-a55c-4b86-416080b345d9", + "last_modified": 1482945809069 + }, + { + "guid": "support@daemon-tools.cc", + "prefs": [], + "schema": 1482945112982, + "blockID": "i5", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=459850", + "who": "Users of Daemon Tools Toolbar version 1.0.0.5 and older for all Mozilla applications.", + "why": "This add-on causes a high volume of crashes.", + "name": "Daemon Tools Toolbar", + "created": "2009-02-13T18:39:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.0.0.5", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8cabafd3-576a-b487-31c8-ab59e0349a0e", + "last_modified": 1482945809045 + }, + { + "guid": "{a3a5c777-f583-4fef-9380-ab4add1bc2a8}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i53", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=719605", + "who": "All users of Firefox with this add-on installed.", + "why": "This add-on is being offered as an online movie viewer, when it reality it only inserts scripts and ads into known sites.", + "name": "Peliculas-FLV (malware)", + "created": "2012-01-19T15:58:10Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "2.0.3", + "minVersion": "2.0.3", + "targetApplication": [] + } + ], + "id": "07bc0962-60da-087b-c3ab-f2a6ab84d81c", + "last_modified": 1482945809021 + }, + { + "guid": "royal@facebook.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i64", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=725777", + "who": "All Firefox users who have installed this add-on.", + "why": "Malicious add-on posing as a Facebook tool.", + "name": "Facebook ! (malware)", + "created": "2012-02-09T13:24:23Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "dd1d2623-0d15-c93e-8fbd-ba07b0299a44", + "last_modified": 1482945808997 + }, + { + "guid": "{28bfb930-7620-11e1-b0c4-0800200c9a66}", + "prefs": [], + "schema": 1482945112982, + "blockID": "i108", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=766852", + "who": "All Firefox user who have this add-on installed.", + "why": "This is malware disguised as an Adobe product. It spams Facebook pages.", + "name": "Aplicativo (malware)", + "created": "2012-06-21T09:24:11Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "908dc4fb-ebc9-cea1-438f-55e4507ba834", + "last_modified": 1482945808973 + }, + { + "guid": "socialnetworktools@mozilla.doslash.org", + "prefs": [], + "schema": 1482945112982, + "blockID": "i78", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=739441", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on hijacks the Facebook UI and adds scripts to track users.", + "name": "Social Network Tools (malware)", + "created": "2012-03-26T16:46:55Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1064cd25-3b87-64bb-b0a6-2518ad281574", + "last_modified": 1482945808950 + }, + { + "guid": "youtubeeing@youtuberie.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i98", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=759663", + "who": "All Firefox users who have installed this add-on.", + "why": "This add-on is malware disguised as a Youtube add-on.", + "name": "Youtube Video Player (malware)", + "created": "2012-05-30T09:30:14Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3484f860-56e1-28e8-5a70-cdcd5ab9d6ee", + "last_modified": 1482945808927 + }, + { + "guid": "pfzPXmnzQRXX6@2iABkVe.com", + "prefs": [], + "schema": 1482945112982, + "blockID": "i99", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=759950", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware disguised as a Flash Player update.", + "name": "Flash Player (malware)", + "created": "2012-05-30T17:10:18Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "29cc4abc-4f52-01f1-eb0b-cad84ba4db13", + "last_modified": 1482945808881 + }, + { + "guid": "/^(pdftoword@addingapps.com|jid0-EYTXLS0GyfQME5irGbnD4HksnbQ@jetpack|jid1-ZjJ7t75BAcbGCX@jetpack)$/", + "prefs": [], + "schema": 1482341309012, + "blockID": "i1425", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1325060", + "who": "All users who have any of the affected versions installed.", + "why": "A security vulnerability was discovered in old versions of the Add-ons SDK, which is exposed by certain old versions of add-ons. In the case of some add-ons that haven't been updated for a long time, all versions are being blocked.", + "name": "Various vulnerable add-on versions", + "created": "2016-12-21T17:23:14Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "150e639f-c832-63d0-a775-59313b2e1bf9", + "last_modified": 1482343886365 + }, + { + "guid": "{cc8f597b-0765-404e-a575-82aefbd81daf}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i380", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=866332", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts and performs unwanted actions on behalf of the user.", + "name": "Update My Browser (malware)", + "created": "2013-06-19T13:03:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4950d7aa-c602-15f5-a7a2-d844182d5cbd", + "last_modified": 1480349217152 + }, + { + "guid": "extension@FastFreeConverter.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i470", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935779", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is part of a malicious Firefox installer bundle.", + "name": "Installer bundle (malware)", + "created": "2013-11-07T15:38:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "649dd933-debf-69b7-020f-496c2c9f99c8", + "last_modified": 1480349217071 + }, + { + "guid": "59D317DB041748fdB89B47E6F96058F3@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i694", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1053540", + "who": "All Firefox users who have this add-ons installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This is a suspicious add-on that appears to be installed without user consent, in violation of the Add-on Guidelines.", + "name": "JsInjectExtension", + "created": "2014-08-21T13:46:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "75692bd4-18e5-a9be-7ec3-9327e159ef68", + "last_modified": 1480349217005 + }, + { + "guid": "/^({bfec236d-e122-4102-864f-f5f19d897f5e}|{3f842035-47f4-4f10-846b-6199b07f09b8}|{92ed4bbd-83f2-4c70-bb4e-f8d3716143fe})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i527", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949566", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted and uses multiple IDs.", + "name": "KeyBar add-on", + "created": "2013-12-20T14:13:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6d68dd97-7965-0a84-8ca7-435aac3c8040", + "last_modified": 1480349216927 + }, + { + "guid": "support@vide1flash2.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i246", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=830159", + "who": "All Firefox users who have this add-on installed.", + "why": "This is an add-on that poses as the Adobe Flash Player and runs malicious code in the user's system.", + "name": "Lastest Adobe Flash Player (malware)", + "created": "2013-01-14T09:17:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2004fba1-74bf-a072-2a59-6e0ba827b541", + "last_modified": 1480349216871 + }, + { + "guid": "extension21804@extension21804.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i312", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=835665", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines, bypassing our third party install opt-in screen. Users who wish to continue using this extension can enable it in the Add-ons Manager.", + "name": "Coupon Companion", + "created": "2013-03-06T14:14:05Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b2cf1256-dadd-6501-1f4e-25902d408692", + "last_modified": 1480349216827 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i602", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:18:05Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.8.*", + "minVersion": "3.15.8", + "targetApplication": [] + } + ], + "id": "b2b4236d-5d4d-82b2-99cd-00ff688badf1", + "last_modified": 1480349216765 + }, + { + "guid": "{FE1DEEEA-DB6D-44b8-83F0-34FC0F9D1052}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i364", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=867670", + "who": "All Firefox users who have this add-on installed. Users who want to enable the add-on again can do so in the Add-ons Manager.", + "why": "This add-on is side-installed with other software, and blocks setting reversions attempted by users who want to recover their settings after they are hijacked by other add-ons.", + "name": "IB Updater", + "created": "2013-06-10T16:14:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a59b967c-66ca-7ad9-2dc6-d0ad37ded5fd", + "last_modified": 1480349216652 + }, + { + "guid": "vpyekkifgv@vpyekkifgv.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i352", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=872211", + "who": "All Firefox users who have this add-on installed.", + "why": "Uses a deceptive name and injects ads into pages without user consent.", + "name": "SQLlite Addon (malware)", + "created": "2013-05-14T13:42:04Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8fd981ab-7ee0-e367-d804-0efe29d63178", + "last_modified": 1480349216614 + }, + { + "guid": "thefoxonlybetter@quicksaver", + "prefs": [], + "schema": 1480349193877, + "blockID": "i702", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1053469", + "who": "All Firefox users who have any of these versions of the add-on installed.", + "why": "Certain versions of The Fox, Only Better weren't developed by the original developer, and are likely malicious in nature. This violates the Add-on Guidelines for reusing an already existent ID.", + "name": "The Fox, Only Better (malicious versions)", + "created": "2014-08-27T10:05:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "1.10", + "targetApplication": [] + } + ], + "id": "60e54f6a-1b10-f889-837f-60a76a98fccc", + "last_modified": 1480349216512 + }, + { + "guid": "{f0e59437-6148-4a98-b0a6-60d557ef57f4}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i304", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=845975", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our installation guidelines and is dropped silently into user's profiles.", + "name": "WhiteSmoke B", + "created": "2013-02-27T13:10:18Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0469e643-1a90-f9be-4aad-b347469adcbe", + "last_modified": 1480349216402 + }, + { + "guid": "xz123@ya456.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i486", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=939254", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on appears to be malware and is installed silently in violation of the Add-on Guidelines.", + "name": "BetterSurf (malware)", + "created": "2013-11-15T13:34:53Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b9825a25-a96c-407e-e656-46a7948e5745", + "last_modified": 1480349215808 + }, + { + "guid": "{C7AE725D-FA5C-4027-BB4C-787EF9F8248A}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i424", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=860641", + "who": "Users of Firefox 23 or later who have RelevantKnowledge 1.0.0.2 or lower.", + "why": "Old versions of this add-on are causing startup crashes in Firefox 23, currently on the Beta channel. RelevantKnowledge users on Firefox 23 and above should update to version 1.0.0.3 of the add-on.", + "name": "RelevantKnowledge 1.0.0.2 and lower", + "created": "2013-07-01T10:45:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.0.0.2", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "23.0a1" + } + ] + } + ], + "id": "c888d167-7970-4b3f-240f-2d8e6f14ded4", + "last_modified": 1480349215779 + }, + { + "guid": "superlrcs@svenyor.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i545", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949596", + "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enable it in the Add-ons Manager.", + "why": "This add-on is in violation of the Add-on Guidelines, using multiple add-on IDs and potentially doing other unwanted activities.", + "name": "SuperLyrics", + "created": "2014-01-30T11:52:42Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "002cd4fa-4c2b-e28b-9220-4a520f4d9ec6", + "last_modified": 1480349215672 + }, + { + "guid": "mbrsepone@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i479", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=937331", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks Facebook accounts.", + "name": "Mozilla Lightweight Pack (malware)", + "created": "2013-11-11T15:42:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0549645e-5f50-5089-1f24-6e7d3bfab8e0", + "last_modified": 1480349215645 + }, + { + "guid": "mbroctone@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i476", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=936590", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks the users' Facebook account.", + "name": "Mozilla Storage Service (malware)", + "created": "2013-11-08T15:32:13Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "92198396-8756-8d09-7f18-a68d29894f71", + "last_modified": 1480349215504 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i616", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:24:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.28.*", + "minVersion": "3.15.28", + "targetApplication": [] + } + ], + "id": "f11b485f-320e-233c-958b-a63377024fad", + "last_modified": 1480349215479 + }, + { + "guid": "/^({e9df9360-97f8-4690-afe6-996c80790da4}|{687578b9-7132-4a7a-80e4-30ee31099e03}|{46a3135d-3683-48cf-b94c-82655cbc0e8a}|{49c795c2-604a-4d18-aeb1-b3eba27e5ea2}|{7473b6bd-4691-4744-a82b-7854eb3d70b6}|{96f454ea-9d38-474f-b504-56193e00c1a5})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i494", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=776404", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on changes search settings without user interaction, and fails to reset them after it is removed. It also uses multiple add-on IDs for no apparent reason. This violates our Add-on Guidelines.", + "name": "uTorrent and related", + "created": "2013-12-02T14:52:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "485210d0-8e69-3436-536f-5d1deeea4167", + "last_modified": 1480349215454 + }, + { + "guid": "{EB7508CA-C7B2-46E0-8C04-3E94A035BD49}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i162", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=799266", + "who": "All Firefox users who have installed any of these add-ons.", + "why": "This block covers a number of malicious add-ons that deceive users, using names like \"Mozilla Safe Browsing\" and \"Translate This!\", and claiming they are developed by \"Mozilla Corp.\". They hijack searches and redirects users to pages they didn't intend to go to.\r\n\r\nNote: this block won't be active until bug 799266 is fixed.", + "name": "Mozilla Safe Browsing and others (Medfos malware)", + "created": "2012-10-11T12:25:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "07566aa3-4ff9-ac4f-9de9-71c77454b4da", + "last_modified": 1480349215428 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i614", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:23:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.26.*", + "minVersion": "3.15.26", + "targetApplication": [] + } + ], + "id": "ede541f3-1748-7b33-9bd6-80e2f948e14f", + "last_modified": 1480349215399 + }, + { + "guid": "/^({976cd962-e0ca-4337-aea7-d93fae63a79c}|{525ba996-1ce4-4677-91c5-9fc4ead2d245}|{91659dab-9117-42d1-a09f-13ec28037717}|{c1211069-1163-4ba8-b8b3-32fc724766be})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i522", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947485", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by being silently installed and using multiple add-on IDs.", + "name": "appbario7", + "created": "2013-12-20T13:15:33Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "580aed26-dc3b-eef8-fa66-a0a402447b7b", + "last_modified": 1480349215360 + }, + { + "guid": "jid0-O6MIff3eO5dIGf5Tcv8RsJDKxrs@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i552", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=974041", + "who": "All Firefox users who have this extension installed.", + "why": "This extension is malware that attempts to make it impossible for a second extension and itself to be disabled, and also forces the new tab page to have a specific URL.", + "name": "Extension_Protected (malware)", + "created": "2014-02-19T15:26:37Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e53063b4-5702-5b66-c860-d368cba4ccb6", + "last_modified": 1480349215327 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i604", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:18:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.11.*", + "minVersion": "3.15.10", + "targetApplication": [] + } + ], + "id": "b910f779-f36e-70e1-b17a-8afb75988c03", + "last_modified": 1480349215302 + }, + { + "guid": "brasilescapefive@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i483", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=938473", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks Facebook accounts.", + "name": "Facebook Video Pack (malware)", + "created": "2013-11-14T09:37:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "85ee7840-f262-ad30-eb91-74b3248fd13d", + "last_modified": 1480349215276 + }, + { + "guid": "brasilescapeeight@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i482", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=938476", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks Facebook accounts.", + "name": "Mozilla Security Pack (malware)", + "created": "2013-11-14T09:36:55Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "457a5722-be90-5a9f-5fa0-4c753e9f324c", + "last_modified": 1480349215249 + }, + { + "guid": "happylyrics@hpyproductions.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i370", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=881815", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into Firefox without the users' consent, violating our Add-on Guidelines.", + "name": "Happy Lyrics", + "created": "2013-06-11T15:42:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "730e616d-94a7-df0c-d31a-98b7875d60c2", + "last_modified": 1480349215225 + }, + { + "guid": "search-snacks@search-snacks.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i872", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1082733", + "who": "All users who have this add-on installed. Users who wish to continue using the add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of our Add-on Guidelines.", + "name": "Search Snacks", + "created": "2015-03-04T14:37:33Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "7567b06f-98fb-9400-8007-5d0357c345d9", + "last_modified": 1480349215198 + }, + { + "os": "WINNT", + "guid": "{ABDE892B-13A8-4d1b-88E6-365A6E755758}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i107", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=764210", + "who": "All Firefox users on Windows who have the RealPlayer Browser Record extension installed.", + "why": "The RealPlayer Browser Record extension is causing significant problems on Flash video sites like YouTube. This block automatically disables the add-on, but users can re-enable it from the Add-ons Manager if necessary.\r\n\r\nThis block shouldn't disable any other RealPlayer plugins, so watching RealPlayer content on the web should be unaffected.\r\n\r\nIf you still have problems playing videos on YouTube or elsewhere, please visit our support site for help.", + "name": "RealPlayer Browser Record Plugin", + "created": "2012-06-14T13:54:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "15.0.5", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e3b89e55-b35f-8694-6f0e-f856e57a191d", + "last_modified": 1480349215173 + }, + { + "guid": "amo-validator-bypass@example.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1058", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1227605", + "who": "All users who install this add-on.", + "why": "This add-on is a proof of concept of a malicious add-on that bypasses the code validator.", + "name": " AMO Validator Bypass", + "created": "2015-11-24T09:03:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "86e38e3e-a729-b5a2-20a8-4738b376eea6", + "last_modified": 1480349214743 + }, + { + "guid": "6lIy@T.edu", + "prefs": [], + "schema": 1480349193877, + "blockID": "i852", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128269", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and performs unwanted actions, in violation of the Add-on Guidelines.", + "name": "unIsaless", + "created": "2015-02-09T15:30:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "39798bc2-9c75-f172-148b-13f3ca1dde9b", + "last_modified": 1480349214613 + }, + { + "guid": "{a7f2cb14-0472-42a1-915a-8adca2280a2c}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i686", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1033809", + "who": "All users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-on Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "HomeTab", + "created": "2014-08-06T16:35:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "33a8f403-b2c8-cadf-e1ba-40b39edeaf18", + "last_modified": 1480349214537 + }, + { + "guid": "{CA8C84C6-3918-41b1-BE77-049B2BDD887C}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i862", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1131230", + "who": "All users who have this add-on installed. Users who wish to continue using the add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of our Add-on Guidelines.", + "name": "Ebay Shopping Assistant by Spigot", + "created": "2015-02-26T12:51:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9a9d6da2-90a1-5b71-8b24-96492d57dfd1", + "last_modified": 1480349214479 + }, + { + "guid": "update@firefox.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i374", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=781088", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious extension that hijacks Facebook accounts.", + "name": "Premium Update (malware)", + "created": "2013-06-18T13:58:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "bb388413-60ea-c9d6-9a3b-c90df950c319", + "last_modified": 1480349214427 + }, + { + "guid": "sqlmoz@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i350", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=871610", + "who": "All Firefox users who have this extension installed.", + "why": "This extension is malware posing as Mozilla software. It hijacks Facebook accounts and spams other Facebook users.", + "name": "Mozilla Service Pack (malware)", + "created": "2013-05-13T09:43:07Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "715082e8-7a30-b27b-51aa-186c38e078f6", + "last_modified": 1480349214360 + }, + { + "guid": "mozillahmpg@mozilla.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i140", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=791867", + "who": "All Firefox users who have installed this add-on.", + "why": "This is a malicious add-on that tries to monetize on its users by embedding unauthorized affiliate codes on shopping websites, and sometimes redirecting users to alternate sites that could be malicious in nature.", + "name": "Google YouTube HD Player (malware)", + "created": "2012-09-17T16:04:10Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "98150e2e-cb45-1fee-8458-28d3602ec2ec", + "last_modified": 1480349214216 + }, + { + "guid": "astrovia@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i489", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=942699", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks Facebook accounts.", + "name": "Facebook Security Service (malware)", + "created": "2013-11-25T12:40:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6f365ff4-e48f-8a06-d19d-55e19fba81f4", + "last_modified": 1480349214157 + }, + { + "guid": "{bbea93c6-64a3-4a5a-854a-9cc61c8d309e}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1126", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1251940", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that disables various security checks in Firefox.", + "name": "Tab Extension (malware)", + "created": "2016-02-29T21:58:10Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5acb9dcc-59d4-46d1-2a11-1194c4948239", + "last_modified": 1480349214066 + }, + { + "guid": "ffxtlbr@iminent.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i628", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=866943", + "who": "All Firefox users who have any of these add-ons installed. Users who wish to continue using them can enable them in the Add-ons Manager.", + "why": "These add-ons have been silently installed repeatedly, and change settings without user consent, in violation of the Add-on Guidelines.", + "name": "Iminent Minibar", + "created": "2014-06-26T15:47:15Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4387ad94-8500-d74d-68e3-20564a9aac9e", + "last_modified": 1480349214036 + }, + { + "guid": "{28387537-e3f9-4ed7-860c-11e69af4a8a0}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i40", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=665775", + "who": "Users of MediaBar versions 4.3.1.00 and below in all versions of Firefox.", + "why": "This add-on causes a high volume of crashes and is incompatible with certain versions of Firefox.", + "name": "MediaBar (2)", + "created": "2011-07-19T10:19:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "4.3.1.00", + "minVersion": "0.1", + "targetApplication": [] + } + ], + "id": "ff95664b-93e4-aa73-ac20-5ffb7c87d8b7", + "last_modified": 1480349214002 + }, + { + "guid": "{41e5ef7a-171d-4ab5-8351-951c65a29908}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i784", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "HelpSiteExpert", + "created": "2014-11-14T14:37:56Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0c05a0bb-30b4-979e-33a7-9f3955eba17d", + "last_modified": 1480349213962 + }, + { + "guid": "/^({2d7886a0-85bb-4bf2-b684-ba92b4b21d23}|{2fab2e94-d6f9-42de-8839-3510cef6424b}|{c02397f7-75b0-446e-a8fa-6ef70cfbf12b}|{8b337819-d1e8-48d3-8178-168ae8c99c36}|firefox@neurowise.info|firefox@allgenius.info)$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i762", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1082599", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "These add-ons are silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "SaveSense, neurowise, allgenius", + "created": "2014-10-17T16:58:10Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c5439f55-ace5-ad73-1270-017c0ba7b2ce", + "last_modified": 1480349213913 + }, + { + "guid": "{462be121-2b54-4218-bf00-b9bf8135b23f}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i226", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=812303", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently side-installed by other software, and doesn't do much more than changing the users' settings, without reverting them on removal.", + "name": "WhiteSmoke", + "created": "2012-11-29T16:27:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "994c6084-e864-0e4e-ac91-455083ee46c7", + "last_modified": 1480349213879 + }, + { + "guid": "firefox@browsefox.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i546", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=936244", + "who": "All Firefox users who have this add-on installed. If you want to continue using it, it can be enabled in the Add-ons Manager.", + "why": "This add-on is silently installed, in violation of the Add-on Guidelines.", + "name": "BrowseFox", + "created": "2014-01-30T12:26:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "407d8c84-8939-cd28-b284-9b680e529bf6", + "last_modified": 1480349213853 + }, + { + "guid": "{6926c7f7-6006-42d1-b046-eba1b3010315}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i382", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=844956", + "who": "All Firefox users who have this add-on installed. Those who wish to continue using it can enable it again in the Add-ons Manager.", + "why": "This add-on is silently installed, bypassing the Firefox opt-in screen and violating our Add-on Guidelines.", + "name": "appbario7", + "created": "2013-06-25T12:05:34Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2367bd94-2bdd-c615-de89-023ba071a443", + "last_modified": 1480349213825 + }, + { + "guid": "05dd836e-2cbd-4204-9ff3-2f8a8665967d@a8876730-fb0c-4057-a2fc-f9c09d438e81.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i468", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935135", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on appears to be part of a Trojan software package.", + "name": "Trojan.DownLoader9.50268 (malware)", + "created": "2013-11-07T14:43:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2fd53d9b-7096-f1fb-fbcb-2b40a6193894", + "last_modified": 1480349213774 + }, + { + "guid": "jid1-0xtMKhXFEs4jIg@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i586", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1011286", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on appears to be malware installed without user consent.", + "name": "ep (malware)", + "created": "2014-06-03T15:50:19Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "50ca2179-83ab-1817-163d-39ed2a9fbd28", + "last_modified": 1480349213717 + }, + { + "guid": "/^({16e193c8-1706-40bf-b6f3-91403a9a22be}|{284fed43-2e13-4afe-8aeb-50827d510e20}|{5e3cc5d8-ed11-4bed-bc47-35b4c4bc1033}|{7429e64a-1fd4-4112-a186-2b5630816b91}|{8c9980d7-0f09-4459-9197-99b3e559660c}|{8f1d9545-0bb9-4583-bb3c-5e1ac1e2920c})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i517", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947509", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by silently installing the add-on, and using multiple add-on IDs.", + "name": "Re-markit", + "created": "2013-12-20T12:54:33Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e88a28ab-5569-f06d-b0e2-15c51bb2a4b7", + "last_modified": 1480349213344 + }, + { + "guid": "safebrowse@safebrowse.co", + "prefs": [], + "schema": 1480349193877, + "blockID": "i782", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1097696", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on loads scripts with malicious code that appears intended to steal usernames, passwords, and other private information.", + "name": "SafeBrowse", + "created": "2014-11-12T14:20:44Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "edd81c91-383b-f041-d8f6-d0b9a90230bd", + "last_modified": 1480349213319 + }, + { + "guid": "{af95cc15-3b9b-45ae-8d9b-98d08eda3111}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i492", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=945126", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious Firefox extension that uses a deceptive name and hijacks users' Facebook accounts.", + "name": "Facebook (malware)", + "created": "2013-12-02T12:45:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "7064e9e2-fba4-7b57-86d7-6f4afbf6f560", + "last_modified": 1480349213294 + }, + { + "guid": "{84a93d51-b7a9-431e-8ff8-d60e5d7f5df1}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i744", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080817", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on appears to be silently installed into users' systems, and changes settings without consent, in violation of the Add-on Guidelines.", + "name": "Spigot Shopping Assistant", + "created": "2014-10-17T15:47:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "dbc7ef8b-2c48-5dae-73a0-f87288c669f0", + "last_modified": 1480349213264 + }, + { + "guid": "{C3949AC2-4B17-43ee-B4F1-D26B9D42404D}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i918", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1170633", + "who": "All Firefox users who have this add-on installed in Firefox 39 and above.\r\n", + "why": "Certain versions of this extension are causing startup crashes in Firefox 39 and above.\r\n", + "name": "RealPlayer Browser Record Plugin", + "created": "2015-06-02T09:58:16Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "39.0a1" + } + ] + } + ], + "id": "7f2a68f3-aa8a-ae41-1e48-d1f8f63d53c7", + "last_modified": 1480349213231 + }, + { + "guid": "831778-poidjao88DASfsAnindsd@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i972", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1190962", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Video patch (malware)", + "created": "2015-08-04T15:18:03Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "39471221-6926-e11b-175a-b28424d49bf6", + "last_modified": 1480349213194 + }, + { + "guid": "lbmsrvfvxcblvpane@lpaezhjez.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i342", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=863385", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines. It also appears to install itself both locally and globally, producing a confusing uninstall experience.", + "name": "RapidFinda", + "created": "2013-05-06T16:18:14Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "98fb4536-07a4-d03a-f7c5-945acecc8203", + "last_modified": 1480349213128 + }, + { + "guid": "{babb9931-ad56-444c-b935-38bffe18ad26}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i499", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=946086", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious Firefox extension that uses a deceptive name and hijacks users' Facebook accounts.", + "name": "Facebook Credits (malware)", + "created": "2013-12-04T15:22:02Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "be1d19fa-1662-322a-13e6-5fa5474f33a7", + "last_modified": 1480349213100 + }, + { + "guid": "{18d5a8fe-5428-485b-968f-b97b05a92b54}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i802", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080839", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and is considered malware, in violation of the Add-on Guidelines.", + "name": "Astromenda Search Addon", + "created": "2014-12-15T10:52:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "bc846147-cdc1-141f-5846-b705c48bd6ed", + "last_modified": 1480349213074 + }, + { + "guid": "{b6ef1336-69bb-45b6-8cba-e578fc0e4433}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i780", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Power-SW", + "created": "2014-11-12T14:00:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3b080157-2900-d071-60fe-52b0aa376cf0", + "last_modified": 1480349213024 + }, + { + "guid": "info@wxdownloadmanager.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i196", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=806451", + "who": "All Firefox users who have these add-ons installed.", + "why": "These are malicious add-ons that are distributed with a trojan and negatively affect web browsing.", + "name": "Codec (malware)", + "created": "2012-11-05T09:24:13Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b62597d0-d2cb-d597-7358-5143a1d13658", + "last_modified": 1480349212999 + }, + { + "os": "WINNT", + "guid": "{C3949AC2-4B17-43ee-B4F1-D26B9D42404D}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i111", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=771802", + "who": "All Firefox users on Windows who have the RealPlayer Browser Record extension installed.", + "why": "The RealPlayer Browser Record extension is causing significant problems on Flash video sites like YouTube. This block automatically disables the add-on, but users can re-enable it from the Add-ons Manager if necessary.\r\n\r\nThis block shouldn't disable any other RealPlayer plugins, so watching RealPlayer content on the web should be unaffected.\r\n\r\nIf you still have problems playing videos on YouTube or elsewhere, please visit our support site for help.", + "name": "RealPlayer Browser Record Plugin", + "created": "2012-07-10T15:28:16Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "15.0.5", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d3f96257-7635-555f-ef48-34d426322992", + "last_modified": 1480349212971 + }, + { + "guid": "l@AdLJ7uz.net", + "prefs": [ + "browser.startup.homepage" + ], + "schema": 1480349193877, + "blockID": "i728", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1076771", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed and changes user settings without consent, in violation of the Add-on Guidelines", + "name": "GGoSavee", + "created": "2014-10-16T16:34:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e6bfa340-7d8a-1627-5cdf-40c0c4982e9d", + "last_modified": 1480349212911 + }, + { + "guid": "{6b2a75c8-6e2e-4267-b955-43e25b54e575}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i698", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1052611", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "BrowserShield", + "created": "2014-08-21T15:46:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "492e4e43-f89f-da58-9c09-d99528ee9ca9", + "last_modified": 1480349212871 + }, + { + "guid": "/^({65f9f6b7-2dae-46fc-bfaf-f88e4af1beca}|{9ed31f84-c8b3-4926-b950-dff74047ff79}|{0134af61-7a0c-4649-aeca-90d776060cb3}|{02edb56b-9b33-435b-b7df-b2843273a694}|{da51d4f6-3e7e-4ef8-b400-9198e0874606}|{b24577db-155e-4077-bb37-3fdd3c302bb5})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i525", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949566", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted and using multiple IDs.", + "name": "KeyBar add-on", + "created": "2013-12-20T14:11:14Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "78562d79-9a64-c259-fb63-ce24e29bb141", + "last_modified": 1480349212839 + }, + { + "guid": "adsremoval@adsremoval.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i560", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=962793", + "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed and changes various user settings, in violation of the Add-on Guidelines.", + "name": "Ad Removal", + "created": "2014-02-27T09:57:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4d150ad4-dc22-9790-07a9-36e0a23f857f", + "last_modified": 1480349212798 + }, + { + "guid": "firefoxaddon@youtubeenhancer.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i445", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=911966", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that imitates a popular video downloader extension, and attempts to hijack Facebook accounts. The add-on available in the add-ons site is safe to use.", + "name": "YouTube Enhancer Plus (malware)", + "created": "2013-09-04T16:53:37Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "208.7.0", + "minVersion": "208.7.0", + "targetApplication": [] + } + ], + "id": "41d75d3f-a57e-d5ad-b95b-22f5fa010b4e", + "last_modified": 1480349212747 + }, + { + "guid": "{336D0C35-8A85-403a-B9D2-65C292C39087}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i224", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=812292", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is side-installed with other software, and blocks setting reversions attempted by users who want to recover their settings after they are hijacked by other add-ons.", + "name": "IB Updater", + "created": "2012-11-29T16:22:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c87666e6-ec9a-2f1e-ad03-a722d2fa2a25", + "last_modified": 1480349212655 + }, + { + "guid": "G4Ce4@w.net", + "prefs": [ + "browser.startup.homepage" + ], + "schema": 1480349193877, + "blockID": "i718", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1076771", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems and changes settings without consent, in violation of the Add-on Guidelines.", + "name": "YoutUbeAdBlaocke", + "created": "2014-10-02T12:21:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3e1e9322-93e9-4ce1-41f5-46ad4ef1471b", + "last_modified": 1480349212277 + }, + { + "guid": "extension@Fast_Free_Converter.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i533", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949597", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by silently installing it.", + "name": "FastFreeConverter", + "created": "2013-12-20T15:04:18Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "726f5645-c0bf-66dc-a97a-d072b46e63e7", + "last_modified": 1480349212247 + }, + { + "guid": "@stopad", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1266", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1298780", + "who": "Users who have version 0.0.4 and earlier of the add-on installed.", + "why": "Stop Ads sends each visited url to a third party server which is not necessary for the add-on to work or disclosed in a privacy policy or user opt-in. Versions 0.0.4 and earlier are affected.", + "name": "Stop Ads Addon", + "created": "2016-08-30T12:24:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "0.0.4", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3d1893dd-2092-d1f7-03f3-9629b7d7139e", + "last_modified": 1480349212214 + }, + { + "guid": "703db0db-5fe9-44b6-9f53-c6a91a0ad5bd@7314bc82-969e-4d2a-921b-e5edd0b02cf1.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i519", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947509", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by silently installing the add-on, and using multiple add-on IDs.", + "name": "Re-markit", + "created": "2013-12-20T12:57:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a27d0f9f-7708-3d5f-82e1-e3f29e6098a0", + "last_modified": 1480349212183 + }, + { + "guid": "{13c9f1f9-2322-4d5c-81df-6d4bf8476ba4}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i348", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=867359", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines. It also fails to revert settings changes on removal.\r\n\r\nUsers who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "mywebsearch", + "created": "2013-05-08T15:55:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "372cf3df-0810-85d8-b5d7-faffff309a11", + "last_modified": 1480349212102 + }, + { + "guid": "{a6e67e6f-8615-4fe0-a599-34a73fc3fba5}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i346", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=867333", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines. Also, it doesn't reset its settings changes on uninstall.\r\n\r\nUsers who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Startnow", + "created": "2013-05-06T17:06:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1caf911c-ff2f-b0f6-0d32-29ef74be81bb", + "last_modified": 1480349212077 + }, + { + "guid": "9518042e-7ad6-4dac-b377-056e28d00c8f@f1cc0a13-4df1-4d66-938f-088db8838882.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i308", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=846455", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed, bypassing our third-party opt-in screen, in violation of our Add-on Guidelines.", + "name": "Solid Savings", + "created": "2013-02-28T13:48:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "df25ee07-74d4-ccd9-dbbe-7eb053015144", + "last_modified": 1480349212020 + }, + { + "guid": "jufa098j-LKooapd9jasJ9jliJsd@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1000", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1201163", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Secure Video (malware)", + "created": "2015-09-07T14:00:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c3a98025-0f4e-3bb4-b475-97329e7b1426", + "last_modified": 1480349211979 + }, + { + "guid": "{46eddf51-a4f6-4476-8d6c-31c5187b2a2f}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i750", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963788", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "Slick Savings", + "created": "2014-10-17T16:17:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9b4ef650-e1ad-d55f-c420-4f26dbb4139c", + "last_modified": 1480349211953 + }, + { + "guid": "{DAC3F861-B30D-40dd-9166-F4E75327FAC7}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i924", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1173154", + "who": "All Firefox users who have this add-on installed in Firefox 39 and above.\r\n", + "why": "Certain versions of this extension are causing startup crashes in Firefox 39 and above.\r\n", + "name": "RealPlayer Browser Record Plugin", + "created": "2015-06-09T15:28:17Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "39.0a1" + } + ] + } + ], + "id": "be57998b-9e4d-1040-e6bb-ed9de056338d", + "last_modified": 1480349211896 + }, + { + "guid": "JMLv@njMaHh.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i790", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1103516", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "YouttubeAdBlocke", + "created": "2014-11-24T14:14:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "070d5747-137d-8500-8713-cfc6437558a3", + "last_modified": 1480349211841 + }, + { + "guid": "istart_ffnt@gmail.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i888", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1152553", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-on Manager.", + "why": "This add-on appears to be malware, being silently installed and hijacking user settings, in violation of the Add-on Guidelines.", + "name": "Istart", + "created": "2015-04-10T16:27:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "32fad759-38d9-dad9-2295-e44cc6887040", + "last_modified": 1480349211785 + }, + { + "guid": "gystqfr@ylgga.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i449", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=912742", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines. It is installed bypassing the Firefox opt-in screen, and manipulates settings without reverting them on removal. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Define Ext", + "created": "2013-09-13T16:19:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8fe8f509-c530-777b-dccf-d10d58ae78cf", + "last_modified": 1480349211748 + }, + { + "guid": "e9d197d59f2f45f382b1aa5c14d82@8706aaed9b904554b5cb7984e9.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i844", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128324", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and attempts to change user settings like the home page and default search, in violation of the Add-on Guidelines.", + "name": "Sense", + "created": "2015-02-06T15:01:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f8f8695c-a356-a1d6-9291-502b377c63c2", + "last_modified": 1480349211713 + }, + { + "guid": "{184AA5E6-741D-464a-820E-94B3ABC2F3B4}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i968", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1164243", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that poses as a Java extension.", + "name": "Java String Helper (malware)", + "created": "2015-08-04T09:41:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "fac1d2cb-eed7-fcef-5d5a-43c556371bd7", + "last_modified": 1480349211687 + }, + { + "guid": "7d51fb17-b199-4d8f-894e-decaff4fc36a@a298838b-7f50-4c7c-9277-df6abbd42a0c.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i455", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=919792", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious extension that hijacks Facebook accounts and posts spam to the users' friends.", + "name": "Video Console (malware)", + "created": "2013-09-25T10:28:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "dd4d2e17-4ce6-36b0-3035-93e9cc5846d4", + "last_modified": 1480349211660 + }, + { + "guid": "prositez@prz.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i764", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "ProfSitez", + "created": "2014-10-29T16:43:15Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "684ad4fd-2cbd-ce2a-34cd-bc66b20ac8af", + "last_modified": 1480349211628 + }, + { + "guid": "{25D77636-38B1-1260-887C-2D4AFA92D6A4}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i536", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=959279", + "who": "All Firefox users who have this extension installed.", + "why": "This is a malicious extension that is installed alongside a trojan. It hijacks searches on selected sites.", + "name": "Microsoft DirectInput Object (malware)", + "created": "2014-01-13T10:36:05Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "cd174588-940e-f5b3-12ea-896c957bd4b3", + "last_modified": 1480349211555 + }, + { + "guid": "{badea1ae-72ed-4f6a-8c37-4db9a4ac7bc9}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i543", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963809", + "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enable it in the Add-ons Manager.", + "why": "This add-on is apparently malware that is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "Address Bar Search", + "created": "2014-01-28T14:28:18Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8c1dd68e-7df6-0c37-2f41-107745a7be54", + "last_modified": 1480349211119 + }, + { + "guid": "{d87d56b2-1379-49f4-b081-af2850c79d8e}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i726", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080835", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Website Xplorer Lite", + "created": "2014-10-13T16:01:03Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "7b0895b4-dd4f-1c91-f4e3-31afdbdf3178", + "last_modified": 1480349211007 + }, + { + "guid": "OKitSpace@OKitSpace.es", + "prefs": [], + "schema": 1480349193877, + "blockID": "i469", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935779", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is part of a malicious Firefox installer bundle.", + "name": "Installer bundle (malware)", + "created": "2013-11-07T15:35:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6a11aa68-0dae-5524-cc96-a5053a31c466", + "last_modified": 1480349210982 + }, + { + "guid": "{c96d1ae6-c4cf-4984-b110-f5f561b33b5a}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i808", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Better Web", + "created": "2014-12-19T09:36:52Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0413d46b-8205-d9e0-65df-4caa3e6355c4", + "last_modified": 1480349210956 + }, + { + "guid": "thefoxonlybetter@quicksaver", + "prefs": [], + "schema": 1480349193877, + "blockID": "i706", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1053469", + "who": "All Firefox users who have any of these versions of the add-on installed.", + "why": "Certain versions of The Fox, Only Better weren't developed by the original developer, and are likely malicious in nature. This violates the Add-on Guidelines for reusing an already existent ID.", + "name": "The Fox, Only Better (malicious versions)", + "created": "2014-08-27T14:50:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "1.6.160", + "minVersion": "1.6.160", + "targetApplication": [] + } + ], + "id": "bb2b2114-f8e7-511d-04dc-abc8366712cc", + "last_modified": 1480349210859 + }, + { + "guid": "CortonExt@ext.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i336", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=864551", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is reported to be installed without user consent, with a non-descriptive name, and ties a number of browser features to Amazon URLs, probably monetizing on affiliate codes.", + "name": "CortonExt", + "created": "2013-04-22T16:10:17Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a5bdd05d-eb4c-ce34-9909-a677b4322384", + "last_modified": 1480349210805 + }, + { + "guid": "1chtw@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i430", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=901770", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that uses a deceptive name and hijacks social networks.", + "name": " Mozilla Service Pack (malware)", + "created": "2013-08-05T16:42:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "bf1e31c7-ba50-1075-29ae-47368ac1d6de", + "last_modified": 1480349210773 + }, + { + "guid": "lrcsTube@hansanddeta.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i344", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=866944", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines.\r\n\r\nUsers who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "LyricsTube", + "created": "2013-05-06T16:44:04Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "424b9f39-d028-b1fb-d011-d8ffbbd20fe9", + "last_modified": 1480349210718 + }, + { + "guid": "{341f4dac-1966-47ff-aacf-0ce175f1498a}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i356", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=868129", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines.\r\n\r\nUsers who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "MyFreeGames", + "created": "2013-05-23T14:45:35Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "560e08b1-3471-ad34-8ca9-463f5ca5328c", + "last_modified": 1480349210665 + }, + { + "guid": "/^({d6e79525-4524-4707-9b97-1d70df8e7e59}|{ddb4644d-1a37-4e6d-8b6e-8e35e2a8ea6c}|{e55007f4-80c5-418e-ac33-10c4d60db01e}|{e77d8ca6-3a60-4ae9-8461-53b22fa3125b}|{e89a62b7-248e-492f-9715-43bf8c507a2f}|{5ce3e0cb-aa83-45cb-a7da-a2684f05b8f3})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i518", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947509", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by silently installing the add-on, and using multiple add-on IDs.", + "name": "Re-markit", + "created": "2013-12-20T12:56:17Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "145b0f22-501e-39eb-371e-ec8342a5add9", + "last_modified": 1480349210606 + }, + { + "guid": "{72b98dbc-939a-4e0e-b5a9-9fdbf75963ef}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i772", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "SitezExpert", + "created": "2014-10-31T16:15:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "386cb2c9-e674-ce2e-345f-d30a785f90c5", + "last_modified": 1480349210536 + }, + { + "guid": "hha8771ui3-Fo9j9h7aH98jsdfa8sda@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i970", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1190963", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Video fix (malware)", + "created": "2015-08-04T15:15:07Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3ca577d8-3685-4ba9-363b-5b2d8d8dd608", + "last_modified": 1480349210477 + }, + { + "guid": "{7e8a1050-cf67-4575-92df-dcc60e7d952d}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i478", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935796", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on violates the Add-on Guidelines. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "SweetPacks", + "created": "2013-11-08T15:42:28Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1519eb45-fcaa-b531-490d-fe366490ed45", + "last_modified": 1480349210416 + }, + { + "guid": "/^({66b103a7-d772-4fcd-ace4-16f79a9056e0}|{6926c7f7-6006-42d1-b046-eba1b3010315}|{72cabc40-64b2-46ed-8648-26d831761150}|{73ee2cf2-7b76-4c49-b659-c3d8cf30825d}|{ca6446a5-73d5-4c35-8aa1-c71dc1024a18}|{5373a31d-9410-45e2-b299-4f61428f0be4})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i521", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947485", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by being silently installed and using multiple add-on IDs.", + "name": "appbario7", + "created": "2013-12-20T13:14:29Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "983cb7fe-e0b4-6a2e-f174-d2670876b2cd", + "last_modified": 1480349210351 + }, + { + "guid": "{dd6b651f-dfb9-4142-b0bd-09912ad22674}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i400", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=835678", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This group of add-ons is silently installed, bypassing our install opt-in screen. This violates our Add-on Guidelines.", + "name": "Searchqu", + "created": "2013-06-25T15:16:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "975d2126-f727-f5b9-ca01-b83345b80c56", + "last_modified": 1480349210301 + }, + { + "guid": "25p@9eAkaLq.net", + "prefs": [ + "browser.startup.homepage" + ], + "schema": 1480349193877, + "blockID": "i730", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1076771", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed and changes user settings without consent, in violation of the Add-on Guidelines\r\n", + "name": "YYOutoubeAdBlocke", + "created": "2014-10-16T16:35:35Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5a0c5818-693f-43ae-f85a-c6928d9c2cc4", + "last_modified": 1480349210275 + }, + { + "guid": "tmbepff@trendmicro.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1222", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1275245", + "who": "All users of this add-on. If you wish to continue using it, you can enable it in the Add-ons Manager.", + "why": "Add-on is causing a high-frequency crash in Firefox", + "name": "Trend Micro BEP 9.1.0.1035 and lower", + "created": "2016-05-24T12:10:34Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "9.1.0.1035", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8045c799-486a-927c-b972-b9da1c2dab2f", + "last_modified": 1480349209818 + }, + { + "guid": "pricepeep@getpricepeep.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i220", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=811433", + "who": "All Firefox users who have Pricepeed below 2.1.0.20 installed.", + "why": "Versions older than 2.1.0.20 of the PricePeep add-on were silently side-installed with other software, injecting advertisements in Firefox. Versions 2.1.0.20 and above don't have the install problems are not blocked.", + "name": "PricePeep", + "created": "2012-11-29T16:18:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "2.1.0.19.99", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "227b9a8d-c18d-239c-135e-d79e614fe392", + "last_modified": 1480349209794 + }, + { + "guid": "hoverst@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i498", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=946029", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious Firefox extension that uses a deceptive name and hijacks users' Facebook accounts.", + "name": "Adobe Flash Player (malware)", + "created": "2013-12-04T15:17:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2b25ba3e-45db-0e6c-965a-3acda1a44117", + "last_modified": 1480349209745 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i606", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:20:07Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.13.*", + "minVersion": "3.15.13", + "targetApplication": [] + } + ], + "id": "c3d88e22-386a-da3b-8aba-3cb526e08053", + "last_modified": 1480349209713 + }, + { + "guid": "advance@windowsclient.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i508", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=950773", + "who": "All Firefox users who have this add-on installed.", + "why": "This is not the Microsoft .NET Framework Assistant created and distributed by Microsoft. It is a malicious extension that is distributed under the same name to trick users into installing it, and turns users into a botnet that conducts SQL injection attacks on visited websites.", + "name": "Microsoft .NET Framework Assistant (malware)", + "created": "2013-12-16T10:15:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e5d30a74-732e-c3fa-f13b-097ee28d4b27", + "last_modified": 1480349209674 + }, + { + "guid": "{5eeb83d0-96ea-4249-942c-beead6847053}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i756", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080846", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "SmarterPower", + "created": "2014-10-17T16:30:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e101dbc0-190c-f6d8-e168-0c1380581cc9", + "last_modified": 1480349209625 + }, + { + "guid": "/^({7e8a1050-cf67-4575-92df-dcc60e7d952d}|{b3420a9c-a397-4409-b90d-bcf22da1a08a}|{eca6641f-2176-42ba-bdbe-f3e327f8e0af}|{707dca12-3f99-4d94-afea-06dcc0ae0108}|{aea20431-87fc-40be-bc5b-18066fe2819c}|{30ee6676-1ba6-455a-a7e8-298fa863a546})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i523", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947481", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted.", + "name": "SweetPacks", + "created": "2013-12-20T13:42:15Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a3a6bc8e-46a1-b3d5-1b20-58b90ba099c3", + "last_modified": 1480349209559 + }, + { + "guid": "{e0352044-1439-48ba-99b6-b05ed1a4d2de}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i710", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Site Counselor", + "created": "2014-09-30T15:28:14Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b8fedf07-dcaf-f0e3-b42b-32db75c4c304", + "last_modified": 1480349209491 + }, + { + "guid": "addlyrics@addlyrics.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i426", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=891605", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines.\r\n\r\nUsers who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Add Lyrics", + "created": "2013-07-09T15:25:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "81678e9e-ebf0-47d6-e409-085c25e67c7e", + "last_modified": 1480349209383 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i610", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:21:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.22.*", + "minVersion": "3.15.22", + "targetApplication": [] + } + ], + "id": "935dfec3-d017-5660-db5b-94ae7cea6e5f", + "last_modified": 1480349209128 + }, + { + "guid": "info@allpremiumplay.info", + "prefs": [], + "schema": 1480349193877, + "blockID": "i163", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=806451", + "who": "All Firefox users who have these add-ons installed.", + "why": "These are malicious add-ons that are distributed with a trojan and negatively affect web browsing.", + "name": "Codec-C (malware)", + "created": "2012-10-29T16:40:07Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6afbf9b8-ae3a-6a48-0f6c-7a3e065ec043", + "last_modified": 1480349209076 + }, + { + "guid": "now.msn.com@services.mozilla.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i490", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=926378", + "who": "All Firefox users who have this add-on installed.", + "why": "As part of their ongoing work to fine-tune their editorial mix, msnNOW has decided that msnNOW will stop publishing on Dec. 3, 2013. Rather than having a single home for trending content, they will continue integrating that material throughout all MSN channels. A big thank you to everyone who followed msnNOW stories using the Firefox sidebar", + "name": "MSNNow (discontinued social provider)", + "created": "2013-11-27T08:06:04Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "de7d699d-016d-d973-5e39-52568de6ffde", + "last_modified": 1480349209021 + }, + { + "guid": "fftoolbar2014@etech.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i858", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1131078", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and changes users' settings, in violation of the Add-on Guidelines.", + "name": "FF Toolbar", + "created": "2015-02-11T15:32:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6877bf40-9e45-7017-4dac-14d09e7f0ef6", + "last_modified": 1480349208988 + }, + { + "guid": "mbrnovone@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i477", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=936249", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks Facebook accounts.", + "name": "Mozilla Security Service (malware)", + "created": "2013-11-08T15:35:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "758c2503-766d-a2f5-4c58-7cea93acfe05", + "last_modified": 1480349208962 + }, + { + "guid": "{739df940-c5ee-4bab-9d7e-270894ae687a}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i530", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949558", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted.", + "name": "WhiteSmoke New", + "created": "2013-12-20T14:49:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f8097aa6-3009-6dfc-59df-353ba6b1142b", + "last_modified": 1480349208933 + }, + { + "guid": "firefoxaddon@youtubeenhancer.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i636", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1033120", + "who": "All Firefox users who have this version of the add-on installed.", + "why": "Version 199.7.0 of the YoutubeEnhancer extension isn't developed by the original developer, and is likely malicious in nature. It violates the Add-on Guidelines for reusing an already existent ID.", + "name": "YoutubeEnhancer - Firefox", + "created": "2014-07-08T15:58:12Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "199.7.0", + "minVersion": "199.7.0", + "targetApplication": [] + } + ], + "id": "204a074b-da87-2784-f15b-43a9ea9a6b36", + "last_modified": 1480349208851 + }, + { + "guid": "extacylife@a.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i505", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947741", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious extension that hijacks users' Facebook accounts.", + "name": "Facebook Haber (malware)", + "created": "2013-12-09T15:08:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5acadb8d-d3be-e0e0-4656-9107f9de0ea9", + "last_modified": 1480349208823 + }, + { + "guid": "{746505DC-0E21-4667-97F8-72EA6BCF5EEF}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i842", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128325", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "Shopper-Pro", + "created": "2015-02-06T14:45:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5f19c5fb-1c78-cbd6-8a03-1678efb54cbc", + "last_modified": 1480349208766 + }, + { + "guid": "{51c77233-c0ad-4220-8388-47c11c18b355}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i580", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1004132", + "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, it can be enabled in the Add-ons Manager.", + "why": "This add-on is silently installed into Firefox, in violation of the Add-on Guidelines.", + "name": "Browser Utility", + "created": "2014-04-30T13:55:35Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "0.1.9999999", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "daa2c60a-5009-2c65-a432-161d50bef481", + "last_modified": 1480349208691 + }, + { + "guid": "{a2bfe612-4cf5-48ea-907c-f3fb25bc9d6b}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i712", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Website Xplorer", + "created": "2014-09-30T15:28:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "13450534-93d7-f2a2-7f0a-e4e3948c4dc1", + "last_modified": 1480349208027 + }, + { + "guid": "ScorpionSaver@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i539", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963826", + "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enable it in the Add-ons Manager.", + "why": "This add-on is being silently installed, in violation of the Add-on Guidelines", + "name": "ScorpionSaver", + "created": "2014-01-28T14:00:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3499c968-6e8b-37f1-5f6e-2384807c2a6d", + "last_modified": 1480349207972 + }, + { + "guid": "{D19CA586-DD6C-4a0a-96F8-14644F340D60}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i42", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=690184", + "who": "Users of McAfee ScriptScan versions 14.4.0 and below for all versions of Firefox and SeaMonkey.", + "why": "This add-on causes a high volume of crashes.", + "name": "McAfee ScriptScan", + "created": "2011-10-03T09:38:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "14.4.0", + "minVersion": "0.1", + "targetApplication": [] + } + ], + "id": "1d35ac9d-49df-23cf-51f5-f3c228ad0dc9", + "last_modified": 1480349207877 + }, + { + "guid": "gjhrjenrengoe@jfdnkwelfwkm.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1042", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1212174", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that takes over Facebook accounts.", + "name": "Avant Player (malware)", + "created": "2015-10-07T13:12:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d6453893-becc-7617-2050-0db284e0e0db", + "last_modified": 1480349207840 + }, + { + "guid": "{33e0daa6-3af3-d8b5-6752-10e949c61516}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i282", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=835683", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on violates our add-on guidelines, bypassing the third party opt-in screen.", + "name": "Complitly", + "created": "2013-02-15T12:19:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.1.999", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1f94bc8d-9d5f-c8f5-45c0-ad1f6e147c71", + "last_modified": 1480349207789 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i608", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:20:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.20.*", + "minVersion": "3.15.18", + "targetApplication": [] + } + ], + "id": "e7d50ff2-5948-d571-6711-37908ccb863f", + "last_modified": 1480349207761 + }, + { + "guid": "{63eb5ed4-e1b3-47ec-a253-f8462f205350}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i786", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "FF-Plugin", + "created": "2014-11-18T12:33:13Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ca4558a2-8ce4-3ca0-3d29-63019f680c8c", + "last_modified": 1480349207705 + }, + { + "guid": "jid1-4vUehhSALFNqCw@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i634", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1033002", + "who": "All Firefox users who have this version of the add-on installed.", + "why": "Version 99.7 of the YouTube Plus Plus extension isn't developed by the original developer, and is likely malicious in nature. It violates the Add-on Guidelines for reusing an already existent ID.", + "name": "YouTube Plus Plus 99.7", + "created": "2014-07-04T14:13:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "99.7", + "minVersion": "99.7", + "targetApplication": [] + } + ], + "id": "a6d017cb-e33f-2239-4e42-ab4e7cfb19fe", + "last_modified": 1480349207680 + }, + { + "guid": "{aab02ab1-33cf-4dfa-8a9f-f4e60e976d27}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i820", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems without their consent, in violation of the Add-on Guidelines.", + "name": "Incredible Web", + "created": "2015-01-13T09:27:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "847ecc6e-1bc1-f7ff-e1d5-a76e6b8447d2", + "last_modified": 1480349207654 + }, + { + "guid": "torntv@torntv.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i320", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=845610", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines, bypassing our third party install opt-in screen. Users who wish to continue using this extension can enable it in the Add-ons Manager.", + "name": "TornTV", + "created": "2013-03-20T16:35:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "cdd492b8-8101-74a9-5760-52ff709fd445", + "last_modified": 1480349207608 + }, + { + "guid": "crossriderapp12555@crossrider.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i674", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=877836", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "The add-on is silently installed, in violation of our Add-on Guidelines.", + "name": "JollyWallet", + "created": "2014-07-22T16:26:19Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d4467e20-0f71-f0e0-8cd6-40c82b6c7379", + "last_modified": 1480349207561 + }, + { + "guid": "344141-fasf9jas08hasoiesj9ia8ws@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1038", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1211169", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Video Plugin (malware)", + "created": "2015-10-05T16:42:09Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f7986b7b-9b5a-d372-8147-8b4bd6f5a29b", + "last_modified": 1480349207485 + }, + { + "guid": "meOYKQEbBBjH5Ml91z0p9Aosgus8P55bjTa4KPfl@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i998", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1201164", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Smooth Player (malware)", + "created": "2015-09-07T13:54:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "30c3e511-9e8d-15ee-0867-d61047e56515", + "last_modified": 1480349207370 + }, + { + "guid": "{8dc5c42e-9204-2a64-8b97-fa94ff8a241f}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i770", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1088726", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and is considered malware, in violation of the Add-on Guidelines.", + "name": "Astrmenda Search", + "created": "2014-10-30T14:52:52Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8a9c7702-0349-70d6-e64e-3a666ab084c6", + "last_modified": 1480349207320 + }, + { + "guid": "savingsslider@mybrowserbar.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i752", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963788", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "Slick Savings", + "created": "2014-10-17T16:18:12Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9b1faf30-5725-7847-d993-b5cdaabc9829", + "last_modified": 1480349207290 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i612", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:23:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.24.*", + "minVersion": "3.15.24", + "targetApplication": [] + } + ], + "id": "e0ff9df4-60e4-dbd0-8018-57f395e6610a", + "last_modified": 1480349206818 + }, + { + "guid": "{1e4ea5fc-09e5-4f45-a43b-c048304899fc}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i812", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Great Finder", + "created": "2015-01-06T13:22:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1ea40b9f-2423-a2fd-a5e9-4ec1df2715f4", + "last_modified": 1480349206784 + }, + { + "guid": "crossriderapp8812@crossrider.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i314", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=835665", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines, bypassing our third party install opt-in screen. Users who wish to continue using this extension can enable it in the Add-ons Manager.", + "name": "Coupon Companion", + "created": "2013-03-06T14:14:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "06c07e28-0a34-e5ee-e724-491a2f6ce586", + "last_modified": 1480349206708 + }, + { + "guid": "/^(ffxtlbr@mixidj\\.com|{c0c2693d-2ee8-47b4-9df7-b67a0ee31988}|{67097627-fd8e-4f6b-af4b-ecb65e50112e}|{f6f0f973-a4a3-48cf-9a7a-b7a69c30d71a}|{a3d0e35f-f1da-4ccb-ae77-e9d27777e68d}|{1122b43d-30ee-403f-9bfa-3cc99b0caddd})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i540", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963819", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on has been repeatedly blocked before and keeps showing up with new add-on IDs, in violation of the Add-on Guidelines.", + "name": "MixiDJ (malware)", + "created": "2014-01-28T14:07:56Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4c03ddda-bb3f-f097-0a7b-b7b77b050584", + "last_modified": 1480349206678 + }, + { + "guid": "hansin@topvest.id", + "prefs": [], + "schema": 1480349193877, + "blockID": "i836", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1130406", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Inside News (malware)", + "created": "2015-02-06T14:17:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0945a657-f28d-a02c-01b2-5115b3f90d7a", + "last_modified": 1480349206628 + }, + { + "guid": "lfind@nijadsoft.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i358", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=874131", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines.\r\n\r\nUsers who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Lyrics Finder", + "created": "2013-05-24T14:09:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2307f11c-6216-0dbf-a464-b2921055ce2b", + "last_modified": 1480349206603 + }, + { + "guid": "plugin@getwebcake.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i484", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=938264", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on violates the Add-on Guidelines and is broadly considered to be malware. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "WebCake", + "created": "2013-11-14T09:55:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2865addd-da1c-20c4-742f-6a2270da2e78", + "last_modified": 1480349206578 + }, + { + "guid": "{c0c2693d-2ee8-47b4-9df7-b67a0ee31988}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i354", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=837838", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines.\r\n\r\nUsers who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Mixi DJ", + "created": "2013-05-23T14:31:21Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "03a745c3-0ee7-e262-ba31-62d4f78ddb62", + "last_modified": 1480349206525 + }, + { + "guid": "/^({7316e43a-3ebd-4bb4-95c1-9caf6756c97f}|{0cc09160-108c-4759-bab1-5c12c216e005}|{ef03e721-f564-4333-a331-d4062cee6f2b}|{465fcfbb-47a4-4866-a5d5-d12f9a77da00}|{7557724b-30a9-42a4-98eb-77fcb0fd1be3}|{b7c7d4b0-7a84-4b73-a7ef-48ef59a52c3b})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i520", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947485", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by being silently installed and using multiple add-on IDs.", + "name": "appbario7", + "created": "2013-12-20T13:11:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e3901c48-9c06-fecb-87d3-efffd9940c22", + "last_modified": 1480349206491 + }, + { + "guid": "{354dbb0a-71d5-4e9f-9c02-6c88b9d387ba}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i538", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=964081", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Show Mask ON (malware)", + "created": "2014-01-27T10:13:17Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "aad90253-8921-b5df-3658-45a70d75f3d7", + "last_modified": 1480349206465 + }, + { + "guid": "{8E9E3331-D360-4f87-8803-52DE43566502}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i461", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=906071", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This is a companion add-on for the SweetPacks Toolbar which is blocked due to guideline violations.", + "name": "Updater By SweetPacks", + "created": "2013-10-17T16:10:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ae8cca6e-4258-545f-9a69-3d908264a701", + "last_modified": 1480349206437 + }, + { + "guid": "info@bflix.info", + "prefs": [], + "schema": 1480349193877, + "blockID": "i172", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=806802", + "who": "All Firefox users who have this add-on installed.", + "why": "These are malicious add-ons that are distributed with a trojan and negatively affect web browsing.", + "name": "Bflix (malware)", + "created": "2012-10-30T13:39:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "7a9062f4-218d-51d2-9b8c-b282e6eada4f", + "last_modified": 1480349206384 + }, + { + "guid": "{dff137ae-1ffd-11e3-8277-b8ac6f996f26}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i450", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=917861", + "who": "All Firefox users who have this add-on installed.", + "why": "This is add-on is malware that silently redirects popular search queries to a third party.", + "name": "Addons Engine (malware)", + "created": "2013-09-18T16:19:34Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8e583fe4-1c09-9bea-2473-faecf3260685", + "last_modified": 1480349206312 + }, + { + "guid": "12x3q@3244516.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i493", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=939254", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on appears to be malware and is installed silently in violation of the Add-on Guidelines.", + "name": "BetterSurf (malware)", + "created": "2013-12-02T12:49:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "af2a9e74-3753-9ff1-d899-5d1e79ed3dce", + "last_modified": 1480349206286 + }, + { + "guid": "{20AD702C-661E-4534-8CE9-BA4EC9AD6ECC}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i626", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1027886", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is probably silently installed, and is causing significant stability issues for users, in violation of the Add-on Guidelines.", + "name": "V-Bates", + "created": "2014-06-19T15:16:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9b9ccabe-8f9a-e3d1-a689-1aefba1f33b6", + "last_modified": 1480349206261 + }, + { + "guid": "{c5e48979-bd7f-4cf7-9b73-2482a67a4f37}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i736", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080842", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "ClearThink", + "created": "2014-10-17T15:22:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6e8b3e4f-2f59-cde3-e6d2-5bc6e216c506", + "last_modified": 1480349206231 + }, + { + "guid": "{41339ee8-61ed-489d-b049-01e41fd5d7e0}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i810", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "FireWeb", + "created": "2014-12-23T10:32:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a35f2ca6-aec4-c01d-170e-650258ebcd2c", + "last_modified": 1480349206165 + }, + { + "guid": "{845cab51-d8d2-472f-8bd9-2b44642d97c2}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i460", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=927456", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and handles users' settings, violating some of the Add-on Guidelines. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Vafmusic9", + "created": "2013-10-17T15:50:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8538ccb4-3b71-9858-3f6d-c0fff7af58b0", + "last_modified": 1480349205746 + }, + { + "guid": "SpecialSavings@SpecialSavings.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i676", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=881511", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This is add-on is generally considered to be unwanted and is probably silently installed, in violation of the Add-on Guidelines.", + "name": "SpecialSavings", + "created": "2014-07-22T16:31:56Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5e921810-fc3a-0729-6749-47e38ad10a22", + "last_modified": 1480349205688 + }, + { + "guid": "afurladvisor@anchorfree.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i434", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=844945", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on bypasses the external install opt in screen in Firefox, violating the Add-on Guidelines. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Hotspot Shield Helper", + "created": "2013-08-09T11:26:11Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "083585eb-d7e7-e228-5fbf-bf35c52044e4", + "last_modified": 1480349205645 + }, + { + "guid": "info@thebflix.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i174", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=806802", + "who": "All Firefox users who have these add-ons installed.", + "why": "These are malicious add-ons that are distributed with a trojan and negatively affect web browsing.", + "name": "Bflix (malware)", + "created": "2012-10-30T13:40:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "811a61d4-9435-133e-6262-fb72486c36b0", + "last_modified": 1480349205526 + }, + { + "guid": "{EEE6C361-6118-11DC-9C72-001320C79847}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i392", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=881447", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on changes search settings without user interaction, and fails to reset them after it is removed. This violates our Add-on Guidelines.", + "name": "SweetPacks Toolbar", + "created": "2013-06-25T12:38:45Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c1dc6607-4c0a-4031-9f14-70ef1ae1edcb", + "last_modified": 1480349205455 + }, + { + "guid": "/^(4cb61367-efbf-4aa1-8e3a-7f776c9d5763@cdece6e9-b2ef-40a9-b178-291da9870c59\\.com|0efc9c38-1ec7-49ed-8915-53a48b6b7600@e7f17679-2a42-4659-83c5-7ba961fdf75a\\.com|6be3335b-ef79-4b0b-a0ba-b87afbc6f4ad@6bbb4d2e-e33e-4fa5-9b37-934f4fb50182\\.com)$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i531", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949672", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted and using multiple IDs.", + "name": "Feven", + "created": "2013-12-20T15:01:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "46aa79a9-d329-f713-d4f2-07d31fe7071e", + "last_modified": 1480349205287 + }, + { + "guid": "afext@anchorfree.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i466", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=933988", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't respect user choice, violating the Add-on Guidelines. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Hotspot Shield Extension", + "created": "2013-11-07T13:32:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8176f879-bd73-5468-e908-2d7cfc115ac2", + "last_modified": 1480349205108 + }, + { + "guid": "{FCE04E1F-9378-4f39-96F6-5689A9159E45}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i920", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1173154", + "who": "All Firefox users who have this add-on installed in Firefox 39 and above.\r\n", + "why": "Certain versions of this extension are causing startup crashes in Firefox 39 and above.\r\n", + "name": "RealPlayer Browser Record Plugin", + "created": "2015-06-09T15:26:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "39.0a1" + } + ] + } + ], + "id": "eb191ff0-20f4-6e04-4344-d880af4faf51", + "last_modified": 1480349204978 + }, + { + "guid": "{9CE11043-9A15-4207-A565-0C94C42D590D}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i503", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947384", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious extension that uses a deceptive name to stay in users' systems.", + "name": "XUL Cache (malware)", + "created": "2013-12-06T11:58:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "dcdae267-8d3a-5671-dff2-f960febbbb20", + "last_modified": 1480349204951 + }, + { + "guid": "{0153E448-190B-4987-BDE1-F256CADA672F}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i914", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1170633", + "who": "All Firefox users who have this add-on installed in Firefox 39 and above.", + "why": "Certain versions of this extension are causing startup crashes in Firefox 39 and above.", + "name": "RealPlayer Browser Record Plugin", + "created": "2015-06-02T09:56:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "39.0a1" + } + ] + } + ], + "id": "2bfe0d89-e458-9d0e-f944-ddeaf8c4db6c", + "last_modified": 1480349204871 + }, + { + "guid": "{77beece6-3997-403a-92fa-0055bfcf88e5}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i452", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=916966", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines, manipulating settings without reverting them on removal. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Entrusted11", + "created": "2013-09-18T16:34:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d348f91f-caeb-a803-dfd9-fd5d285aa0fa", + "last_modified": 1480349204844 + }, + { + "guid": "dealcabby@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i222", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=811435", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently side-installed with other software, injecting advertisements in Firefox.", + "name": "DealCabby", + "created": "2012-11-29T16:20:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6585f0bd-4f66-71e8-c565-d9762c5c084a", + "last_modified": 1480349204818 + }, + { + "guid": "{3c9a72a0-b849-40f3-8c84-219109c27554}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i510", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=951301", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks users' Facebook accounts.", + "name": "Facebook Haber (malware)", + "created": "2013-12-17T14:27:13Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "7cfa3d0b-0ab2-5e3a-8143-1031c180e32f", + "last_modified": 1480349204778 + }, + { + "guid": "{2aab351c-ad56-444c-b935-38bffe18ad26}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i500", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=946087", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious Firefox extension that uses a deceptive name and hijacks users' Facebook accounts.", + "name": "Adobe Photo (malware)", + "created": "2013-12-04T15:29:44Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f7a76d34-ddcd-155e-9fae-5967bd796041", + "last_modified": 1480349204716 + }, + { + "guid": "{0A92F062-6AC6-8180-5881-B6E0C0DC2CC5}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i864", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1131220", + "who": "All users who have this add-on installed. Users who wish to continue using the add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems and makes unwanted settings changes, in violation of our Add-on Guidelines.", + "name": "BlockAndSurf", + "created": "2015-02-26T12:56:19Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "acb16d1c-6274-93a3-7c1c-7ed36ede64a9", + "last_modified": 1480349204612 + }, + { + "guid": "trackerbird@bustany.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i986", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1189264", + "who": "All Thunderbird users who have this version of the add-on installed on Thunderbird 38.0a2 and above.", + "why": "This add-on is causing consistent crashes on Thunderbird 38.0a2 and above.", + "name": "trackerbird 1.2.6", + "created": "2015-08-17T15:56:04Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.2.6", + "minVersion": "1.2.6", + "targetApplication": [ + { + "guid": "{3550f703-e582-4d05-9a08-453d09bdfdc6}", + "maxVersion": "*", + "minVersion": "38.0a2" + } + ] + } + ], + "id": "bb1c699e-8790-4528-0b6d-4f83b7a3152d", + "last_modified": 1480349204041 + }, + { + "guid": "{0134af61-7a0c-4649-aeca-90d776060cb3}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i448", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=912746", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines. It manipulates settings without reverting them on removal. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "KeyBar add-on", + "created": "2013-09-13T16:15:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "cf428416-4974-8bb4-7928-c0cb2cfe7957", + "last_modified": 1480349203968 + }, + { + "guid": "/^(firefox@vebergreat\\.net|EFGLQA@78ETGYN-0W7FN789T87\\.COM)$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i564", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=974104", + "who": "All Firefox users who have these add-ons installed. If you wish to continue using these add-ons, you can enable them in the Add-ons Manager.", + "why": "These add-ons are silently installed by the Free Driver Scout installer, in violation of our Add-on Guidelines.", + "name": "veberGreat and vis (Free Driver Scout bundle)", + "created": "2014-03-05T13:02:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "487538f1-698e-147e-6395-986759ceed7e", + "last_modified": 1480349203902 + }, + { + "guid": "69ffxtbr@PackageTracer_69.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i882", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1153001", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on appears to be malware, hijacking user's settings, in violation of the Add-on Guidelines.", + "name": "PackageTracer", + "created": "2015-04-10T16:18:35Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0d37b4e0-3c60-fdad-dd8c-59baff6eae87", + "last_modified": 1480349203836 + }, + { + "guid": "{a9bb9fa0-4122-4c75-bd9a-bc27db3f9155}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i404", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=835678", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This group of add-ons is silently installed, bypassing our install opt-in screen. This violates our Add-on Guidelines.", + "name": "Searchqu", + "created": "2013-06-25T15:16:43Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "fb7a1dc7-16a0-4f70-8289-4df494e0d0fa", + "last_modified": 1480349203633 + }, + { + "guid": "P2@D.edu", + "prefs": [], + "schema": 1480349193877, + "blockID": "i850", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128269", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and performs unwanted actions, in violation of the Add-on Guidelines.", + "name": "unIsaless", + "created": "2015-02-09T15:29:21Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "49536a29-fc7e-9fd0-f415-e15ac090fa56", + "last_modified": 1480349203605 + }, + { + "guid": "linksicle@linksicle.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i472", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935779", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is part of a malicious Firefox installer bundle.", + "name": "Installer bundle (malware)", + "created": "2013-11-07T15:38:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9b5b15b3-6da7-cb7c-3c44-30b4fe079d52", + "last_modified": 1480349203581 + }, + { + "guid": "{377e5d4d-77e5-476a-8716-7e70a9272da0}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i398", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=835678", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This group of add-ons is silently installed, bypassing our install opt-in screen. This violates our Add-on Guidelines.", + "name": "Searchqu", + "created": "2013-06-25T15:15:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ea94df32-2a85-23da-43f7-3fc5714530ec", + "last_modified": 1480349203519 + }, + { + "guid": "{4933189D-C7F7-4C6E-834B-A29F087BFD23}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i437", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=900695", + "who": "All Firefox users.", + "why": "This add-on is widely reported to be malware.", + "name": "Win32.SMSWebalta (malware)", + "created": "2013-08-09T15:14:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "cbef1357-d6bc-c8d3-7a82-44af6b1c390f", + "last_modified": 1480349203486 + }, + { + "guid": "{ADFA33FD-16F5-4355-8504-DF4D664CFE10}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i306", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=844972", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed, bypassing our third-party opt-in screen, in violation of our Add-on Guidelines. It's also possible that it changes user settings without their consent.", + "name": "Nation Toolbar", + "created": "2013-02-28T12:56:48Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "017fd151-37ca-4646-4763-1d303fb918fa", + "last_modified": 1480349203460 + }, + { + "guid": "detgdp@gmail.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i884", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1152614", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on appears to be malware, hijacking user settings, in violation of the Add-on Guidelines.", + "name": "Security Protection (malware)", + "created": "2015-04-10T16:21:34Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1b5cc88e-499d-2a47-d793-982d4c05e6ee", + "last_modified": 1480349203433 + }, + { + "guid": "/^(67314b39-24e6-4f05-99f3-3f88c7cddd17@6c5fa560-13a3-4d42-8e90-53d9930111f9\\.com|ffxtlbr@visualbee\\.com|{7aeae561-714b-45f6-ace3-4a8aed6e227b}|{7093ee04-f2e4-4637-a667-0f730797b3a0}|{53c4024f-5a2e-4f2a-b33e-e8784d730938})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i514", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947473", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by using multiple add-on IDs and making unwanted settings changes.", + "name": "VisualBee Toolbar", + "created": "2013-12-20T12:25:50Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5f91eee1-7303-3f97-dfe6-1e897a156c7f", + "last_modified": 1480349203408 + }, + { + "guid": "FXqG@xeeR.net", + "prefs": [ + "browser.startup.homepage" + ], + "schema": 1480349193877, + "blockID": "i720", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1076771", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems and changes settings without consent, in violation of the Add-on Guidelines.", + "name": "GoSSave", + "created": "2014-10-02T12:23:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8ebbc061-a4ff-b75b-ec42-eb17c42a2956", + "last_modified": 1480349203341 + }, + { + "guid": "kallow@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i495", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=945426", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious Firefox extension that uses a deceptive name and hijacks users' Facebook accounts.", + "name": "Facebook Security Service (malware)", + "created": "2013-12-02T15:09:42Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1a2c37a9-e7cc-2d03-2043-098d36b8aca2", + "last_modified": 1480349203247 + }, + { + "guid": "008abed2-b43a-46c9-9a5b-a771c87b82da@1ad61d53-2bdc-4484-a26b-b888ecae1906.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i528", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949565", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by being silently installed.", + "name": "weDownload Manager Pro", + "created": "2013-12-20T14:40:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "da46065f-1c68-78f7-80fc-8ae07b5df68d", + "last_modified": 1480349203131 + }, + { + "guid": "{25dd52dc-89a8-469d-9e8f-8d483095d1e8}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i714", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Web Counselor", + "created": "2014-10-01T15:36:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e46c31ad-0ab3-e48a-47aa-9fa91b675fda", + "last_modified": 1480349203066 + }, + { + "guid": "{B1FC07E1-E05B-4567-8891-E63FBE545BA8}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i926", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1173154", + "who": "All Firefox users who have this add-on installed in Firefox 39 and above.\r\n", + "why": "Certain versions of this extension are causing startup crashes in Firefox 39 and above.\r\n", + "name": "RealPlayer Browser Record Plugin", + "created": "2015-06-09T15:28:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "39.0a1" + } + ] + } + ], + "id": "09868783-261a-ac24-059d-fc772218c1ba", + "last_modified": 1480349202708 + }, + { + "guid": "/^(torntv@torntv\\.com|trtv3@trtv\\.com|torntv2@torntv\\.com|e2fd07a6-e282-4f2e-8965-85565fcb6384@b69158e6-3c3b-476c-9d98-ae5838c5b707\\.com)$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i529", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949559", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by being silently installed.", + "name": "TornTV", + "created": "2013-12-20T14:46:03Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "040e5ec2-ea34-816a-f99f-93296ce845e8", + "last_modified": 1480349202677 + }, + { + "guid": "249911bc-d1bd-4d66-8c17-df533609e6d8@c76f3de9-939e-4922-b73c-5d7a3139375d.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i532", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949672", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted and using multiple IDs.", + "name": "Feven", + "created": "2013-12-20T15:02:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d32b850d-82d5-b63d-087c-fb2041b2c232", + "last_modified": 1480349202631 + }, + { + "guid": "{7D4F1959-3F72-49d5-8E59-F02F8AA6815D}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i394", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=881447", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This is a companion add-on for the SweetPacks Toolbar which is blocked due to guideline violations.", + "name": "Updater By SweetPacks", + "created": "2013-06-25T12:40:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "851c2b8e-ea19-3a63-eac5-f931a8da5d6e", + "last_modified": 1480349202341 + }, + { + "guid": "g@uzcERQ6ko.net", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i776", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1076771", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed and changes user settings without consent, in violation of the Add-on Guidelines", + "name": "GoSave", + "created": "2014-10-31T16:23:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ee1e1a44-b51b-9f12-819d-64c3e515a147", + "last_modified": 1480349202307 + }, + { + "guid": "ffxtlbr@incredibar.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i318", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=812264", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines, bypassing our third party install opt-in screen. Users who wish to continue using this extension can enable it in the Add-ons Manager.", + "name": "IncrediBar", + "created": "2013-03-20T14:40:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9e84b07c-84d5-c932-85f2-589713d7e380", + "last_modified": 1480349202280 + }, + { + "guid": "M1uwW0@47z8gRpK8sULXXLivB.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i870", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1131159", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that goes by the the name \"Flash Player 11\".", + "name": "Flash Player 11 (malware)", + "created": "2015-03-04T14:34:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "71d961b2-37d1-d393-76f5-3afeef57e749", + "last_modified": 1480349202252 + }, + { + "guid": "4zffxtbr-bs@VideoDownloadConverter_4z.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i507", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949266", + "who": "All Firefox users who have this add-on installed.", + "why": "Certain versions of this add-on contains an executable that is flagged by multiple tools as malware. Newer versions no longer use it.", + "name": "VideoDownloadConverter", + "created": "2013-12-12T15:37:23Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "5.75.3.25126", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0a0f106a-ecc6-c537-1818-b36934943e91", + "last_modified": 1480349202156 + }, + { + "guid": "contato@facefollow.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i509", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=950846", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on spams users' Facebook accounts.", + "name": "Face follow", + "created": "2013-12-16T16:15:15Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "56f15747-af8c-342c-6877-a41eeacded84", + "last_modified": 1480349202067 + }, + { + "guid": "/^({83a8ce1b-683c-4784-b86d-9eb601b59f38}|{ef1feedd-d8da-4930-96f1-0a1a598375c6}|{79ff1aae-701f-4ca5-aea3-74b3eac6f01b}|{8a184644-a171-4b05-bc9a-28d75ffc9505}|{bc09c55d-0375-4dcc-836e-0e3c8addfbda}|{cef81415-2059-4dd5-9829-1aef3cf27f4f})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i526", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949566", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted and uses multiple IDs.", + "name": "KeyBar add-on", + "created": "2013-12-20T14:12:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9dfa4e92-bbf2-66d1-59a9-51402d1d226c", + "last_modified": 1480349202010 + }, + { + "guid": "{f2548724-373f-45fe-be6a-3a85e87b7711}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i768", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1088726", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and is considered malware, in violation of the Add-on Guidelines.", + "name": "Astro New Tab", + "created": "2014-10-30T14:52:09Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8510e9e2-c7d8-90d0-a2ff-eb09293acc6e", + "last_modified": 1480349201854 + }, + { + "guid": "KSqOiTeSJEDZtTGuvc18PdPmYodROmYzfpoyiCr2@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1032", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1211172", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Video Player (malware)", + "created": "2015-10-05T16:22:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3d9188ac-235f-773a-52a2-261b3ea9c03c", + "last_modified": 1480349201504 + }, + { + "guid": "{849ded12-59e9-4dae-8f86-918b70d213dc}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i708", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1047102", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed and changes homepage and search settings without the user's consent, in violation of the Add-on Guidelines.", + "name": "Astromenda New Tab", + "created": "2014-09-02T16:29:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a319bfee-464f-1c33-61ad-738c52842fbd", + "last_modified": 1480349201453 + }, + { + "guid": "grjkntbhr@hgergerherg.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1018", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1208196", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "GreenPlayer (malware)", + "created": "2015-09-24T16:04:53Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "9c47d940-bdd9-729f-e32e-1774d87f24b5", + "last_modified": 1480349201425 + }, + { + "guid": "{EEF73632-A085-4fd3-A778-ECD82C8CB297}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i165", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=806451", + "who": "All Firefox users who have these add-ons installed.", + "why": "These are malicious add-ons that are distributed with a trojan and negatively affect web browsing.", + "name": "Codec-M (malware)", + "created": "2012-10-29T16:41:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e5ecd02a-20ee-749b-d5cf-3d74d1173a1f", + "last_modified": 1480349201262 + }, + { + "guid": "firefox-extension@mozilla.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i688", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1049533", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that hides itself under the name Java_plugin, among others.", + "name": "FinFisher (malware)", + "created": "2014-08-06T17:13:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "98aca74a-69c7-9960-cccc-096a4a4adc6c", + "last_modified": 1480349201235 + }, + { + "guid": "{e44a1809-4d10-4ab8-b343-3326b64c7cdd}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i451", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=916966", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines, manipulating settings without reverting them on removal. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Entrusted", + "created": "2013-09-18T16:33:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ad5f53ed-7a43-cb1f-cbd7-41808fac1791", + "last_modified": 1480349201128 + }, + { + "guid": "{21EAF666-26B3-4A3C-ABD0-CA2F5A326744}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i620", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024752", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is probably silently installed, and is causing significant stability issues for users, in violation of the Add-on Guidelines.", + "name": "V-Bates", + "created": "2014-06-12T15:27:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2d8833db-01a7-a758-080f-19e47abc54cb", + "last_modified": 1480349201096 + }, + { + "guid": "{1FD91A9C-410C-4090-BBCC-55D3450EF433}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i338", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=844979", + "who": "All Firefox users who have this add-on installed.", + "why": "This extension overrides search settings, and monitors any further changes done to them so that they can be reverted. This violates our add-on guidelines.", + "name": "DataMngr (malware)", + "created": "2013-04-24T11:30:28Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "2e35995f-bec6-aa2b-3372-346d3325f72e", + "last_modified": 1480349201059 + }, + { + "guid": "9598582LLKmjasieijkaslesae@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i996", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1201165", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that takes over Facebook accounts.", + "name": "Secure Player (malware)", + "created": "2015-09-07T13:50:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "52f9c6e7-f7d5-f52e-cc35-eb99ef8b4b6a", + "last_modified": 1480349201029 + }, + { + "guid": "{bf7380fa-e3b4-4db2-af3e-9d8783a45bfc}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i406", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=776404", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on changes search settings without user interaction, and fails to reset them after it is removed. This violates our Add-on Guidelines.", + "name": "uTorrentBar", + "created": "2013-06-27T10:46:53Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3bcefc4b-110c-f3b8-17ad-f9fc97c1120a", + "last_modified": 1480349201000 + }, + { + "guid": "{424b0d11-e7fe-4a04-b7df-8f2c77f58aaf}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i800", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080839", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and is considered malware, in violation of the Add-on Guidelines.", + "name": "Astromenda NT", + "created": "2014-12-15T10:51:56Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "07bdf6aa-cfc8-ed21-6b36-6f90af02b169", + "last_modified": 1480349200939 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i618", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.\r\n", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.\r\n", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:25:04Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.31.*", + "minVersion": "3.15.31", + "targetApplication": [] + } + ], + "id": "825feb43-d6c2-7911-4189-6f589f612c34", + "last_modified": 1480349200911 + }, + { + "guid": "{167d9323-f7cc-48f5-948a-6f012831a69f}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i262", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=812303", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently side-installed by other software, and doesn't do much more than changing the users' settings, without reverting them on removal.", + "name": "WhiteSmoke (malware)", + "created": "2013-01-29T13:33:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a8f249fe-3db8-64b8-da89-7b584337a7af", + "last_modified": 1480349200885 + }, + { + "guid": "/^({988919ff-0cd8-4d0c-bc7e-60d55a49eb64}|{494b9726-9084-415c-a499-68c07e187244}|{55b95864-3251-45e9-bb30-1a82589aaff1}|{eef3855c-fc2d-41e6-8d91-d368f51b3055}|{90a1b331-c2b4-4933-9f63-ba7b84d60d58}|{d2cf9842-af95-48cd-b873-bfbb48cd7f5e})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i541", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963819", + "who": "All Firefox users who have this add-on installed", + "why": "This add-on has been repeatedly blocked before and keeps showing up with new add-on IDs, in violation of the Add-on Guidelines.", + "name": "MixiDJ (malware)", + "created": "2014-01-28T14:09:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "36196aed-9d0d-ebee-adf1-d1f7fadbc48f", + "last_modified": 1480349200819 + }, + { + "guid": "{29b136c9-938d-4d3d-8df8-d649d9b74d02}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i598", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1011322", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed, in violation with our Add-on Guidelines.", + "name": "Mega Browse", + "created": "2014-06-12T13:21:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "63b1c965-27c3-cd06-1b76-8721add39edf", + "last_modified": 1480349200775 + }, + { + "guid": "{6e7f6f9f-8ce6-4611-add2-05f0f7049ee6}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i868", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1086574", + "who": "All users who have this add-on installed. Users who wish to continue using the add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of our Add-on Guidelines.", + "name": "Word Proser", + "created": "2015-02-26T14:58:59Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f54797da-cdcd-351a-c95e-874b64b0d226", + "last_modified": 1480349200690 + }, + { + "guid": "{02edb56b-9b33-435b-b7df-b2843273a694}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i438", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=896581", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines. It is installed bypassing the Firefox opt-in screen, and manipulates settings without reverting them on removal. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "KeyBar Toolbar", + "created": "2013-08-09T15:27:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "896710d2-5a65-e9b0-845b-05aa72c2bd51", + "last_modified": 1480349200338 + }, + { + "guid": "{1cdbda58-45f8-4d91-b566-8edce18f8d0a}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i724", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080835", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Website Counselor Pro", + "created": "2014-10-13T16:00:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "7b70bd36-d2f7-26fa-9038-8b8dd132cd81", + "last_modified": 1480349200288 + }, + { + "guid": "{b12785f5-d8d0-4530-a3ea-5c4263b85bef}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i988", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1161573", + "who": "All users who have this add-on installed. Those who wish continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on overrides user's preferences without consent, in violation of the Add-on Guidelines.", + "name": "Hero Fighter Community Toolbar", + "created": "2015-08-17T16:04:35Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3e6d73f2-e8e3-af69-866e-30d3977b09e4", + "last_modified": 1480349200171 + }, + { + "guid": "{c2d64ff7-0ab8-4263-89c9-ea3b0f8f050c}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i39", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=665775", + "who": "Users of MediaBar versions 4.3.1.00 and below in all versions of Firefox.", + "why": "This add-on causes a high volume of crashes and is incompatible with certain versions of Firefox.", + "name": "MediaBar", + "created": "2011-07-19T10:18:12Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "4.3.1.00", + "minVersion": "0.1", + "targetApplication": [] + } + ], + "id": "e928a115-9d8e-86a4-e2c7-de39627bd9bf", + "last_modified": 1480349200047 + }, + { + "guid": "{9edd0ea8-2819-47c2-8320-b007d5996f8a}", + "prefs": [ + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i684", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1033857", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is believed to be silently installed in Firefox, in violation of the Add-on Guidelines.", + "name": "webget", + "created": "2014-08-06T13:33:33Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d38561f5-370f-14be-1443-a74dad29b1f3", + "last_modified": 1480349199962 + }, + { + "guid": "/^({ad9a41d2-9a49-4fa6-a79e-71a0785364c8})|(ffxtlbr@mysearchdial\\.com)$/", + "prefs": [ + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i670", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1036740", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on has been repeatedly been silently installed into users' systems, and is known for changing the default search without user consent, in violation of the Add-on Guidelines.", + "name": "MySearchDial", + "created": "2014-07-18T15:47:35Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a04075e6-5df2-2e1f-85a6-3a0171247349", + "last_modified": 1480349199927 + }, + { + "guid": "/^({1f43c8af-e9e4-4e5a-b77a-f51c7a916324}|{3a3bd700-322e-440a-8a6a-37243d5c7f92}|{6a5b9fc2-733a-4964-a96a-958dd3f3878e}|{7b5d6334-8bc7-4bca-a13e-ff218d5a3f17}|{b87bca5b-2b5d-4ae8-ad53-997aa2e238d4}|{bf8e032b-150f-4656-8f2d-6b5c4a646e0d})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1136", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1251940", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hides itself from view and disables various security features in Firefox.", + "name": "Watcher (malware)", + "created": "2016-03-04T17:56:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a2d0378f-ebe4-678c-62d8-2e4c6a613c17", + "last_modified": 1480349199818 + }, + { + "guid": "liiros@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i814", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1119657", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems without their consent and performs unwanted operations.", + "name": "One Tab (malware)", + "created": "2015-01-09T12:49:05Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "387c054d-cc9f-7ebd-c814-b4c1fbcb2880", + "last_modified": 1480349199791 + }, + { + "guid": "{97E22097-9A2F-45b1-8DAF-36AD648C7EF4}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i916", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1170633", + "who": "All Firefox users who have this add-on installed in Firefox 39 and above.\r\n", + "why": "Certain versions of this extension are causing startup crashes in Firefox 39 and above.\r\n", + "name": "RealPlayer Browser Record Plugin", + "created": "2015-06-02T09:57:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "39.0a1" + } + ] + } + ], + "id": "94fba774-c4e6-046a-bc7d-ede787a9d0fe", + "last_modified": 1480349199738 + }, + { + "guid": "{b64982b1-d112-42b5-b1e4-d3867c4533f8}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i167", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=805973", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is a frequent cause for browser crashes and other problems.", + "name": "Browser Manager", + "created": "2012-10-29T17:17:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "00bbe501-2d27-7a1c-c344-6eea1c707473", + "last_modified": 1480349199673 + }, + { + "guid": "{58bd07eb-0ee0-4df0-8121-dc9b693373df}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i286", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=842206", + "who": "All Firefox users who have this extension installed.", + "why": "This extension is malicious and is installed under false pretenses, causing problems for many Firefox users. Note that this is not the same BrowserProtect extension that is listed on our add-ons site. That one is safe to use.", + "name": "Browser Protect / bProtector (malware)", + "created": "2013-02-18T10:54:28Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b40a60d3-b9eb-09eb-bb02-d50b27aaac9f", + "last_modified": 1480349199619 + }, + { + "guid": "trtv3@trtv.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i465", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=845610", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on doesn't follow our Add-on Guidelines, bypassing our third party install opt-in screen. Users who wish to continue using this extension can enable it in the Add-ons Manager.", + "name": "TornTV", + "created": "2013-11-01T15:21:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3d4d8a33-2eff-2556-c699-9be0841a8cd4", + "last_modified": 1480349199560 + }, + { + "guid": "{d2cf9842-af95-48cd-b873-bfbb48cd7f5e}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i439", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=902569", + "who": "All Firefox users who have this add-on installed.", + "why": "This is another instance of the previously blocked Mixi DJ add-on, which doesn't follow our Add-on Guidelines. If you wish to continue using it, it can be enabled in the Add-ons Manager.", + "name": "Mixi DJ V45", + "created": "2013-08-09T16:08:18Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e81c31fc-265e-61b9-d4c1-0e2f31f1652e", + "last_modified": 1480349199478 + }, + { + "guid": "/^({b95faac1-a3d7-4d69-8943-ddd5a487d966}|{ecce0073-a837-45a2-95b9-600420505f7e}|{2713b394-286f-4d7c-89ea-4174eeab9f5a}|{da7a20cf-bef4-4342-ad78-0240fdf87055})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i624", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947482", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is known to change user settings without their consent, is distributed under multiple add-on IDs, and is also correlated with reports of tab functions being broken in Firefox, in violation of the Add-on Guidelines.\r\n", + "name": "WiseConvert", + "created": "2014-06-18T13:50:38Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ed57d7a6-5996-c7da-8e07-1ad125183e84", + "last_modified": 1480349199446 + }, + { + "guid": "{f894a29a-f065-40c3-bb19-da6057778493}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i742", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080817", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on appears to be silently installed into users' systems, and changes settings without consent, in violation of the Add-on Guidelines.", + "name": "Spigot Shopping Assistant", + "created": "2014-10-17T15:46:59Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "39d8334e-4b7c-4336-2d90-e6aa2d783967", + "last_modified": 1480349199083 + }, + { + "guid": "plugin@analytic-s.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i467", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935797", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on bypasses the external install opt in screen in Firefox, violating the Add-on Guidelines. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Analytics", + "created": "2013-11-07T14:08:48Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ffbed3f3-e5c9-bc6c-7530-f68f47b7efd6", + "last_modified": 1480349199026 + }, + { + "guid": "{C4A4F5A0-4B89-4392-AFAC-D58010E349AF}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i678", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=895668", + "who": "All Firefox users who have this add-on installed. If you wish to continue using it, you can enable it in the Add-ons Manager.", + "why": "This add-on is generally silently installed, in violation of the Add-on Guidelines.", + "name": "DataMngr", + "created": "2014-07-23T14:12:23Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "151021fc-ce4e-a734-e075-4ece19610f64", + "last_modified": 1480349198947 + }, + { + "guid": "HxLVJK1ioigz9WEWo8QgCs3evE7uW6LEExAniBGG@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1036", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1211170", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Mega Player (malware)", + "created": "2015-10-05T16:37:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "32e34b41-a73c-72d4-c96c-136917ad1d4d", + "last_modified": 1480349198894 + }, + { + "guid": "{6af08a71-380e-42dd-9312-0111d2bc0630}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i822", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1126353", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on appears to be malware, hiding itself in the Add-ons Manager, and keeping track of certain user actions.", + "name": "{6af08a71-380e-42dd-9312-0111d2bc0630} (malware)", + "created": "2015-01-27T09:50:40Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "96d0c12b-a6cf-4539-c1cf-a1c75c14ff24", + "last_modified": 1480349198826 + }, + { + "guid": "colmer@yopmail.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i550", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=968445", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks Facebook accounts.", + "name": "Video Plugin Facebook (malware)", + "created": "2014-02-06T15:49:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c394d10b-384e-cbd0-f357-9c521715c373", + "last_modified": 1480349198744 + }, + { + "guid": "fplayer@adobe.flash", + "prefs": [], + "schema": 1480349193877, + "blockID": "i444", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=909433", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware disguised as the Flash Player plugin.", + "name": "Flash Player (malware)", + "created": "2013-08-26T14:49:48Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c6557989-1b59-72a9-da25-b816c4a4c723", + "last_modified": 1480349198667 + }, + { + "guid": "{6E19037A-12E3-4295-8915-ED48BC341614}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i24", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=615518", + "who": "Users of RelevantKnowledge version 1.3.328.4 and older in Firefox 4 and later.", + "why": "This add-on causes a high volume of Firefox crashes.", + "name": "comScore RelevantKnowledge", + "created": "2011-03-02T17:42:56Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.3.328.4", + "minVersion": "0.1", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "3.7a1pre" + } + ] + } + ], + "id": "7c189c5e-f95b-0aef-e9e3-8e879336503b", + "last_modified": 1480349198606 + }, + { + "guid": "crossriderapp4926@crossrider.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i91", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=754648", + "who": "All Firefox users who have installed this add-on.", + "why": "Versions of this add-on prior to 0.81.44 automatically post message to users' walls and hide them from their view. Version 0.81.44 corrects this.", + "name": "Remove My Timeline (malware)", + "created": "2012-05-14T14:16:43Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "0.81.43", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5ee3e72e-96fb-c150-fc50-dd581e960963", + "last_modified": 1480349198547 + }, + { + "guid": "/^(93abedcf-8e3a-4d02-b761-d1441e437c09@243f129d-aee2-42c2-bcd1-48858e1c22fd\\.com|9acfc440-ac2d-417a-a64c-f6f14653b712@09f9a966-9258-4b12-af32-da29bdcc28c5\\.com|58ad0086-1cfb-48bb-8ad2-33a8905572bc@5715d2be-69b9-4930-8f7e-64bdeb961cfd\\.com)$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i544", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=949596", + "who": "All Firefox users who have this add-on installed. If you wish to continue using it, it can be enabled in the Add-ons Manager.", + "why": "This add-on is in violation of the Add-on Guidelines, using multiple add-on IDs and potentially doing other unwanted activities.", + "name": "SuperLyrics", + "created": "2014-01-30T11:51:19Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d8d25967-9814-3b65-0787-a0525c16e11e", + "last_modified": 1480349198510 + }, + { + "guid": "wHO@W9.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i980", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1192468", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "BestSavEFOrYoU (malware)", + "created": "2015-08-11T11:20:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4beb917f-68f2-1f91-beed-dff6d83006f8", + "last_modified": 1480349198483 + }, + { + "guid": "frhegnejkgner@grhjgewfewf.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1040", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1212451", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Async Codec (malware)", + "created": "2015-10-07T13:03:37Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "fb6ab4ce-5517-bd68-2cf7-a93a109a528a", + "last_modified": 1480349198458 + }, + { + "guid": "firefox@luckyleap.net", + "prefs": [], + "schema": 1480349193877, + "blockID": "i471", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935779", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is part of a malicious Firefox installer bundle.", + "name": "Installer bundle (malware)", + "created": "2013-11-07T15:38:33Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3a9e04c7-5e64-6297-8442-2816915aad77", + "last_modified": 1480349198433 + }, + { + "guid": "lugcla21@gmail.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i432", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=902072", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on includes malicious code that spams users' Facebook accounts with unwanted messages.", + "name": "FB Color Changer (malware)", + "created": "2013-08-06T13:16:22Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b6943f35-9429-1f8e-bf8e-fe37979fe183", + "last_modified": 1480349198372 + }, + { + "guid": "{99079a25-328f-4bd4-be04-00955acaa0a7}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i402", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=835678", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This group of add-ons is silently installed, bypassing our install opt-in screen. This violates our Add-on Guidelines.", + "name": "Searchqu", + "created": "2013-06-25T15:16:17Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "16008331-8b47-57c8-a6f7-989914d1cb8a", + "last_modified": 1480349198341 + }, + { + "guid": "{81b13b5d-fba1-49fd-9a6b-189483ac548a}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i473", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935779", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is part of a malicious Firefox installer bundle.", + "name": "Installer bundle (malware)", + "created": "2013-11-07T15:38:43Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "76debc7b-b875-6da4-4342-1243cbe437f6", + "last_modified": 1480349198317 + }, + { + "guid": "{e935dd68-f90d-46a6-b89e-c4657534b353}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i732", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Sites Pro", + "created": "2014-10-16T16:38:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "97fdc235-ac1a-9f20-1b4a-17c2f0d89ad1", + "last_modified": 1480349198260 + }, + { + "guid": "{32da2f20-827d-40aa-a3b4-2fc4a294352e}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i748", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963787", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "Start Page", + "created": "2014-10-17T16:02:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6c980c8e-4a3c-7912-4a3a-80add457575a", + "last_modified": 1480349198223 + }, + { + "guid": "chinaescapeone@facebook.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i431", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=901770", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that uses a deceptive name and hijacks social networks.", + "name": "F-Secure Security Pack (malware)", + "created": "2013-08-05T16:43:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "fbd89a9d-9c98-8481-e4cf-93e327ca8be1", + "last_modified": 1480349198192 + }, + { + "guid": "{cc6cc772-f121-49e0-b1f0-c26583cb0c5e}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i716", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Website Counselor", + "created": "2014-10-02T12:12:34Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "debcd28c-884b-ca42-d983-6fabf91034dd", + "last_modified": 1480349198148 + }, + { + "guid": "{906000a4-88d9-4d52-b209-7a772970d91f}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i474", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=935779", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is part of a malicious Firefox installer bundle.", + "name": "Installer bundle (malware)", + "created": "2013-11-07T15:38:48Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "326d05b9-ace7-67c6-b094-aad926c185a5", + "last_modified": 1480349197744 + }, + { + "guid": "{AB2CE124-6272-4b12-94A9-7303C7397BD1}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i20", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=627278", + "who": "Users of Skype extension versions below 5.2.0.7165 for all versions of Firefox.", + "why": "This add-on causes a high volume of Firefox crashes and introduces severe performance issues. Please update to the latest version. For more information, please read our announcement.", + "name": "Skype extension", + "created": "2011-01-20T18:39:25Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "5.2.0.7164", + "minVersion": "0.1", + "targetApplication": [] + } + ], + "id": "60e16015-1803-197a-3241-484aa961d18f", + "last_modified": 1480349197667 + }, + { + "guid": "f6682b47-e12f-400b-9bc0-43b3ccae69d1@39d6f481-b198-4349-9ebe-9a93a86f9267.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i682", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1043017", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is being silently installed, in violation of the Add-on Guidelines.", + "name": "enformation", + "created": "2014-08-04T16:07:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a7ae65cd-0869-67e8-02f8-6d22c56a83d4", + "last_modified": 1480349197636 + }, + { + "guid": "rally_toolbar_ff@bulletmedia.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i537", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=950267", + "who": "All Firefox users who have this extension installed. If you want to continue using it, you can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by silently installing it.", + "name": "Rally Toolbar", + "created": "2014-01-23T15:51:48Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4ac6eb63-b51a-3296-5b02-bae77f424032", + "last_modified": 1480349197604 + }, + { + "guid": "x77IjS@xU.net", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i774", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1076771", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed and changes user settings without consent, in violation of the Add-on Guidelines\r\n", + "name": "YoutubeAdBlocke", + "created": "2014-10-31T16:22:53Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "4771da14-bcf2-19b1-3d71-bc61a1c7d457", + "last_modified": 1480349197578 + }, + { + "guid": "{49c53dce-afa0-49a1-a08b-2eb8e8444128}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i441", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=844985", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed, violating our Add-on Guidelines. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "ytbyclick", + "created": "2013-08-09T16:58:50Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5f08d720-58c2-6acb-78ad-7af45c82c90b", + "last_modified": 1480349197550 + }, + { + "guid": "{bb7b7a60-f574-47c2-8a0b-4c56f2da9802}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i754", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080850", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "AdvanceElite", + "created": "2014-10-17T16:27:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f222ceb2-9b69-89d1-8dce-042d8131a12e", + "last_modified": 1480349197500 + }, + { + "guid": "/^((34qEOefiyYtRJT@IM5Munavn\\.com)|(Mro5Fm1Qgrmq7B@ByrE69VQfZvZdeg\\.com)|(KtoY3KGxrCe5ie@yITPUzbBtsHWeCdPmGe\\.com)|(9NgIdLK5Dq4ZMwmRo6zk@FNt2GCCLGyUuOD\\.com)|(NNux7bWWW@RBWyXdnl6VGls3WAwi\\.com)|(E3wI2n@PEHTuuNVu\\.com)|(2d3VuWrG6JHBXbQdbr@3BmSnQL\\.com))$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i324", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=841791", + "who": "All users who have this add-on installed.", + "why": "This extension is malware, installed pretending to be the Flash Player plugin.", + "name": "Flash Player (malware)", + "created": "2013-03-22T14:48:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5be3a399-af3e-644e-369d-628273b3fdc2", + "last_modified": 1480349197432 + }, + { + "guid": "{8f894ed3-0bf2-498e-a103-27ef6e88899f}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i792", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "ExtraW", + "created": "2014-11-26T13:49:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "bebc9e15-59a1-581d-0163-329d7414edff", + "last_modified": 1480349197368 + }, + { + "guid": "profsites@pr.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i734", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.\r\n", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "ProfSites", + "created": "2014-10-16T16:39:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "0d6d84d7-0b3f-c5ab-57cc-6b66b0775a23", + "last_modified": 1480349197341 + }, + { + "guid": "{872b5b88-9db5-4310-bdd0-ac189557e5f5}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i497", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=945530", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making settings changes that can't be easily reverted.", + "name": "DVDVideoSoft Menu", + "created": "2013-12-03T16:08:09Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e8da89c4-c585-77e4-9872-591d20723a7e", + "last_modified": 1480349197240 + }, + { + "guid": "123456789@offeringmedia.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i664", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1036757", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that attempts to hide itself by impersonating the Adobe Flash plugin.", + "name": "Taringa MP3 / Adobe Flash", + "created": "2014-07-10T15:41:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6d0a7dda-d92a-c8e2-21be-c92b0a88ac8d", + "last_modified": 1480349197208 + }, + { + "guid": "{df6bb2ec-333b-4267-8c4f-3f27dc8c6e07}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i487", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=940681", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Facebook 2013 (malware)", + "created": "2013-11-19T14:59:45Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5867c409-b342-121e-3c3b-426e2f0ba1d4", + "last_modified": 1480349197109 + }, + { + "guid": "/^({4e988b08-8c51-45c1-8d74-73e0c8724579}|{93ec97bf-fe43-4bca-a735-5c5d6a0a40c4}|{aed63b38-7428-4003-a052-ca6834d8bad3}|{0b5130a9-cc50-4ced-99d5-cda8cc12ae48}|{C4CFC0DE-134F-4466-B2A2-FF7C59A8BFAD})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i524", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947481", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted.", + "name": "SweetPacks", + "created": "2013-12-20T13:43:21Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1a3a26a2-cdaa-e5ba-f6ac-47b98ae2cc26", + "last_modified": 1480349197082 + }, + { + "guid": "{87b5a11e-3b54-42d2-9102-0a7cb1f79ebf}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i838", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128327", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and performs unwanted actions, in violation of the Add-on Guidelines.", + "name": "Cyti Web (malware)", + "created": "2015-02-06T14:29:12Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1ba0e57c-4c0c-4eb6-26e7-c2016769c343", + "last_modified": 1480349196965 + }, + { + "guid": "/^({bf67a47c-ea97-4caf-a5e3-feeba5331231}|{24a0cfe1-f479-4b19-b627-a96bf1ea3a56})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i542", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963819", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on has been repeatedly blocked before and keeps showing up with new add-on IDs, in violation of the Add-on Guidelines.", + "name": "MixiDJ (malware)", + "created": "2014-01-28T14:10:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "fc442b64-1b5d-bebb-c486-f431b154f3db", + "last_modified": 1480349196622 + }, + { + "guid": "/^({ebd898f8-fcf6-4694-bc3b-eabc7271eeb1}|{46008e0d-47ac-4daa-a02a-5eb69044431a}|{213c8ed6-1d78-4d8f-8729-25006aa86a76}|{fa23121f-ee7c-4bd8-8c06-123d087282c5}|{19803860-b306-423c-bbb5-f60a7d82cde5})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i622", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947482", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is known to change user settings without their consent, is distributed under multiple add-on IDs, and is also correlated with reports of tab functions being broken in Firefox, in violation of the Add-on Guidelines.", + "name": "WiseConvert", + "created": "2014-06-18T13:48:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "ffd184fa-aa8f-8a75-ff00-ce285dec5b22", + "last_modified": 1480349196597 + }, + { + "guid": "/^({fa95f577-07cb-4470-ac90-e843f5f83c52}|ffxtlbr@speedial\\.com)$/", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i696", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1031115", + "who": "All Firefox users who have any of these add-ons installed. Users who wish to continue using these add-ons can enable them in the Add-ons Manager.", + "why": "These add-ons are silently installed and change homepage and search defaults without user consent, in violation of the Add-on Guidelines. They are also distributed under more than one add-on ID.", + "name": "Speedial", + "created": "2014-08-21T13:55:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "130c7419-f727-a2fb-3891-627bc69a43bb", + "last_modified": 1480349196565 + }, + { + "guid": "pennerdu@faceobooks.ws", + "prefs": [], + "schema": 1480349193877, + "blockID": "i442", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=904050", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that hijacks Facebook accounts.", + "name": "Console Video (malware)", + "created": "2013-08-13T14:00:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "fb83e48e-a780-9d06-132c-9ecc65b43674", + "last_modified": 1480349196541 + }, + { + "guid": "{E90FA778-C2B7-41D0-9FA9-3FEC1CA54D66}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i446", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=788838", + "who": "All Firefox users who have this add-on installed. The add-on can be enabled again in the Add-ons Manager.", + "why": "This add-on is installed silently, in violation of our Add-on Guidelines.", + "name": "YouTube to MP3 Converter", + "created": "2013-09-06T15:59:29Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "83eb6337-a3b6-84e4-e76c-ee9200b80796", + "last_modified": 1480349196471 + }, + { + "guid": "{ad7ce998-a77b-4062-9ffb-1d0b7cb23183}", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i804", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1080839", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and is considered malware, in violation of the Add-on Guidelines.", + "name": "Astromenda Search Addon", + "created": "2014-12-15T10:53:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "633f9999-c81e-bd7a-e756-de7d34feb39d", + "last_modified": 1480349196438 + }, + { + "guid": "dodatek@flash2.pl", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1279", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1312748", + "who": "Any user with version 1.3 or newer of this add-on installed.", + "why": "This add-on claims to be a flash plugin and it does some work on youtube, but it also steals your facebook and adfly credentials and sends them to a remote server.", + "name": "Aktualizacja Flash WORK addon", + "created": "2016-10-27T15:52:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "1.3", + "targetApplication": [] + } + ], + "id": "2dab5211-f9ec-a1bf-c617-6f94f28b5ee1", + "last_modified": 1480349196331 + }, + { + "guid": "{2d069a16-fca1-4e81-81ea-5d5086dcbd0c}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i440", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=903647", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is installed silently and doesn't follow many other of the Add-on Guidelines. If you want to continue using this add-on, you can enable it in the Add-ons Manager.", + "name": "GlitterFun", + "created": "2013-08-09T16:26:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e3f77f3c-b1d6-3b29-730a-846007b9cb16", + "last_modified": 1480349196294 + }, + { + "guid": "xivars@aol.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i501", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=946420", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious Firefox extension that uses a deceptive name and hijacks users' Facebook accounts.", + "name": "Video Plugin Facebook (malware)", + "created": "2013-12-04T15:34:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3303d201-7006-3c0d-5fd5-45503e2e690c", + "last_modified": 1480349196247 + }, + { + "guid": "2bbadf1f-a5af-499f-9642-9942fcdb7c76@f05a14cc-8842-4eee-be17-744677a917ed.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i700", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1052599", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is widely considered malware and is apparently installed silently into users' systems, in violation of the Add-on Guidelines.", + "name": "PIX Image Viewer", + "created": "2014-08-21T16:15:16Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1b72889b-90e6-ea58-4fe8-d48257df7d8b", + "last_modified": 1480349196212 + }, + { + "guid": "toolbar@ask.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i600", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1024719", + "who": "All Firefox users who have these versions of the Ask Toolbar installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "Certain old versions of the Ask Toolbar are causing problems to users when trying to open new tabs. Using more recent versions of the Ask Toolbar should also fix this problem.", + "name": "Ask Toolbar (old Avira Security Toolbar bundle)", + "created": "2014-06-12T14:16:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "3.15.5.*", + "minVersion": "3.15.5", + "targetApplication": [] + } + ], + "id": "51c4ab3b-9ad3-c5c3-98c8-a220025fc5a3", + "last_modified": 1480349196158 + }, + { + "guid": "{729c9605-0626-4792-9584-4cbe65b243e6}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i788", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Browser Ext Assistance", + "created": "2014-11-20T10:07:19Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3c588238-2501-6a53-65ea-5c8ff0f3e51d", + "last_modified": 1480349196123 + }, + { + "guid": "webbooster@iminent.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i630", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=866943", + "who": "All Firefox users who have any of these add-ons installed. Users who wish to continue using them can enable them in the Add-ons Manager.", + "why": "These add-ons have been silently installed repeatedly, and change settings without user consent, in violation of the Add-on Guidelines.", + "name": "Iminent Minibar", + "created": "2014-06-26T15:49:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d894ea79-8215-7a0c-b0e9-be328c3afceb", + "last_modified": 1480349196032 + }, + { + "guid": "firefox@bandoo.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i23", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=629634", + "who": "Users of Bandoo version 5.0 for Firefox 3.6 and later.", + "why": "This add-on causes a high volume of Firefox crashes.", + "name": "Bandoo", + "created": "2011-03-01T23:30:23Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "5.0", + "minVersion": "5.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "3.7a1pre" + } + ] + } + ], + "id": "bd487cf4-3f6a-f956-a6e9-842ac8deeac5", + "last_modified": 1480349195915 + }, + { + "guid": "5nc3QHFgcb@r06Ws9gvNNVRfH.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i372", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=875752", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware pretending to be the Flash Player plugin.", + "name": "Flash Player 11 (malware)", + "created": "2013-06-18T13:23:40Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "dc71fcf5-fae4-5a5f-6455-ca7bbe4202db", + "last_modified": 1480349195887 + }, + { + "guid": "/^(7tG@zEb\\.net|ru@gfK0J\\.edu)$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i854", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=952255", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and performs unwanted actions, in violation of the Add-on Guidelines.", + "name": "youtubeadblocker (malware)", + "created": "2015-02-09T15:41:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "cfe42207-67a9-9b88-f80c-994e6bdd0c55", + "last_modified": 1480349195851 + }, + { + "guid": "{a7aae4f0-bc2e-a0dd-fb8d-68ce32c9261f}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i378", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=865090", + "who": "All Firefox users who have installed this add-on.", + "why": "This extension is malware that hijacks Facebook accounts for malicious purposes.", + "name": "Myanmar Extension for Facebook (malware)", + "created": "2013-06-18T15:58:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "30ecd9b9-4023-d9ef-812d-f1a75bb189b0", + "last_modified": 1480349195823 + }, + { + "guid": "crossriderapp5060@crossrider.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i228", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=810016", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently side-installed by other software, and it overrides user preferences and inserts advertisements in web content.", + "name": "Savings Sidekick", + "created": "2012-11-29T16:31:13Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "a37f76ac-7b77-b5a3-bac8-addaacf34bae", + "last_modified": 1480349195769 + }, + { + "guid": "/^(saamazon@mybrowserbar\\.com)|(saebay@mybrowserbar\\.com)$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i672", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1011337", + "who": "All Firefox users who have these add-ons installed. Users wishing to continue using these add-ons can enable them in the Add-ons Manager.", + "why": "These add-ons are being silently installed, in violation of the Add-on Guidelines.", + "name": "Spigot Shopping Assistant", + "created": "2014-07-22T15:13:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e072a461-ee5a-c83d-8d4e-5686eb585a15", + "last_modified": 1480349195347 + }, + { + "guid": "{b99c8534-7800-48fa-bd71-519a46cdc7e1}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i596", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1011325", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed, in violation with our Add-on Guidelines.", + "name": "BrowseMark", + "created": "2014-06-12T13:19:59Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f411bb0f-7c82-9061-4a80-cabc8ff45beb", + "last_modified": 1480349195319 + }, + { + "guid": "/^({94d62e35-4b43-494c-bf52-ba5935df36ef}|firefox@advanceelite\\.com|{bb7b7a60-f574-47c2-8a0b-4c56f2da9802})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i856", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1130323", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and performs unwanted actions, in violation of the Add-on Guidelines.", + "name": "AdvanceElite (malware)", + "created": "2015-02-09T15:51:11Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e3d52650-d3e2-4cef-71f7-e6188f56fe4d", + "last_modified": 1480349195286 + }, + { + "guid": "{458fb825-2370-4973-bf66-9d7142141847}", + "prefs": [ + "app.update.auto", + "app.update.enabled", + "app.update.interval", + "app.update.url" + ], + "schema": 1480349193877, + "blockID": "i1024", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1209588", + "who": "All users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on hides itself in the Add-ons Manager, interrupts the Firefox update process, and reportedly causes other problems to users, in violation of the Add-on Guidelines.", + "name": "Web Shield", + "created": "2015-09-29T09:25:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "32c5baa7-d547-eaab-302d-b873c83bfe2d", + "last_modified": 1480349195258 + }, + { + "guid": "{f2456568-e603-43db-8838-ffa7c4a685c7}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i778", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Sup-SW", + "created": "2014-11-07T13:53:13Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "93568fa2-0cb7-4e1d-e893-d7261e81547c", + "last_modified": 1480349195215 + }, + { + "guid": "{77BEC163-D389-42c1-91A4-C758846296A5}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i566", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=964594", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-on Manager.", + "why": "This add-on is silently installed into Firefox, in violation of the Add-on Guidelines.", + "name": "V-Bates", + "created": "2014-03-05T13:20:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "080edbac-25d6-e608-abdd-feb1ce7a9a77", + "last_modified": 1480349195185 + }, + { + "guid": "helper@vidscrab.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1077", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1231010", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using the add-on can enable it in the Add-ons Manager.", + "why": "This add-on injects remote scripts and injects unwanted content into web pages.", + "name": "YouTube Video Downloader (from AddonCrop)", + "created": "2016-01-14T14:32:53Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "36b2e1e0-5fda-bde3-db55-dfcbe24dfd04", + "last_modified": 1480349195157 + }, + { + "guid": "jid1-XLjasWL55iEE1Q@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i578", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1002037", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious add-on that presents itself as \"Flash Player\" but is really injecting unwanted content into Facebook pages.", + "name": "Flash Player (malware)", + "created": "2014-04-28T16:25:03Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "1e75b2f0-02fc-77a4-ad2f-52a4caff1a71", + "last_modified": 1480349195058 + }, + { + "guid": "{a3a5c777-f583-4fef-9380-ab4add1bc2a8}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i142", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=792132", + "who": "Todos los usuarios de Firefox que instalaron la versión 4.2 del complemento Cuevana Stream.\r\n\r\nAll Firefox users who have installed version 4.2 of the Cuevana Stream add-on.", + "why": "Español\r\nUna versión maliciosa del complemento Cuevana Stream (4.2) fue colocada en el sitio Cuevana y distribuida a muchos usuarios del sitio. Esta versión recopila información de formularios web y los envía a una dirección remota con fines maliciosos. Se le recomienda a todos los usuarios que instalaron esta versión que cambien sus contraseñas inmediatamente, y que se actualicen a la nueva versión segura, que es la 4.3.\r\n\r\nEnglish\r\nA malicious version of the Cuevana Stream add-on (4.2) was uploaded to the Cuevana website and distributed to many of its users. This version takes form data and sends it to a remote location with malicious intent. It is recommended that all users who installed this version to update their passwords immediately, and update to the new safe version, version 4.3.\r\n\r\n", + "name": "Cuevana Stream (malicious version)", + "created": "2012-09-18T13:37:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "4.2", + "minVersion": "4.2", + "targetApplication": [] + } + ], + "id": "91e551b9-7e94-60e2-f1bd-52f25844ab16", + "last_modified": 1480349195007 + }, + { + "guid": "{34712C68-7391-4c47-94F3-8F88D49AD632}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i922", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1173154", + "who": "All Firefox users who have this add-on installed in Firefox 39 and above.\r\n", + "why": "Certain versions of this extension are causing startup crashes in Firefox 39 and above.\r\n", + "name": "RealPlayer Browser Record Plugin", + "created": "2015-06-09T15:27:31Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "39.0a1" + } + ] + } + ], + "id": "dd350efb-34ac-2bb5-5afd-eed722dbb916", + "last_modified": 1480349194976 + }, + { + "guid": "PDVDZDW52397720@XDDWJXW57740856.com", + "prefs": [ + "browser.startup.homepage", + "browser.search.defaultenginename" + ], + "schema": 1480349193877, + "blockID": "i846", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128320", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and attempts to change user settings like the home page and default search, in violation of the Add-on Guidelines.", + "name": "Ge-Force", + "created": "2015-02-06T15:03:39Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "c33e950c-c977-ed89-c86a-3be8c4be1967", + "last_modified": 1480349194949 + }, + { + "guid": "discoverypro@discoverypro.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i582", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1004231", + "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enabled it in the Add-ons Manager.", + "why": "This add-on is silently installed by the CNET installer for MP3 Rocket and probably other software packages. This is in violation of the Add-on Guidelines.", + "name": "Website Discovery Pro", + "created": "2014-04-30T16:10:03Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "34eab242-6fbc-a459-a89e-0dc1a0b8355d", + "last_modified": 1480349194878 + }, + { + "guid": "jid1-bKSXgRwy1UQeRA@jetpack", + "prefs": [], + "schema": 1480349193877, + "blockID": "i680", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=979856", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into user's systems, in violation of the Add-on Guidelines.", + "name": "Trusted Shopper", + "created": "2014-08-01T16:34:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f701b790-b266-c69d-0fba-f2d189cb0f34", + "last_modified": 1480349194851 + }, + { + "guid": "bcVX5@nQm9l.org", + "prefs": [], + "schema": 1480349193877, + "blockID": "i848", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128266", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and performs unwanted actions, in violation of the Add-on Guidelines.", + "name": "boomdeal", + "created": "2015-02-09T15:21:17Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f8d6d4e1-b9e6-07f5-2b49-192106a45d82", + "last_modified": 1480349194799 + }, + { + "guid": "aytac@abc.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i504", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947341", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious extension that hijacks users' Facebook accounts.", + "name": "Facebook Haber (malware)", + "created": "2013-12-06T12:07:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "bfaf8298-dd69-165c-e1ed-ad55584abd18", + "last_modified": 1480349194724 + }, + { + "guid": "Adobe@flash.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i136", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=790100", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware posing as a legitimate Adobe product.", + "name": "Adobe Flash (malware)", + "created": "2012-09-10T16:09:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "47ac744e-3176-5cb6-1d02-b460e0c7ada0", + "last_modified": 1480349194647 + }, + { + "guid": "{515b2424-5911-40bd-8a2c-bdb20286d8f5}", + "prefs": [], + "schema": 1480349193877, + "blockID": "i491", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=940753", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted.", + "name": "Connect DLC", + "created": "2013-11-29T14:52:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "6d658443-b34a-67ad-934e-cbf7cd407460", + "last_modified": 1480349194580 + }, + { + "guid": "/^({3f3cddf8-f74d-430c-bd19-d2c9147aed3d}|{515b2424-5911-40bd-8a2c-bdb20286d8f5}|{17464f93-137e-4646-a0c6-0dc13faf0113}|{d1b5aad5-d1ae-4b20-88b1-feeaeb4c1ebc}|{aad50c91-b136-49d9-8b30-0e8d3ead63d0})$/", + "prefs": [], + "schema": 1480349193877, + "blockID": "i516", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947478", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by making changes that can't be easily reverted and being distributed under multiple add-on IDs.", + "name": "Connect DLC", + "created": "2013-12-20T12:38:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "96f8e157-8b8b-8e2e-76cd-6850599b4370", + "last_modified": 1480349194521 + }, + { + "guid": "wxtui502n2xce9j@no14", + "prefs": [], + "schema": 1480349193877, + "blockID": "i1012", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1206157", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that takes over Facebook accounts.", + "name": "Video fix (malware)", + "created": "2015-09-21T13:04:09Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "246798ac-25fa-f4a4-258c-a71f9f6ae091", + "last_modified": 1480349194463 + }, + { + "guid": "flashX@adobe.com", + "prefs": [], + "schema": 1480349193877, + "blockID": "i168", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=807052", + "who": "All Firefox users who have this add-on installed.", + "why": "This is an exploit proof-of-concept created for a conference presentation, which will probably be copied and modified for malicious purposes. \r\n", + "name": "Zombie Browser Pack", + "created": "2012-10-30T12:07:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d7c69812-801c-8d8e-12cb-c5171bdc48a1", + "last_modified": 1480349194428 + }, + { + "guid": "{4889ddce-7a83-45e6-afc9-1e4f1149fff4}", + "prefs": [], + "schema": 1480343836083, + "blockID": "i840", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1128327", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed and performs unwanted actions, in violation of the Add-on Guidelines.", + "name": "Cyti Web (malware)", + "created": "2015-02-06T14:30:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "be600f35-0633-29f3-c571-819e19d85db9", + "last_modified": 1480349193867 + }, + { + "guid": "{55dce8ba-9dec-4013-937e-adbf9317d990", + "prefs": [], + "schema": 1480343836083, + "blockID": "i690", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1048647", + "who": "All Firefox users. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is being silently installed in users' systems, in violation of the Add-on Guidelines.", + "name": "Deal Keeper", + "created": "2014-08-12T16:23:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "512b0d40-a10a-5ddc-963b-b9c487eb1422", + "last_modified": 1480349193833 + }, + { + "guid": "/^new@kuot\\.pro|{13ec6687-0b15-4f01-a5a0-7a891c18e4ee}|rebeccahoppkins(ty(tr)?)?@gmail\\.com|{501815af-725e-45be-b0f2-8f36f5617afc}|{9bdb5f1f-b1e1-4a75-be31-bdcaace20a99}|{e9d93e1d-792f-4f95-b738-7adb0e853b7b}|dojadewaskurwa@gmail\\.com$/", + "prefs": [], + "schema": 1480343836083, + "blockID": "i1414", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1312748", + "who": "All users who have this add-on installed.", + "why": "This add-on claims to be a flash plugin and it does some work on youtube, but it also steals your facebook and adfly credentials and sends them to a remote server.", + "name": "Aktualizacja dodatku Flash (malware)", + "created": "2016-10-28T18:06:03Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "5cebc983-bc88-d5f8-6807-bd1cbfcd82fd", + "last_modified": 1480349193798 + }, + { + "guid": "{58d2a791-6199-482f-a9aa-9b725ec61362}", + "prefs": [], + "schema": 1480343836083, + "blockID": "i746", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=963787", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' systems, in violation of the Add-on Guidelines.", + "name": "Start Page", + "created": "2014-10-17T16:01:53Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "8ebbc7d0-635c-b74a-de9f-16eb5837b36a", + "last_modified": 1480349193730 + }, + { + "guid": "{94cd2cc3-083f-49ba-a218-4cda4b4829fd}", + "prefs": [], + "schema": 1480343836083, + "blockID": "i590", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1013678", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is silently installed into users' profiles, in violation of the Add-on Guidelines.", + "name": "Value Apps", + "created": "2014-06-03T16:12:50Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "556b8d4d-d6c2-199d-9f33-8eccca07e8e7", + "last_modified": 1480349193649 + }, + { + "guid": "contentarget@maildrop.cc", + "prefs": [], + "schema": 1480343836083, + "blockID": "i818", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1119971", + "who": "All Firefox users who have this add-on installed.", + "why": "This is a malicious extension that hijacks Facebook accounts.", + "name": "Astro Play (malware)", + "created": "2015-01-12T09:29:19Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "440e9923-027a-6089-e036-2f78937dc193", + "last_modified": 1480349193622 + }, + { + "guid": "noOpus@outlook.com", + "prefs": [], + "schema": 1480343836083, + "blockID": "i816", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1119659", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems without their consent and performs unwanted operations.", + "name": "Full Screen (malware)", + "created": "2015-01-09T12:52:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b64d7cef-8b6c-2575-16bc-732fca7db377", + "last_modified": 1480349193537 + }, + { + "guid": "{c95a4e8e-816d-4655-8c79-d736da1adb6d}", + "prefs": [], + "schema": 1480343836083, + "blockID": "i433", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=844945", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on bypasses the external install opt in screen in Firefox, violating the Add-on Guidelines. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "name": "Hotspot Shield", + "created": "2013-08-09T11:25:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b3168278-a8ae-4882-7f26-355bc362bed0", + "last_modified": 1480349193510 + }, + { + "guid": "{9802047e-5a84-4da3-b103-c55995d147d1}", + "prefs": [], + "schema": 1480343836083, + "blockID": "i722", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1073810", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is silently installed into users' systems. It uses very unsafe practices to load its code, and leaks information of all web browsing activity. These are all violations of the Add-on Guidelines.", + "name": "Web Finder Pro", + "created": "2014-10-07T12:58:14Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "50097c29-26b1-bf45-ffe1-83da217eb127", + "last_modified": 1480349193482 + }, + { + "guid": "/^({bf9194c2-b86d-4ebc-9b53-1c08b6ff779e}|{61a83e16-7198-49c6-8874-3e4e8faeb4f3}|{f0af464e-5167-45cf-9cf0-66b396d1918c}|{5d9968c3-101c-4944-ba71-72d77393322d}|{01e86e69-a2f8-48a0-b068-83869bdba3d0})$/", + "prefs": [], + "schema": 1480343836083, + "blockID": "i515", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=947473", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using it can enable it in the Add-ons Manager.", + "why": "The installer that includes this add-on violates the Add-on Guidelines by using multiple add-on IDs and making unwanted settings changes.", + "name": "VisualBee Toolbar", + "created": "2013-12-20T12:26:49Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "029fa6f9-2351-40b7-5443-9a66e057f199", + "last_modified": 1480349193449 + }, + { + "guid": "/^({d50bfa5f-291d-48a8-909c-5f1a77b31948}|{d54bc985-6e7b-46cd-ad72-a4a266ad879e}|{d89e5de3-5543-4363-b320-a98cf150f86a}|{f3465017-6f51-4980-84a5-7bee2f961eba}|{fae25f38-ff55-46ea-888f-03b49aaf8812})$/", + "prefs": [], + "schema": 1480343836083, + "blockID": "i1137", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1251940", + "who": "All users who have this add-on installed.", + "why": "This is a malicious add-on that hides itself from view and disables various security features in Firefox.", + "name": "Watcher (malware)", + "created": "2016-03-04T17:56:42Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "252e18d0-85bc-7bb3-6197-5f126424c9b3", + "last_modified": 1480349193419 + }, + { + "guid": "ffxtlbr@claro.com", + "prefs": [], + "schema": 1480343836083, + "blockID": "i218", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=816762", + "who": "All Firefox users who have installed this add-on.", + "why": "The Claro Toolbar is side-installed with other software, unexpectedly changing users' settings and then making it impossible for these settings to be reverted by users.", + "name": "Claro Toolbar", + "created": "2012-11-29T16:07:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "e017a3b2-9b37-b8a0-21b0-bc412ae8a7f4", + "last_modified": 1480349193385 + }, + { + "guid": "/^(j003-lqgrmgpcekslhg|SupraSavings|j003-dkqonnnthqjnkq|j003-kaggrpmirxjpzh)@jetpack$/", + "prefs": [], + "schema": 1480343836083, + "blockID": "i692", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1048656", + "who": "All Firefox users who have this add-on installed. Users who wish to continue using this add-on can enable it in the Add-ons Manager.", + "why": "This add-on is being silently installed in users' systems, in violation of the Add-on Guidelines.", + "name": "SupraSavings", + "created": "2014-08-12T16:27:06Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "b0d30256-4581-1489-c241-d2e85b6c38f4", + "last_modified": 1480349193295 + }, + { + "guid": "helperbar@helperbar.com", + "prefs": [], + "schema": 1480343836083, + "blockID": "i258", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=817786", + "who": "All Firefox users who have this add-on installed. This only applies to version 1.0 of Snap.do. Version 1.1 fixed all the issues for which this block was created.", + "why": "This extension violates a number of our Add-on Guidelines, particularly on installation and settings handling. It also causes some stability problems in Firefox due to the way the toolbar is handled.\r\n\r\nUsers who wish to keep the add-on enabled can enable it again in the Add-ons Manager.", + "name": "Snap.do", + "created": "2013-01-28T13:52:26Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.0", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "f1ede5b8-7757-5ec5-d8ed-1a01889154aa", + "last_modified": 1480349193254 + }, + { + "guid": "/^((support2_en@adobe14\\.com)|(XN4Xgjw7n4@yUWgc\\.com)|(C7yFVpIP@WeolS3acxgS\\.com)|(Kbeu4h0z@yNb7QAz7jrYKiiTQ3\\.com)|(aWQzX@a6z4gWdPu8FF\\.com)|(CBSoqAJLYpCbjTP90@JoV0VMywCjsm75Y0toAd\\.com)|(zZ2jWZ1H22Jb5NdELHS@o0jQVWZkY1gx1\\.com))$/", + "prefs": [], + "schema": 1480343836083, + "blockID": "i326", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=841791", + "who": "All users who have this add-on installed.", + "why": "This extension is malware, installed pretending to be the Flash Player plugin.", + "name": "Flash Player (malware)", + "created": "2013-03-22T14:49:08Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "3142020b-8af9-1bac-60c5-ce5ad0ff3d42", + "last_modified": 1480349193166 + }, + { + "guid": "newmoz@facebook.com", + "prefs": [], + "schema": 1480343836083, + "blockID": "i576", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=997986", + "who": "All Firefox users who have this add-on installed.", + "why": "This add-on is malware that hijacks Facebook user accounts and sends spam on the user's behalf.", + "name": "Facebook Service Pack (malware)", + "created": "2014-04-22T14:34:42Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "d85798d3-9b87-5dd9-ace2-64914b93df77", + "last_modified": 1480349193114 + }, + { + "guid": "{0F827075-B026-42F3-885D-98981EE7B1AE}", + "prefs": [], + "schema": 1480343836083, + "blockID": "i334", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=862272", + "who": "All Firefox users who have this extension installed.", + "why": "This extension is malicious and is installed under false pretenses, causing problems for many Firefox users. Note that this is not the same BrowserProtect extension that is listed on our add-ons site. That one is safe to use.", + "name": "Browser Protect / bProtector (malware)", + "created": "2013-04-16T13:25:01Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 3, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [] + } + ], + "id": "aad4545f-8f9d-dd53-2aa8-e8945cad6185", + "last_modified": 1480349192987 + } + ], + "timestamp": 1604940558744 +} diff --git a/services/settings/dumps/blocklists/gfx.json b/services/settings/dumps/blocklists/gfx.json new file mode 100644 index 0000000000..5d2f0de672 --- /dev/null +++ b/services/settings/dumps/blocklists/gfx.json @@ -0,0 +1,1509 @@ +{ + "data": [ + { + "os": "Linux", + "schema": 1692576006114, + "vendor": "0x1002", + "details": { + "bug": "1831598", + "name": "AMD R600 Linux Webrender" + }, + "devices": [ + "0x9400", + "0x9401", + "0x9402", + "0x9403", + "0x9405", + "0x940a", + "0x940b", + "0x940f", + "0x94c0", + "0x94c1", + "0x94c3", + "0x94c4", + "0x94c5", + "0x94c6", + "0x94c7", + "0x94c8", + "0x94c9", + "0x94cb", + "0x94cc", + "0x94cd", + "0x9580", + "0x9581", + "0x9583", + "0x9586", + "0x9587", + "0x9588", + "0x9589", + "0x958a", + "0x958b", + "0x958c", + "0x958d", + "0x958e", + "0x958f", + "0x9500", + "0x9501", + "0x9504", + "0x9505", + "0x9506", + "0x9507", + "0x9508", + "0x9509", + "0x950f", + "0x9511", + "0x9515", + "0x9517", + "0x9519", + "0x95c0", + "0x95c2", + "0x95c4", + "0x95c5", + "0x95c6", + "0x95c7", + "0x95c9", + "0x95cc", + "0x95cd", + "0x95ce", + "0x95cf", + "0x9590", + "0x9591", + "0x9593", + "0x9595", + "0x9596", + "0x9597", + "0x9598", + "0x9599", + "0x959b", + "0x9610", + "0x9611", + "0x9612", + "0x9613", + "0x9614", + "0x9615", + "0x9616", + "0x9710", + "0x9711", + "0x9712", + "0x9713", + "0x9714", + "0x9715" + ], + "enabled": true, + "feature": "WEBRENDER", + "driverVendor": "", + "versionRange": { + "maxVersion": "", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "80366b54-4a61-4aa9-af07-15c43abe4684", + "last_modified": 1692730580117 + }, + { + "os": "Linux", + "schema": 1680017089172, + "vendor": "0x10de", + "details": { + "bug": "1824778", + "name": "NVIDIA driver stability issues" + }, + "devices": [], + "enabled": true, + "feature": "DMABUF", + "hardware": "", + "driverVendor": "nvidia/unknown", + "versionRange": { + "maxVersion": "111.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "78cc082a-221b-4353-8b07-57db1d03c871", + "last_modified": 1680018554787 + }, + { + "os": "Linux", + "schema": 1680017296058, + "vendor": "0x10de", + "details": { + "bug": "1824778", + "name": "NVIDIA driver stability issues" + }, + "devices": [], + "enabled": true, + "feature": "THREADSAFE_GL", + "hardware": "", + "driverVendor": "nvidia/unknown", + "versionRange": { + "maxVersion": "111.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "57d7fc9f-3e12-4b79-b28e-84abda7496af", + "last_modified": 1680018554776 + }, + { + "os": "Linux", + "schema": 1677861301308, + "vendor": "0x10de", + "details": { + "bug": "1788573", + "name": "NVIDIA driver stability issues" + }, + "devices": [], + "enabled": true, + "feature": "DMABUF", + "hardware": "", + "driverVendor": "non-mesa/all", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "530.30.2.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "LESS_THAN_OR_EQUAL", + "id": "5193a59e-7502-4462-8a56-5d918f8983eb", + "last_modified": 1677879347585 + }, + { + "os": "Linux", + "schema": 1677861725427, + "vendor": "0x10de", + "details": { + "bug": "1788573", + "name": "NVIDIA driver stability issues" + }, + "devices": [], + "enabled": true, + "feature": "THREADSAFE_GL", + "hardware": "", + "driverVendor": "non-mesa/all", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "530.30.2.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "LESS_THAN_OR_EQUAL", + "id": "382db832-b5ac-4d3d-831a-f7409ae82ce2", + "last_modified": 1677879347580 + }, + { + "os": "All", + "schema": 1677776405692, + "vendor": "0x1002", + "details": { + "bug": "1817269", + "name": "AMD video overlay bugs" + }, + "devices": [], + "enabled": true, + "feature": "VIDEO_OVERLAY", + "hardware": "", + "driverVendor": "", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "6fb52585-39e1-4ba0-8e35-9e2246b57a15", + "last_modified": 1677776905806 + }, + { + "os": "All", + "schema": 1677776209251, + "vendor": "0x10de", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1817617", + "name": "Block video overlay on Nvidia too" + }, + "devices": [], + "enabled": true, + "feature": "VIDEO_OVERLAY", + "hardware": "", + "driverVendor": "", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "0dee1a85-cdaa-4bf5-9300-cadfd4e2d1c6", + "last_modified": 1677776905802 + }, + { + "os": "Linux", + "schema": 1677775532727, + "vendor": "0x10de", + "details": { + "bug": "1817336", + "name": "Nouveau multithread race" + }, + "devices": [], + "enabled": true, + "feature": "ACCELERATED_CANVAS2D", + "hardware": "", + "driverVendor": "mesa/nouveau", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "b2a88f5c-4470-4441-9ffd-a5386a528d8b", + "last_modified": 1677776209179 + }, + { + "os": "Linux", + "schema": 1677775524274, + "vendor": "0x10de", + "details": { + "bug": "1777849", + "name": "GLX race caused by accelerated canvas with WebGL2 on Nvidia" + }, + "devices": [], + "enabled": true, + "feature": "ACCELERATED_CANVAS2D", + "hardware": "", + "driverVendor": "", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_OS_VERSION", + "windowProtocol": "x11/all", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "c0927c75-f1b0-4efc-9183-5861809858f0", + "last_modified": 1677776209175 + }, + { + "os": "Linux", + "schema": 1677775515298, + "vendor": "0x8086", + "details": { + "bug": "1777849", + "name": "GLX race caused by accelerated canvas with WebGL2 on Intel" + }, + "devices": [], + "enabled": true, + "feature": "ACCELERATED_CANVAS2D", + "hardware": "", + "driverVendor": "", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_OS_VERSION", + "windowProtocol": "x11/all", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "28ebdbf7-d638-481c-bb91-99c97ca4a5b2", + "last_modified": 1677776209169 + }, + { + "os": "Linux", + "schema": 1677775501360, + "vendor": "0x1002", + "details": { + "bug": "1777849", + "name": "GLX race caused by accelerated canvas with WebGL2" + }, + "devices": [], + "enabled": true, + "feature": "ACCELERATED_CANVAS2D", + "hardware": "", + "driverVendor": "", + "versionRange": { + "maxVersion": "110.*", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_OS_VERSION", + "windowProtocol": "x11/all", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "d8694183-9ab5-4f3a-a3e5-9da7b82c56da", + "last_modified": 1677776209165 + }, + { + "os": "Android", + "schema": 1643797519778, + "vendor": "Vivante Corporation", + "details": { + "bug": "1719327", + "name": "Vivante startup crashes" + }, + "devices": [ + "Vivante GC7000UL" + ], + "enabled": true, + "feature": "WEBRENDER", + "hardware": "", + "driverVendor": "", + "versionRange": { + "maxVersion": "97", + "minVersion": "" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "bbd241d5-e3b2-44eb-bb4d-69ae07f12a0e", + "last_modified": 1643818378440 + }, + { + "os": "All", + "schema": 1623194747864, + "vendor": "0x8086", + "details": { + "bug": "1715382", + "name": "Block D2D to avoid bug 1712047" + }, + "devices": [ + "0x0046", + "0x0042" + ], + "enabled": true, + "feature": "DIRECT2D", + "hardware": "gen5", + "driverVendor": "", + "versionRange": { + "maxVersion": "89.0", + "minVersion": "89.0" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "windowProtocol": "", + "driverVersionMax": "", + "desktopEnvironment": "", + "driverVersionComparator": "", + "id": "53585ac9-ab16-4201-a075-64730df7c752", + "last_modified": 1623195938349 + }, + { + "os": "All", + "schema": 1605987696433, + "vendor": "0x8086", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1678942", + "name": "sandybridge from WebRender in 83 on Windows" + }, + "devices": [ + "0x0102", + "0x0106", + "0x010a", + "0x0112", + "0x0116", + "0x0122", + "0x0126" + ], + "enabled": true, + "feature": "WEBRENDER", + "hardware": "", + "versionRange": { + "maxVersion": "83.*", + "minVersion": "83.0" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "driverVersionMax": "", + "driverVersionComparator": "", + "id": "6359e124-61ae-466f-9066-8f58f783b1ba", + "last_modified": 1606146402211 + }, + { + "os": "Darwin 20", + "schema": 1605878544498, + "vendor": "0x8086", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1678448", + "name": "sandybridge from WebRender in 83 on macOS" + }, + "devices": [ + "0x0102", + "0x0106", + "0x010a", + "0x0112", + "0x0116", + "0x0122", + "0x0126" + ], + "enabled": true, + "feature": "WEBRENDER", + "hardware": "", + "versionRange": { + "maxVersion": "83.*", + "minVersion": "83.0" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "driverVersionMax": "", + "driverVersionComparator": "", + "id": "f138fcc2-3477-4b1e-a0d3-97c7a99246cf", + "last_modified": 1605895309599 + }, + { + "os": "Darwin 19", + "schema": 1605885821224, + "vendor": "0x8086", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1678448", + "name": "sandybridge from WebRender in 83 on macOS" + }, + "devices": [ + "0x0102", + "0x0106", + "0x010a", + "0x0112", + "0x0116", + "0x0122", + "0x0126" + ], + "enabled": true, + "feature": "WEBRENDER", + "hardware": "", + "versionRange": { + "maxVersion": "83.*", + "minVersion": "83.0" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "driverVersionMax": "", + "id": "29815508-ba95-474b-ab55-e1669ece31bb", + "last_modified": 1605895309594 + }, + { + "os": "Darwin 18", + "schema": 1605887703589, + "vendor": "0x8086", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1678448", + "name": "sandybridge from WebRender in 83 on macOS" + }, + "devices": [ + "0x0102", + "0x0106", + "0x010a", + "0x0112", + "0x0116", + "0x0122", + "0x0126" + ], + "enabled": true, + "feature": "WEBRENDER", + "hardware": "", + "versionRange": { + "maxVersion": "83.*", + "minVersion": "83.0" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "driverVersionMax": "", + "driverVersionComparator": "", + "id": "84ab1546-f779-42cb-8692-4426d38a06ec", + "last_modified": 1605895309589 + }, + { + "os": "Darwin 17", + "schema": 1605887758025, + "vendor": "0x8086", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1678448", + "name": "sandybridge from WebRender in 83 on macOS" + }, + "devices": [ + "0x0102", + "0x0106", + "0x010a", + "0x0112", + "0x0116", + "0x0122", + "0x0126" + ], + "enabled": true, + "feature": "WEBRENDER", + "hardware": "", + "versionRange": { + "maxVersion": "83.*", + "minVersion": "83.0" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "driverVersionMax": "", + "driverVersionComparator": "", + "id": "bec686f1-428d-4979-931a-211974cd7f33", + "last_modified": 1605895309584 + }, + { + "os": "Darwin 16", + "schema": 1605887815692, + "vendor": "0x8086", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1678448", + "name": "sandybridge from WebRender in 83 on macOS" + }, + "devices": [ + "0x0102", + "0x0106", + "0x010a", + "0x0112", + "0x0116", + "0x0122", + "0x0126" + ], + "enabled": true, + "feature": "WEBRENDER", + "hardware": "", + "versionRange": { + "maxVersion": "83.*", + "minVersion": "83.0" + }, + "driverVersion": "", + "featureStatus": "BLOCKED_DEVICE", + "driverVersionMax": "", + "driverVersionComparator": "", + "id": "fb5e482f-a0b2-434f-a667-d01b1d7fdf77", + "last_modified": 1605895309578 + }, + { + "os": "Darwin 11", + "schema": 1480349134676, + "vendor": "0x10de", + "blockID": "g200", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=809550", + "who": "All Firefox users.", + "why": "Security problems.", + "name": "Mac OS X WebGL anti-aliasing", + "created": "2012-11-12T10:35:32Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "ae36a32e-5f4f-2f98-804f-22b0de5e7bfa", + "last_modified": 1480349135384 + }, + { + "os": "WINNT 6.0", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g1217", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1266220", + "who": ".", + "why": ".", + "name": "10.18.10.3947 winnt 6.0", + "created": "2016-05-23T15:43:58Z" + }, + "devices": [], + "enabled": true, + "feature": "HARDWARE_VIDEO_DECODING", + "driverVersion": "10.18.10.3947", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "a3fe9700-b9e6-1c31-f94b-030f72f4aa07", + "last_modified": 1480349135360 + }, + { + "os": "All", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g1124", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1170143", + "who": ".", + "why": ".", + "name": "Intel Driver 8.15.10.2086", + "created": "2016-02-22T23:40:14Z" + }, + "devices": [ + "0x2a42", + "0x2e22", + "0x2e12", + "0x2e32", + "0x0046" + ], + "enabled": true, + "driverVersion": "8.15.10.2086", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "851c8fcb-5763-bf89-813d-0f98cdd6f8c1", + "last_modified": 1480349135337 + }, + { + "os": "Darwin 12", + "schema": 1480349134676, + "vendor": "0x10de", + "blockID": "g202", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=809550", + "who": "All Firefox users.", + "why": "Security problems.", + "name": "Mac OS X WebGL anti-aliasing", + "created": "2012-11-12T10:37:07Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "a825cb5d-36f6-54b0-31b3-6668aae49cdd", + "last_modified": 1480349135314 + }, + { + "os": "Darwin 12", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g208", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=809550", + "who": "All Firefox users.", + "why": "Security problems", + "name": "Mac OS X WebGL anti-aliasing", + "created": "2012-11-12T10:42:34Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "a8e8aa1a-8e70-af51-065d-81ed31d23221", + "last_modified": 1480349135291 + }, + { + "os": "All", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g1073", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1207993", + "who": ".", + "why": ".", + "name": "Crash in igd10umd32.dll@0x18f35", + "created": "2015-12-21T16:01:46Z" + }, + "devices": [ + "0x2a42", + "0x2e22", + "0x2e12", + "0x2e32", + "0x0046" + ], + "enabled": true, + "driverVersion": "8.15.10.1994", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "13f75dff-c65f-eab1-4be5-864a79503ad5", + "last_modified": 1480349135267 + }, + { + "os": "WINNT 10.0", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g1221", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1266220", + "who": ".", + "why": ".", + "name": "10.18.10.3947 winnt 10.0", + "created": "2016-05-23T15:47:17Z" + }, + "devices": [], + "enabled": true, + "feature": "HARDWARE_VIDEO_DECODING", + "driverVersion": "10.18.10.3947", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "65ad7851-4af6-2a10-1d47-72943e63dcf0", + "last_modified": 1480349135245 + }, + { + "os": "WINNT 5.2", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g1216", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1266220", + "who": ".", + "why": ".", + "name": "10.18.10.3947 winnt 5.2", + "created": "2016-05-23T15:32:22Z" + }, + "devices": [], + "enabled": true, + "feature": "HARDWARE_VIDEO_DECODING", + "driverVersion": "10.18.10.3947", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "18968a19-7317-cc56-f9fc-f568f40abca4", + "last_modified": 1480349135215 + }, + { + "os": "WINNT 5.1", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g1215", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1266220", + "who": ".", + "why": ".", + "name": "10.18.10.3947 winnt 5.1", + "created": "2016-05-23T15:31:32Z" + }, + "devices": [], + "enabled": true, + "feature": "HARDWARE_VIDEO_DECODING", + "driverVersion": "10.18.10.3947", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "5c141970-5076-0af4-152f-efb3642a7b14", + "last_modified": 1480349135171 + }, + { + "os": "Darwin 12", + "schema": 1480349134676, + "vendor": "0x1002", + "blockID": "g234", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=815437", + "who": "All Firefox users.", + "why": "Security problems.", + "name": "Mac OS X WebGL anti-aliasing (AMD)", + "created": "2012-11-30T12:50:57Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "3310f8b8-1789-1b13-9326-5531943514ae", + "last_modified": 1480349135145 + }, + { + "os": "Darwin 10", + "schema": 1480349134676, + "vendor": "0x1002", + "blockID": "g230", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=815437", + "who": "All Firefox users.", + "why": "Security problems.", + "name": "Mac OS X WebGL anti-aliasing (AMD)", + "created": "2012-11-30T12:48:55Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "4769433c-51e9-a0f9-eef5-45829ac54243", + "last_modified": 1480349135120 + }, + { + "os": "WINNT 6.1", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g1218", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1266220", + "who": ".", + "why": ".", + "name": "10.18.10.3947 winnt 6.1", + "created": "2016-05-23T15:44:43Z" + }, + "devices": [], + "enabled": true, + "feature": "HARDWARE_VIDEO_DECODING", + "driverVersion": "10.18.10.3947", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "7a3ea538-8519-2845-53bf-6a4be0ff9e6b", + "last_modified": 1480349135096 + }, + { + "os": "Darwin 10", + "schema": 1480349134676, + "vendor": "0x10de", + "blockID": "g198", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=809550", + "who": "All Firefox users.", + "why": "Security problems.", + "name": "Mac OS X WebGL anti-aliasing", + "created": "2012-11-12T10:34:32Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "c9551a39-62fb-f652-8032-cbe9d2ce419c", + "last_modified": 1480349135072 + }, + { + "os": "WINNT 5.1", + "schema": 1480349134676, + "vendor": "0x8086", + "blockID": "g511", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=951422", + "who": "All Firefox users.", + "why": "Stability.", + "name": "Intel driver < 6.14.10.5216 for DIRECT3D_9_LAYERS on XP", + "created": "2013-12-18T14:29:36Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT3D_9_LAYERS", + "driverVersion": "6.14.10.5218", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN", + "id": "37ffcc44-0d03-681e-69bf-89c38111fcdd", + "last_modified": 1480349135048 + }, + { + "os": "WINNT 6.1", + "schema": 1480343737148, + "vendor": "0x10de", + "blockID": "g36", + "details": { + "bug": "", + "who": "", + "why": "", + "name": "", + "created": "2011-03-01T12:13:42Z" + }, + "devices": [ + "0x0a6c" + ], + "enabled": true, + "feature": "DIRECT3D_9_LAYERS", + "driverVersion": "8.17.12.5896", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN_OR_EQUAL", + "id": "6ac4f440-9661-ee18-248f-f3b7b6eb8deb", + "last_modified": 1480349134662 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1069", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1207993", + "who": ".", + "why": ".", + "name": "Crash in igd10umd32.dll@0x18f35", + "created": "2015-12-21T15:58:51Z" + }, + "devices": [ + "0x2a42", + "0x2e22", + "0x2e12", + "0x2e32", + "0x0046" + ], + "enabled": true, + "driverVersion": "8.15.10.1855", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "9928c898-4941-555d-e502-54a90e9e5371", + "last_modified": 1480349134638 + }, + { + "os": "WINNT 10.0", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g974", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1189266", + "who": "All users who have these cards on Windows 10.", + "why": "Crashes on Windows 10.", + "name": "ATI cards on Windows 10", + "created": "2015-08-11T10:30:37Z" + }, + "devices": [ + "0x6920", + "0x6921", + "0x6928", + "0x6929", + "0x692b", + "0x692f", + "0x6930", + "0x6938", + "0x6939", + "0x6900", + "0x6901", + "0x6902", + "0x6903", + "0x6907", + "0x7300", + "0x9870", + "0x9874", + "0x9875", + "0x9876", + "0x9877" + ], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "15.201.1151.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN", + "id": "ec9cf554-49de-e5ea-bbdc-54f84d290605", + "last_modified": 1480349134615 + }, + { + "os": "WINNT 6.1", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g280", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=840161", + "who": "All Firefox user who use this driver.", + "why": "Crashes.", + "name": "AMD Radeon HD 6290/6300/6310/6320 for Direct3D 9", + "created": "2013-02-13T14:52:05Z" + }, + "devices": [ + "0x9802", + "0x9803", + "0x9803", + "0x9804", + "0x9805", + "0x9806", + "0x9807" + ], + "enabled": true, + "feature": "DIRECT3D_9_LAYERS", + "featureStatus": "BLOCKED_DEVICE", + "id": "9b7a25d9-075e-e5e9-678e-af1292db7463", + "last_modified": 1480349134582 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g984", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1193802", + "who": "All users using these driver versions.", + "why": "Major graphics artifacts.", + "name": "Intel graphics block", + "created": "2015-08-14T16:21:45Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "8.15.10.2413", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN_OR_EQUAL", + "id": "f1830d7b-c807-afda-1b97-f41474157165", + "last_modified": 1480349134550 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1070", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1207993", + "who": ".", + "why": ".", + "name": "Crash in igd10umd32.dll@0x18f35", + "created": "2015-12-21T15:59:29Z" + }, + "devices": [ + "0x2a42", + "0x2e22", + "0x2e12", + "0x2e32", + "0x0046" + ], + "enabled": true, + "driverVersion": "8.15.10.1872", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "0f7b2114-976c-db1c-2d9a-02d47d1968e1", + "last_modified": 1480349134528 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x1022", + "blockID": "g148", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=793869", + "who": "All Firefox users who have these drivers installed.", + "why": "Some features in these drivers are causing frequent crashes in Firefox.", + "name": "ATI/AMD driver 8.982.0.0 (Direct3D)", + "created": "2012-09-25T14:31:35Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT3D_9_LAYERS", + "driverVersion": "8.982.0.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "85925d60-d04e-2cbd-cdee-de70ea6be4f4", + "last_modified": 1480349134505 + }, + { + "os": "WINNT 6.2", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g192", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=806991", + "who": "All Firefox users.", + "why": "Crashes.", + "name": "AMD DIRECT2D on Windows 8, [0x1002]", + "created": "2012-11-02T16:33:13Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "9.10.8.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN_OR_EQUAL", + "id": "5e34680b-5279-4ffc-7382-b1bc661b3094", + "last_modified": 1480349134483 + }, + { + "os": "WINNT 6.1", + "schema": 1480343737148, + "vendor": "0x10de", + "blockID": "g35", + "details": { + "bug": "", + "who": "", + "why": "", + "name": "", + "created": "2011-03-01T12:13:40Z" + }, + "devices": [ + "0x0a6c" + ], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "8.17.12.5896", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN_OR_EQUAL", + "id": "00a6b9d2-285f-83f0-0a1f-ef0205a60067", + "last_modified": 1480349134459 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1071", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1207993", + "who": ".", + "why": ".", + "name": "Crash in igd10umd32.dll@0x18f35", + "created": "2015-12-21T16:00:05Z" + }, + "devices": [ + "0x2a42", + "0x2e22", + "0x2e12", + "0x2e32", + "0x0046" + ], + "enabled": true, + "driverVersion": "8.15.10.1883", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "d4297784-dbac-906c-16b7-1a8792e868de", + "last_modified": 1480349134436 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g150", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=793869", + "who": "All Firefox users who have these drivers installed.", + "why": "Some features in these drivers are causing frequent crashes in Firefox.", + "name": "ATI/AMD driver 8.982.0.0 (Direct3D)", + "created": "2012-09-25T14:33:17Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT3D_9_LAYERS", + "driverVersion": "8.982.0.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "6a3c05bb-f971-d3d4-2a02-184e6970ba87", + "last_modified": 1480349134413 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g144", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=792480", + "who": "All Firefox users who have these drivers installed.", + "why": "Some features in these drivers are causing frequent crashes in Firefox.", + "name": "ATI/AMD driver 8.982.0.0", + "created": "2012-09-24T08:23:25Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "8.982.0.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "35e25a7c-d77d-6b43-fa5e-39b7e9c8a481", + "last_modified": 1480349134391 + }, + { + "os": "WINNT 5.1", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1251", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1283601", + "who": ".", + "why": ".", + "name": "Intel driver < 6.14.10.5216 for DIRECT3D_9_LAYERS on XP", + "created": "2016-07-22T12:30:30Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_ANGLE", + "versionRange": { + "maxVersion": "49.9" + }, + "driverVersion": "6.14.10.5218", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN", + "id": "ea27a4d2-ed88-0190-db12-473820b340b2", + "last_modified": 1480349134369 + }, + { + "os": "Darwin 11", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g206", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=809550", + "who": "All Firefox users.", + "why": "Security problems", + "name": "Mac OS X WebGL anti-aliasing", + "created": "2012-11-12T10:39:32Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "91cc2cec-11dd-0f77-ffad-f57e69b3f441", + "last_modified": 1480349134345 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1068", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1207993", + "who": ".", + "why": ".", + "name": "Crash in igd10umd32.dll@0x18f35", + "created": "2015-12-21T15:58:01Z" + }, + "devices": [ + "0x2a42", + "0x2e22", + "0x2e12", + "0x2e32", + "0x0046" + ], + "enabled": true, + "driverVersion": "8.15.10.1851", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "b7452893-25cc-497a-626c-aacee78683c6", + "last_modified": 1480349134323 + }, + { + "os": "WINNT 6.2", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1219", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1266220", + "who": ".", + "why": ".", + "name": "10.18.10.3947 winnt 6.2", + "created": "2016-05-23T15:45:21Z" + }, + "devices": [], + "enabled": true, + "feature": "HARDWARE_VIDEO_DECODING", + "driverVersion": "10.18.10.3947", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "73e4e12d-f27a-1113-b0e9-c05cb4dd78d2", + "last_modified": 1480349134301 + }, + { + "os": "WINNT 5.1", + "schema": 1480343737148, + "vendor": "0x10de", + "blockID": "g37", + "details": { + "bug": "", + "who": "", + "why": "", + "name": "", + "created": "2011-03-14T14:11:51Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT3D_9_LAYERS", + "driverVersion": "7.0.0.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "GREATER_THAN_OR_EQUAL", + "id": "fb507393-460e-685e-91de-1ce3b18ddbc0", + "last_modified": 1480349134278 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x1022", + "blockID": "g146", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=792480", + "who": "All Firefox users who have these drivers installed.", + "why": "Some features in these drivers are causing frequent crashes in Firefox.", + "name": "ATI/AMD driver 8.982.0.0", + "created": "2012-09-24T08:23:33Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "8.982.0.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "e6984930-7969-3cbc-fbaa-0f350afe6f14", + "last_modified": 1480349134255 + }, + { + "os": "WINNT 8.1", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g992", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1189266", + "who": "Users who have these cards installed.", + "why": "Crashing.", + "name": "ATI cards on Windows 10", + "created": "2015-08-20T09:56:09Z" + }, + "devices": [ + "0x6920", + "0x6921", + "0x6928", + "0x6929", + "0x692b", + "0x692f", + "0x6930", + "0x6938", + "0x6939", + "0x6900", + "0x6901", + "0x6902", + "0x6903", + "0x6907", + "0x7300", + "0x9870", + "0x9874", + "0x9875", + "0x9876", + "0x9877" + ], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "15.201.1151.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN", + "id": "77b27d80-1ebe-04de-2db9-bb197acbac32", + "last_modified": 1480349134232 + }, + { + "os": "Darwin 11", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g232", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=809550", + "who": "All Firefox users.", + "why": "Security problems.", + "name": "Mac OS X WebGL anti-aliasing (AMD)", + "created": "2012-11-30T12:49:56Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "8f8e9025-a9ec-5f1e-ae9f-aee28176bdf7", + "last_modified": 1480349134210 + }, + { + "os": "WINNT 6.3", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1220", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1266220", + "who": ".", + "why": ".", + "name": "10.18.10.3947 winnt 6.3", + "created": "2016-05-23T15:46:34Z" + }, + "devices": [], + "enabled": true, + "feature": "HARDWARE_VIDEO_DECODING", + "driverVersion": "10.18.10.3947", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "1f91fdd6-8b89-d948-d05a-86491c705ce2", + "last_modified": 1480349134187 + }, + { + "os": "Darwin 10", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g204", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=809550", + "who": "All Firefox users.", + "why": "Security problems.", + "name": "Mac OS X WebGL anti-aliasing", + "created": "2012-11-12T10:38:32Z" + }, + "devices": [], + "enabled": true, + "feature": "WEBGL_MSAA", + "featureStatus": "BLOCKED_DEVICE", + "id": "33866148-1d41-2d4c-27b8-5f147467686b", + "last_modified": 1480349134160 + }, + { + "os": "WINNT 6.1", + "schema": 1480343737148, + "vendor": "0x1002", + "blockID": "g278", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=840161", + "who": "All Firefox users using these drivers.", + "why": "Crashes", + "name": "AMD Radeon HD 6290/6300/6310/6320 for Direct2D", + "created": "2013-02-13T14:50:33Z" + }, + "devices": [ + "0x9802", + "0x9803", + "0x9803", + "0x9804", + "0x9805", + "0x9806", + "0x9807" + ], + "enabled": true, + "feature": "DIRECT2D", + "featureStatus": "BLOCKED_DEVICE", + "id": "bccdbab1-c335-6f2e-6d0a-d563ccb2f1f4", + "last_modified": 1480349134138 + }, + { + "os": "All", + "schema": 1480343737148, + "vendor": "0x8086", + "blockID": "g1072", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1207993", + "who": ".", + "why": ".", + "name": "Crash in igd10umd32.dll@0x18f35", + "created": "2015-12-21T16:00:50Z" + }, + "devices": [ + "0x2a42", + "0x2e22", + "0x2e12", + "0x2e32", + "0x0046" + ], + "enabled": true, + "driverVersion": "8.15.10.1892", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "EQUAL", + "id": "d780a857-b80d-0049-981e-1a8d3ebe485e", + "last_modified": 1480349134115 + }, + { + "os": "WINNT 6.2", + "schema": 1480343737148, + "vendor": "0x1022", + "blockID": "g194", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=806991", + "who": "All Firefox users.", + "why": "Crashes.", + "name": "AMD DIRECT2D on Windows 8, [0x1022]", + "created": "2012-11-02T16:35:37Z" + }, + "devices": [], + "enabled": true, + "feature": "DIRECT2D", + "driverVersion": "9.10.8.0", + "featureStatus": "BLOCKED_DRIVER_VERSION", + "driverVersionComparator": "LESS_THAN_OR_EQUAL", + "id": "0afc5fe9-1f81-7fde-2424-0cb4d933c173", + "last_modified": 1480349134090 + } + ], + "timestamp": 1692730580117 +} diff --git a/services/settings/dumps/blocklists/moz.build b/services/settings/dumps/blocklists/moz.build new file mode 100644 index 0000000000..8ace1d0541 --- /dev/null +++ b/services/settings/dumps/blocklists/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Blocklist Implementation") + +# The addons blocklist is also in mobile/android/installer/package-manifest.in +FINAL_TARGET_FILES.defaults.settings.blocklists += [ + "addons-bloomfilters.json", + "gfx.json", +] +if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": + FINAL_TARGET_FILES.defaults.settings.blocklists += ["plugins.json"] + +FINAL_TARGET_FILES.defaults.settings.blocklists["addons-bloomfilters"] += [ + "addons-bloomfilters/addons-mlbf.bin", + "addons-bloomfilters/addons-mlbf.bin.meta.json", +] + +if CONFIG["MOZ_BUILD_APP"] == "browser": + DIST_SUBDIR = "browser" diff --git a/services/settings/dumps/blocklists/plugins.json b/services/settings/dumps/blocklists/plugins.json new file mode 100644 index 0000000000..d9aff97acb --- /dev/null +++ b/services/settings/dumps/blocklists/plugins.json @@ -0,0 +1,3515 @@ +{ + "data": [ + { + "os": "Linux", + "schema": 1603099489080, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1671688", + "why": "Old versions of the Flash Player plugin have known vulnerabilities. All users are strongly recommended to check for updates on Adobe's Flash page.", + "name": "Flash Player Plugin on Linux 32.0.0.443 and older (click-to-play)" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/flashplayer/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "32.0.0.443", + "minVersion": "0", + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libflashplayer\\.so", + "id": "49b843cc-a8fc-4ede-be0c-a0da56d0214f", + "last_modified": 1603126502200 + }, + { + "schema": 1603050119276, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1671688", + "why": "Old versions of the Flash Player plugin have known vulnerabilities. All users are strongly recommended to check for updates on Adobe's Flash page.", + "name": "Flash Player Plugin 32.0.0.443 and older (click-to-play)" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/flashplayer/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "32.0.0.443", + "minVersion": "0", + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(NPSWF32.*\\.dll)|(NPSWF64.*\\.dll)|(Flash\\ Player\\.plugin)", + "id": "832dc9ff-3314-4df2-abcf-7bd65a645371", + "last_modified": 1603126502196 + }, + { + "schema": 1524072853593, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1454720", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our java.com.", + "name": "Java Plugin 8 update 76 to 170 (click-to-play), Windows" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 8 U(7[6-9]|[8-9]\\d|1([0-6]\\d|70))(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "33147281-45b2-487e-9fea-f66c6517252d", + "last_modified": 1524073129197 + }, + { + "schema": 1524072853593, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1454720", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on java.com.", + "name": "Java Plugin 8 update 76 to 170 (click-to-play), Linux" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 11\\.(7[6-9]|[8-9]\\d|1([0-6]\\d|70))(\\.\\d+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "ab59635e-2e93-423a-9d57-871dde8ae675", + "last_modified": 1524073076276 + }, + { + "schema": 1524068554047, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1454720", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on java.com.", + "name": "Java Plugin 8 update 170 and lower (click-to-play), Mac OS X" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 8 Update 170", + "minVersion": "Java 8 Update", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "f24ffd6f-e02b-4cf4-91d9-d54cd793e4bf", + "last_modified": 1524072853584 + }, + { + "schema": 1519390914958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1330483", + "who": "All users who have these versions of the plugin installed.", + "why": "Old versions of the Adobe Reader plugin have known vulnerabilities. All users are strongly recommended to check for updates on Adobe's Reader page.", + "name": "Adobe Reader XI 11.0.18 and lower" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/reader/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "11.0.18", + "minVersion": "11.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(nppdf32\\.dll)|(AdobePDFViewerNPAPI\\.plugin)", + "id": "59c31ade-88d6-4b22-8601-5316f82e3977", + "last_modified": 1519390923381 + }, + { + "schema": 1519390914958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1330483", + "who": "All users who have these versions of the plugin installed.", + "why": "Old versions of the Adobe Reader plugin have known vulnerabilities. All users are strongly recommended to check for updates on Adobe's Reader page.", + "name": "Adobe Reader 15.006.30244 and lower" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/reader/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "15.006.30244", + "minVersion": "15.006", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(nppdf32\\.dll)|(AdobePDFViewerNPAPI\\.plugin)", + "id": "3f136e56-4c93-4619-8c0d-d86258c1065d", + "last_modified": 1519390923357 + }, + { + "schema": 1519390914958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1330483", + "who": "All users who have these versions of the plugin installed.", + "why": "Old versions of the Adobe Reader plugin have known vulnerabilities. All users are strongly recommended to check for updates on Adobe's Reader page.", + "name": "Adobe Reader 15.020.20042 and lower" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/reader/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "15.020.20042", + "minVersion": "15.020", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(nppdf32\\.dll)|(AdobePDFViewerNPAPI\\.plugin)", + "id": "43b45ad8-a373-42c1-89c6-64e2746885e5", + "last_modified": 1519390923333 + }, + { + "schema": 1519390914958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1347194", + "why": "Old versions of this plugin have critical security vulnerabilities. You can update your plugin on Adobe's website.", + "name": "Adobe Shockwave for Director, version 12.2.7.197" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "12.2.7.197", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "np32dsw_1227197\\.dll", + "id": "97647cd8-03c5-416c-b9d3-cd5ef87ab39f", + "last_modified": 1519390923310 + }, + { + "schema": 1519390914958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1381926", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on java.com.", + "name": "Java Plugin 7 update 97 to 150 (click-to-play), Mac OS X" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 150", + "minVersion": "Java 7 Update 97", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "d70fdf87-0441-479c-833f-2213b769eb40", + "last_modified": 1519390923286 + }, + { + "schema": 1519390914958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1381926", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our java.com.", + "name": "Java Plugin 7 update 97 to 150 (click-to-play), Windows" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 7 U(97|98|99|1([0-4][0-9]|50))(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "427f5ec6-d1a7-4725-ac29-d5c5e51de537", + "last_modified": 1519390923263 + }, + { + "schema": 1519390914958, + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1381926", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on java.com.", + "name": "Java Plugin 7 update 97 to 150 (click-to-play), Linux" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 10\\.(97|98|99|1([0-4]\\d|50))(\\.\\d+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "fdc40de3-95ab-41a5-94cf-9b400221a713", + "last_modified": 1519390923239 + }, + { + "schema": 1519390914958, + "blockID": "p416", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=885362", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 6 updates 42 to 45 (click-to-play), Mac OS X", + "created": "2013-06-28T12:44:58Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 6 Update 45", + "minVersion": "Java 6 Update 42", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "02a5865d-f6ea-a330-674e-7dea7390680f", + "last_modified": 1519390921640 + }, + { + "schema": 1519390914958, + "blockID": "p119", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=780717", + "who": "All Firefox users who have installed the Java plugin, JRE versions below 1.6.0_33 or between 1.7.0 and 1.7.0_4.", + "why": "Outdated versions of the Java plugin are vulnerable to an actively exploited security issue. All users are strongly encouraged to update their Java plugin. For more information, please read our blog post or Oracle's Advisory.", + "name": "Java Plugin", + "created": "2012-08-14T09:27:32Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.(6\\.0_(\\d|[0-2]\\d?|3[0-2])|7\\.0(_0?([1-4]))?)([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 1, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "d1aa9366-d40b-a68d-b3ed-4b36e4939442", + "last_modified": 1519390921616 + }, + { + "schema": 1519390914958, + "blockID": "p328", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=853629", + "who": "All Firefox users who have these versions of the plugin installed.", + "why": "Old versions of this plugin are potentially insecure and unstable. All affected users should visit the plugin check page to look for updates for their Silverlight plugin.", + "name": "Silverlight for Mac OS X between 5.1 and 5.1.20124.* (click-to-play)", + "created": "2013-03-26T09:35:05Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "5.1.20124.9999", + "minVersion": "5.1", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "19.0a1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.16a1" + }, + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0.4" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "Silverlight\\.plugin", + "id": "dd81b232-09cb-e31d-ed8b-c5cc6ea28dd0", + "last_modified": 1519390921592 + }, + { + "schema": 1519390914958, + "blockID": "p210", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=812896", + "who": "All Firefox users who have the Java 7 plugin, updates 7 and below.", + "why": "The Java 7 Runtime Environment, update version 7, has a serious security vulnerability that is fixed in the latest update. All Firefox users are strongly encouraged to update as soon as possible. This can be done on the Plugin Check page.", + "name": "Java Plugin 1.7u7 (Linux)", + "created": "2012-11-22T09:31:33Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.7\\.0(_0?7)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 1, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "8e562dba-7ae7-fa85-2c31-479c4e661056", + "last_modified": 1519390921568 + }, + { + "schema": 1519390914958, + "blockID": "p214", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=812896", + "who": "All Firefox users who have the Java 7 plugin, updates 7 and below.", + "why": "The Java 7 Runtime Environment, update version 7 and below, has a serious security vulnerability that is fixed in the latest update. All Firefox users are strongly encouraged to update as soon as possible. This can be done on the Plugin Check page.", + "name": "Java Plugin 1.7u7 (Windows)", + "created": "2012-11-22T09:34:13Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 7 U7(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 1, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "npjp2\\.dll", + "id": "524ff62f-9564-a2b2-2585-88b88ea4c2f9", + "last_modified": 1519390921544 + }, + { + "schema": 1519390914958, + "blockID": "p414", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=885362", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 6 updates 42 to 45 (click-to-play), Windows", + "created": "2013-06-28T12:43:43Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 6 U4[2-5](\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "e36516a6-8bb3-09cc-02e3-72c67046b42e", + "last_modified": 1519390921520 + }, + { + "schema": 1519390914958, + "blockID": "p184", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829111", + "who": "All users who have these versions of the plugin installed in Firefox 17 and above.", + "why": "The Java plugin is causing significant security problems. All users are strongly recommended to keep the plugin disabled unless necessary.", + "name": "Java Plugin 7 update 11 and lower (click-to-play), Linux", + "created": "2012-10-30T14:39:15Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.7\\.0(_0?([0-9]|(1[0-1]))?)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "06621403-39a8-3867-ba6e-406dc20c88af", + "last_modified": 1519390921496 + }, + { + "schema": 1519390914958, + "blockID": "p412", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=885362", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 6 updates 42 to 45 (click-to-play), Linux", + "created": "2013-06-28T12:42:20Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.6\\.0_4[2-5]([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "8f652c63-cda4-1640-0b0c-23025e336b20", + "last_modified": 1519390921472 + }, + { + "schema": 1519390914958, + "blockID": "p29", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=421993", + "who": "Users of all versions of Yahoo Application State Plugin for Firefox 3 and later.\r\n\r\nUsers of all versions of Yahoo Application State Plugin for SeaMonkey 1.0.0.5 and later.", + "why": "This plugin causes a high volume of Firefox and SeaMonkey crashes.", + "name": "Yahoo Application State Plugin (SeaMonkey)", + "created": "2011-03-31T16:28:26Z" + }, + "enabled": true, + "matchName": "^Yahoo Application State Plugin$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "1.0.0.5" + } + ] + } + ], + "matchFilename": "npYState.dll", + "matchDescription": "^Yahoo Application State Plugin$", + "id": "8f52a562-5438-731b-5c64-7f76009f1489", + "last_modified": 1519390921448 + }, + { + "schema": 1519390914958, + "blockID": "p138", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=794247", + "who": "All Firefox users who have the Java 7 plugin, update 6 and below.", + "why": "The Java 7 Runtime Environment, update versions 6 and below, has a serious security vulnerability that is fixed in the latest update. All Firefox users are strongly encouraged to update as soon as possible. This can be done on the Plugin Check page.", + "name": "Java Plugin", + "created": "2012-09-13T14:49:52Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "Java 7 Update 06", + "minVersion": "Java 7 Update 01", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "5e898a46-8ea9-2fab-5dfe-a43e51641d81", + "last_modified": 1519390921424 + }, + { + "schema": 1519390914958, + "blockID": "p457", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=927273", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 25 to 44 (click-to-play), Linux", + "created": "2013-10-16T16:28:58Z" + }, + "enabled": true, + "matchName": "Java(\\(TM\\))? Plug-in ((1\\.7\\.0_(2[5-9]|3\\d|4[0-4]))|(10\\.4[0-4](\\.[0-9]+)?))([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "5ca97332-f4b7-81f5-d441-6acb1834f332", + "last_modified": 1519390921400 + }, + { + "schema": 1519390914958, + "blockID": "p294", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=843373", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 12 to 15 (click-to-play), Windows", + "created": "2013-02-25T12:33:48Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 7 U1[2-5](\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "ab8aff96-ead4-615d-be44-bcb7c31699f0", + "last_modified": 1519390921376 + }, + { + "schema": 1519390914958, + "blockID": "p186", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829111", + "who": "All users who have these versions of the plugin installed in Firefox 17 and above.", + "why": "The Java plugin is causing significant security problems. All users are strongly recommended to keep the plugin disabled unless necessary.", + "name": "Java Plugin 6 updates 31 through 38 (click-to-play), Windows", + "created": "2012-10-30T14:45:39Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 6 U3[1-8](\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "8488cdb0-1f00-1349-abfc-f50e85c9163b", + "last_modified": 1519390921351 + }, + { + "os": "Darwin", + "schema": 1519390914958, + "blockID": "p242", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=821422", + "who": "All Firefox (18 and above) users on Mac OS X who have installed a version of Flip4Mac older than 2.4.4.", + "why": "Old versions of the Flip4Mac WMV plugin are causing significant stability problems in Firefox 18 and above. Users are encouraged update to the latest version of the plugin, available at the Flip4Mac site.", + "name": "Flip4Mac WMV Plugin", + "created": "2012-12-21T13:32:36Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "2.4.3.999", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "18.0a1" + } + ] + } + ], + "matchDescription": "Flip4Mac", + "id": "cef6f402-bdf8-0ba6-66d8-8ac42c72d4be", + "last_modified": 1519390921327 + }, + { + "schema": 1519390914958, + "blockID": "p26", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=421993", + "who": "Users of all versions of Yahoo Application State Plugin for Firefox 3 and later.\r\n\r\nUsers of all versions of Yahoo Application State Plugin for SeaMonkey 1.0.0.5 and later.", + "why": "This plugin causes a high volume of Firefox and SeaMonkey crashes.", + "name": "Yahoo Application State Plugin", + "created": "2011-03-31T16:28:26Z" + }, + "enabled": true, + "matchName": "^Yahoo Application State Plugin$", + "versionRange": [ + { + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "3.0a1" + } + ] + } + ], + "matchFilename": "npYState.dll", + "matchDescription": "^Yahoo Application State Plugin$", + "id": "6a1b6dfe-f463-3061-e8f8-6e896ccf2a8a", + "last_modified": 1519390921303 + }, + { + "schema": 1519390914958, + "blockID": "p459", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=927273", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 25 to 44 (click-to-play), Mac OS X", + "created": "2013-10-16T16:29:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 44", + "minVersion": "Java 7 Update 25", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "5b0bbf0e-dbae-7896-3c31-c6cb7a74e6fa", + "last_modified": 1519390921278 + }, + { + "schema": 1519390914958, + "blockID": "p188", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829111", + "who": "All users who have these versions of the plugin installed in Firefox 17 and above.", + "why": "The Java plugin is causing significant security problems. All users are strongly recommended to keep the plugin disabled unless necessary.", + "name": "Java Plugin 6 updates 38 and lower (click-to-play), Mac OS X", + "created": "2012-10-30T14:48:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 6 Update 38", + "minVersion": "Java 6 Update 0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "61944828-8178-71ab-e9c6-d846b5f45d96", + "last_modified": 1519390921253 + }, + { + "schema": 1519390914958, + "blockID": "p292", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=843373", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 12 to 15 (click-to-play), Mac OS X", + "created": "2013-02-25T12:32:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 15", + "minVersion": "Java 7 Update 12", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "9a09fe22-c3d8-57e6-4e99-ecd06fb03081", + "last_modified": 1519390921228 + }, + { + "schema": 1519390914958, + "blockID": "p125", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=780717", + "who": "All Firefox users who have installed the Java plugin, JRE versions below 1.6.0_33 or between 1.7.0 and 1.7.0_4.", + "why": "Outdated versions of the Java plugin are vulnerable to an actively exploited security issue. All users are strongly encouraged to update their Java plugin. For more information, please read our blog post or Oracle's Advisory.", + "name": "Java Plugin", + "created": "2012-08-14T09:31:17Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE ((6( U(\\d|([0-2]\\d)|3[0-2]))?)|(7(\\sU[0-4])?))(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 1, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "npjp2\\.dll", + "id": "7538347d-ca2e-09b8-c5d8-0a9d140f8c87", + "last_modified": 1519390921203 + }, + { + "schema": 1519390914958, + "blockID": "p134", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=794247", + "who": "All Firefox users who have the Java 7 plugin, update 6 and below.", + "why": "The Java 7 Runtime Environment, update versions 6 and below, has a serious security vulnerability that is fixed in the latest update. All Firefox users are strongly encouraged to update as soon as possible. This can be done on the Plugin Check page.", + "name": "Java Plugin", + "created": "2012-08-31T15:22:51Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 7 U[5-6](\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 1, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "npjp2\\.dll", + "id": "1f519b98-a4b6-2a98-b1ff-b642f7b4d2bb", + "last_modified": 1519390921179 + }, + { + "schema": 1519390914958, + "blockID": "p123", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=780717", + "who": "All Firefox users who have installed the Java plugin, JRE versions below 1.6.0_33 or between 1.7.0 and 1.7.0_4.", + "why": "Outdated versions of the Java plugin are vulnerable to an actively exploited security issue. All users are strongly encouraged to update their Java plugin. For more information, please read our blog post or Oracle's Advisory.", + "name": "Java Plugin", + "created": "2012-08-14T09:29:42Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "14.2.0", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "JavaPlugin2_NPAPI\\.plugin", + "id": "46bf36a7-1934-38f8-1fcc-c9c4bcc8343e", + "last_modified": 1519390921152 + }, + { + "schema": 1519390914958, + "blockID": "p190", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829111", + "who": "All users who have these versions of the plugin installed in Firefox 17 and above.", + "why": "The Java plugin is causing significant security problems. All users are strongly recommended to keep the plugin disabled unless necessary.", + "name": "Java Plugin 6 updates 31 through 38 (click-to-play), Linux", + "created": "2012-10-30T14:55:37Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.6\\.0_3[1-8]([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "9f9eb0ae-6495-aaa6-041a-d802cdb8e134", + "last_modified": 1519390921128 + }, + { + "schema": 1519390914958, + "blockID": "p418", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=885362", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 16 to 24 (click-to-play), Linux", + "created": "2013-06-28T12:46:18Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.7\\.0_(1[6-9]|2[0-4])([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "ee5c1584-0170-8702-5f99-e0325b4a91a8", + "last_modified": 1519390921103 + }, + { + "schema": 1519390914958, + "blockID": "p182", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829111", + "who": "All users who have these versions of the plugin installed in Firefox 17 and above.", + "why": "The Java plugin is causing significant security problems. All users are strongly recommended to keep the plugin disabled unless necessary.", + "name": "Java Plugin 7 update 11 and lower (click-to-play), Windows", + "created": "2012-10-30T14:33:04Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 7 U([0-9]|(1[0-1]))(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "385d9911-f8bc-83e0-8cd2-94c98087938c", + "last_modified": 1519390919419 + }, + { + "schema": 1519390914958, + "blockID": "p422", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=885362", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 16 to 24 (click-to-play), Mac OS X", + "created": "2013-06-28T12:48:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 24", + "minVersion": "Java 7 Update 16", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "709960a7-d268-0c70-d6ad-17bbed0cd1c4", + "last_modified": 1519390919396 + }, + { + "schema": 1519390914958, + "blockID": "p212", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=812896", + "who": "All Firefox users who have the Java 7 plugin, updates 7 and below.", + "why": "The Java 7 Runtime Environment, update version 7, has a serious security vulnerability that is fixed in the latest update. All Firefox users are strongly encouraged to update as soon as possible. This can be done on the Plugin Check page.", + "name": "Java Plugin 1.7u7 (Mac OS X)", + "created": "2012-11-22T09:33:07Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "Java 7 Update 07", + "minVersion": "Java 7 Update 07", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "5ec1cd0f-d478-e6e0-989e-c91694b2a411", + "last_modified": 1519390919373 + }, + { + "schema": 1519390914958, + "blockID": "p302", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=843373", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.\r\n", + "name": "Java Plugin 6 updates 39 to 41 (click-to-play), Linux", + "created": "2013-02-25T12:37:17Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.6\\.0_(39|40|41)([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "caec8103-dec9-8018-eb3d-9cf21cbf68a6", + "last_modified": 1519390919350 + }, + { + "schema": 1519390914958, + "blockID": "p132", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=794247", + "who": "All Firefox users who have the Java 7 plugin, updates 6 and below.", + "why": "The Java 7 Runtime Environment, update versions 6 and below, has a serious security vulnerability that is fixed in the latest update. All Firefox users are strongly encouraged to update as soon as possible. This can be done on the Plugin Check page.", + "name": "Java Plugin", + "created": "2012-08-31T15:21:34Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.7\\.0(_0?([5-6]))?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 1, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.14.*", + "minVersion": "0.1" + } + ] + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "572d0485-3388-7c2a-a062-b062e42c8710", + "last_modified": 1519390919327 + }, + { + "schema": 1519390914958, + "blockID": "p296", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=843373", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 12 to 15 (click-to-play), Linux", + "created": "2013-02-25T12:34:37Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Plug-in 1\\.7\\.0_1[2-5]([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "5b7aaf34-48a3-dfa5-1db8-550faef41501", + "last_modified": 1519390919304 + }, + { + "schema": 1519390914958, + "blockID": "p298", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=843373", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.\r\n", + "name": "Java Plugin 6 updates 39 to 41 (click-to-play), Mac OS X", + "created": "2013-02-25T12:35:34Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 6 Update 41", + "minVersion": "Java 6 Update 39", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "cfd76877-e79d-8e8c-c6a7-6b596fd344e8", + "last_modified": 1519390919281 + }, + { + "schema": 1519390914958, + "blockID": "p458", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=927273", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 25 to 44 (click-to-play), Windows", + "created": "2013-10-16T16:29:18Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 7 U(2[5-9]|3\\d|4[0-4])(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "d199d513-3c49-b53c-9447-33c8021c0df2", + "last_modified": 1519390919257 + }, + { + "schema": 1519390914958, + "blockID": "p420", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=885362", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 16 to 24 (click-to-play), Windows", + "created": "2013-06-28T12:47:32Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 7 U(1[6-9]|2[0-4])(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "5942b7ae-bcdc-e329-9f17-f7a795c9ec83", + "last_modified": 1519390919234 + }, + { + "schema": 1519390914958, + "blockID": "p254", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=830410", + "who": "All Firefox users on Mac OS X who are using the affected versions of the PDF Browser Plugin.", + "why": "The PDF Browser Plugin is causing frequent crashes on Firefox 18 and above. All users are recommended to keep the plugin disabled and update it if a new version becomes available.", + "name": "PDF Browser Plugin 2.4.2 and below", + "created": "2013-01-15T11:54:24Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "2.4.2", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "18.0a1" + } + ] + } + ], + "matchFilename": "PDF Browser Plugin\\.plugin", + "id": "2dd1b53a-52db-2d0f-392a-410d5aade9d6", + "last_modified": 1519390919211 + }, + { + "schema": 1519390914958, + "blockID": "p34", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=634639", + "who": "Users of Java 2 Plugin versions 1.5_00 to 1.6_99 in Firefox 3.6 and later.", + "why": "These versions of the Java plugin are no longer supported by Oracle and cause a high volume of Firefox crashes.", + "name": "Java Plugin", + "created": "2011-02-17T17:20:54Z" + }, + "enabled": true, + "versionRange": [ + { + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "3.6a1pre" + } + ] + } + ], + "matchFilename": "[Nn][Pp][Jj][Pp][Ii]1[56]0_[0-9]+\\.[Dd][Ll][Ll]", + "id": "91618232-c650-5dc6-00fe-293c0baedc34", + "last_modified": 1519390919188 + }, + { + "schema": 1519390914958, + "blockID": "p180", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829111", + "who": "All users who have these versions of the plugin installed in Firefox 17 and above.", + "why": "The Java plugin is causing significant security problems. All users are strongly recommended to keep the plugin disabled unless necessary.", + "name": "Java Plugin 7 update 10 and lower (click-to-play), Mac OS X", + "created": "2012-10-30T14:29:51Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 10", + "minVersion": "Java 7 Update 0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "12f38c07-c1e1-2464-2eab-d454cf471eab", + "last_modified": 1519390919165 + }, + { + "schema": 1519390914958, + "blockID": "p32", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=539282", + "who": "All users of the Viewpoint plugin for Firefox 3 and later.", + "why": "This plugin causes a high volume of Firefox crashes.", + "name": "Viewpoint", + "created": "2011-03-31T16:28:26Z" + }, + "enabled": true, + "versionRange": [ + { + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "3.0" + } + ] + } + ], + "matchFilename": "npViewpoint.dll", + "id": "1206e79a-4817-16e9-0f5e-7762a8d19216", + "last_modified": 1519390919142 + }, + { + "schema": 1519390914958, + "blockID": "p94", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=754723", + "who": "All Firefox users who have a version of the Flash Player Plugin older than 10.2.159.1.", + "why": "Old versions of the Flash Player plugin are targeted by known security vulnerabilities, and cause frequent Firefox crashes. We strongly recommend all users to visit the Flash Player homepage to download and install the latest available version of this plugin.\r\n\r\nThis is not an ordinary block. It's just an upgrade notification that will be shown to all users who have these old versions installed, asking them to download the latest version. If they choose to ignore the notification, the plugin will continue to work normally.", + "name": "Flash Player Plugin", + "created": "2012-05-23T09:15:23Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/flashplayer/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "10.2.159.1", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0.1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "2.13.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "Flash\\ Player\\.plugin", + "id": "2a741cac-32d7-a67e-0bf7-b5b53a0ff22b", + "last_modified": 1519390919119 + }, + { + "schema": 1519390914958, + "blockID": "p300", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=843373", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 6 updates 39 to 41 (click-to-play), Windows", + "created": "2013-02-25T12:36:25Z" + }, + "enabled": true, + "matchName": "Java\\(TM\\) Platform SE 6 U(39|40|41)(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "17.0" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.14" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "d64e08b6-66b2-f534-53d8-1cbcbb139f2a", + "last_modified": 1519390919095 + }, + { + "schema": 1519390914958, + "blockID": "p152", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=797378", + "who": "All Firefox users who have this plugin installed.", + "why": "This plugin is outdated and is potentially insecure. Affected users should go to the plugin check page and update to the latest version.", + "name": "Silverlight 4.1.10328.0 and lower", + "created": "2012-10-05T10:34:12Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "4.1.10328.0", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npctrl\\.dll", + "id": "abdcbe90-5575-6b4b-12de-bbabd93ecf57", + "last_modified": 1519390919072 + }, + { + "schema": 1519390914958, + "blockID": "p904", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1159917", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 44 and lower (click-to-play), Mac OS X", + "created": "2015-05-19T09:01:42Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 8 Update 44", + "minVersion": "Java 8", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "47f6217d-0aa6-1e39-a9c9-cc64d57bb91f", + "last_modified": 1519390919049 + }, + { + "schema": 1519390914958, + "blockID": "p1061", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1217932", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 81 to 90 (click-to-play), Windows", + "created": "2015-12-02T12:39:41Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 7 U(8[1-9]|90)(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "69196ada-69bd-6454-eea8-6f8b2037368c", + "last_modified": 1519390919026 + }, + { + "schema": 1519390914958, + "blockID": "p1120", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1241237", + "who": "All users who have these versions of the Silverlight plugin installed.", + "why": "Old versions of the Silverlight plugin are known to have critical security vulnerabilities. You can get the latest version of Silverlight here.", + "name": "Silverlight plugin 5.1.41105.0 and lower (click to play)", + "created": "2016-02-03T09:42:43Z" + }, + "enabled": true, + "infoURL": "https://www.microsoft.com/getsilverlight", + "versionRange": [ + { + "severity": 0, + "maxVersion": "5.1.41105.0", + "minVersion": "5.1.20125", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(Silverlight\\.plugin|npctrl\\.dll)", + "id": "f0bfeb8c-04ee-d5ef-6670-b28c0e0d0f58", + "last_modified": 1519390919002 + }, + { + "schema": 1519390914958, + "blockID": "p102", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=695927", + "who": "All Firefox users who have this plugin installed.", + "why": "This plugin was discontinued years ago and only worked up to Firefox version 1.5. Because plugins aren't marked as incompatible, there are still many users who have it installed and have recently been experiencing crashes related to it.", + "name": "Mozilla ActiveX Plug-in", + "created": "2012-06-07T17:43:57Z" + }, + "enabled": true, + "versionRange": [ + { + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "npmozax\\.dll", + "id": "2e001e2f-483b-7595-8810-3672e0346950", + "last_modified": 1519390918979 + }, + { + "os": "Darwin", + "schema": 1519390914958, + "blockID": "p252", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=826002", + "who": "All Firefox users on Mac OS X who have installed the Adobe Reader XI plugin. Users are recommended to update when a new version of Adobe Reader becomes available, or to downgrade to the latest version of Adobe Reader 10.", + "why": "The Adobe Reader XI plugin is causing frequent crashes on Firefox for Mac OS X. The impact and this block are currently limited to versions 11.0.0 and 11.0.01.", + "name": "Adobe Reader XI for Mac OS X", + "created": "2013-01-15T10:56:30Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "11.0.01", + "minVersion": "11.0.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "AdobePDFViewerNPAPI\\.plugin", + "id": "821ddb3d-bba4-6479-28b6-9974383fa8c9", + "last_modified": 1519390918955 + }, + { + "schema": 1519390914958, + "blockID": "p1004", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=974012", + "who": "All users who have these versions of the plugin installed", + "why": "These versions of the Unity Web Player plugin have known vulnerabilities that can put users at risk.", + "name": "Unity Web Player 5.0 to 5.0.3f1 (click-to-play), Mac OS", + "created": "2015-09-09T14:11:57Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "Unity Web Player\\.plugin", + "matchDescription": "^($|Unity Web Player version 5.0(\\.([0-2]|3f1))?[^0-9.])", + "id": "5a0b69cb-a516-e7a2-ea3b-b4fafcc13c6b", + "last_modified": 1519390918932 + }, + { + "schema": 1519390914958, + "blockID": "p248", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829054", + "who": "All users who have Firefox 18 or above installed, and the Sibelius Scorch plugin.", + "why": "The Sibelius Scorch plugin is not compatible with Firefox 18 and above, and is crashing whenever loaded on affected versions of Firefox. ", + "name": "Sibelius Scorch plugin", + "created": "2013-01-14T09:26:47Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "6.2.0b88", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "Scorch\\.plugin", + "id": "28b5baa1-6490-38e2-395f-53fc84aa12df", + "last_modified": 1519390918909 + }, + { + "schema": 1519390914958, + "blockID": "p1151", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1264874", + "who": "All users who have the QuickTime Plugin for Windows installed.", + "why": "The QuickTime Plugin for Windows has been discontinued by Apple and has known critical security vulnerabilities. All users are strongly encouraged to remove it or keep it disabled.", + "name": "QuickTime Plugin for Windows", + "created": "2016-04-18T17:41:00Z" + }, + "enabled": true, + "infoURL": "https://support.apple.com/en-us/HT205771", + "versionRange": [ + { + "severity": 0, + "maxVersion": "*", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 2 + } + ], + "matchFilename": "npqtplugin\\.dll", + "id": "bb8db302-9579-42d2-ff75-c61500f6a020", + "last_modified": 1519390918887 + }, + { + "schema": 1519390914958, + "blockID": "p28", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=512406", + "who": "All users of Internet Saving Optimizer for all Mozilla applications.", + "why": "This plugin causes a high volume of Firefox crashes and is considered malware.", + "name": "Internet Saving Optimizer (plugin)", + "created": "2011-03-31T16:28:26Z" + }, + "enabled": true, + "versionRange": [ + { + "maxVersion": "*", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "NPFFAddOn.dll", + "id": "c85a4a9c-d61c-130f-d525-ea36becac235", + "last_modified": 1519390918863 + }, + { + "schema": 1519390914958, + "blockID": "p456", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=470936", + "who": "All Firefox users who are using old versions of the VLC player plugin.", + "why": "Old versions of the VLC player are known to cause stability problems in Firefox. All users are recommended to update to the latest version of their VLC software.", + "name": "VLC Player plugin 2.0.5 and lower (click-to-play)", + "created": "2013-09-30T14:35:07Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "2.0.5", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npvlc\\.dll", + "id": "0cfaaefe-88a4-dda4-96bc-110e402ca9d5", + "last_modified": 1519390917137 + }, + { + "schema": 1519390914958, + "blockID": "p1144", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1259458", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 64 to 76 (click-to-play), Windows", + "created": "2016-03-31T16:18:15Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 8 U(6[4-9]|7[0-6])(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "b3c55844-79d2-66dd-0d44-82fb4533307e", + "last_modified": 1519390917115 + }, + { + "schema": 1519390914958, + "blockID": "p408", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=837377", + "who": "All Firefox users who have affected versions of the QuickTime plugin installed.", + "why": "Old versions of the QuickTime plugin are known to be insecure and cause stability problems. All users are recommended to update to the latest version available. You can check for updates in the plugin check page.", + "name": "QuickTime Plugin 7.6.5 and lower (click-to-play), Mac OS X", + "created": "2013-06-28T09:52:37Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "7.6.5", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "QuickTime Plugin\\.plugin", + "id": "4f39cd0f-976c-1f71-2d9c-2abe1b4706d9", + "last_modified": 1519390917091 + }, + { + "schema": 1519390914958, + "blockID": "p1059", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1217932", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 81 to 90 (click-to-play), Mac OS X", + "created": "2015-12-02T12:37:58Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 90", + "minVersion": "Java 7 Update 81", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "799c4f10-d75d-30a0-5570-56df6be628e0", + "last_modified": 1519390917067 + }, + { + "schema": 1519390914958, + "blockID": "p558", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=974012", + "who": "All Firefox users who have these versions of the plugin installed.", + "why": "Current versions of the Unity Web Player plugin have known vulnerabilities that can put users at risk.", + "name": "Unity Web Player 4.6.6f1 and lower (click-to-play), Mac OS", + "created": "2014-02-25T08:29:51Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "Unity Web Player\\.plugin", + "matchDescription": "^($|Unity Web Player version ([0-3]|(4\\.([0-5]|6(\\.([0-5]|6f1)))?[^0-9.])))", + "id": "bbf83e70-65f5-75af-b841-7cebcf3e38c1", + "last_modified": 1519390917044 + }, + { + "schema": 1519390914958, + "blockID": "p1145", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1259458", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 91 to 97 (click-to-play), Linux", + "created": "2016-03-31T16:18:53Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 10\\.(9[1-7])(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "d4e75c8c-9c32-6093-12cc-1de0fd8f9e87", + "last_modified": 1519390917020 + }, + { + "schema": 1519390914958, + "blockID": "p33", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=558584", + "who": "Users of Java Deployment Toolkit versions 6.0.200.0 and older in all versions of Firefox.", + "why": "This plugin has publicly-known security vulnerabilities. For more information, please visit the vendor page.", + "name": "Java Deployment Toolkit", + "created": "2010-04-16T17:52:32Z" + }, + "enabled": true, + "matchName": "[0-6]\\.0\\.[01]\\d{2}\\.\\d+", + "versionRange": [ + { + "severity": 1, + "maxVersion": "", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "npdeploytk.dll", + "id": "b12b4282-d327-06d6-6e62-4788ba2c4b2f", + "last_modified": 1519390916997 + }, + { + "schema": 1519390914958, + "blockID": "p1060", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1217932", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 46 to 64 (click-to-play), Mac OS X", + "created": "2015-12-02T12:38:53Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 8 Update 64", + "minVersion": "Java 8 Update 46", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "aee0ad26-018f-392b-91f7-6a8aaf332774", + "last_modified": 1519390916973 + }, + { + "os": "Darwin", + "schema": 1519390914958, + "blockID": "p89", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=750387", + "who": "Firefox users on Mac OS X who have installed the Adobe Acrobat plugin.", + "why": "The Adobe Acrobat plugin doesn't work on Mac OS X in default (64-bit) mode, showing a blank page when users click on links to PDF files in Firefox. It also causes a significant number of crashes in 32-bit mode.\r\n\r\nThere's more information on this blog post.", + "name": "Adobe Acrobat NPAPI Plug-in", + "created": "2012-05-04T11:29:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "10.1.3", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "AdobePDFViewerNPAPI\\.plugin", + "id": "473f4d9c-e38e-a348-26d1-de7b842893d9", + "last_modified": 1519390916947 + }, + { + "schema": 1519390914958, + "blockID": "p129", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=782672", + "who": "All Firefox users on Mac OS X who have old versions of the Silverlight plugin.", + "why": "Old versions of the Silverlight plugin for Mac OS X are causing serious stability problems in Firefox. Affected users can visit the official Silverlight page and get the latest version of the Silverlight plugin to correct this problem.", + "name": "Silverlight for Mac OS X", + "created": "2012-08-22T10:37:12Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "5.0.99999", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "Silverlight\\.plugin", + "id": "ab861311-9c24-5f55-0d3a-f0072868357c", + "last_modified": 1519390916923 + }, + { + "schema": 1519390914958, + "blockID": "p1142", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1259458", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 64 to 76 (click-to-play), Mac OS X", + "created": "2016-03-31T16:16:46Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 8 Update 76", + "minVersion": "Java 8 Update 64", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "646ebafe-dd54-e45f-9569-c245ef72259d", + "last_modified": 1519390916899 + }, + { + "schema": 1519390914958, + "blockID": "p250", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=828982", + "who": "All Firefox users on Windows who have installed the Foxit Reader Plugin, versions 2.2.1.530 and below.", + "why": "The Foxit Reader plugin is vulnerable to a critical security bug that can compromise a user's system by visiting a malicious site.", + "name": "Foxit Reader Plugin 2.2.1.530 and below", + "created": "2013-01-14T13:07:03Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "2.2.1.530", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 2 + } + ], + "matchFilename": "npFoxitReaderPlugin\\.dll", + "id": "c6299be0-ab77-8bbd-bcaf-c886cec4e799", + "last_modified": 1519390916873 + }, + { + "schema": 1519390914958, + "blockID": "p85", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=741592", + "who": "All Firefox users who have installed the Java plugin, JRE versions below 1.6.0_31 or between 1.7.0 and 1.7.0_2.", + "why": "Outdated versions of the Java plugin are vulnerable to an actively exploited security issue. All Mac OS X users are strongly encouraged to update their Java plugin through Software Update, or disable it if no alternatives are available. For more information, please read our blog post or Oracle's Advisory.\r\n\r\n", + "name": "Java Plugin", + "created": "2012-04-16T13:58:43Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "13.6.0", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "JavaPlugin2_NPAPI\\.plugin", + "id": "4df1324d-8236-b7ae-db55-4fb1b7db2754", + "last_modified": 1519390916849 + }, + { + "schema": 1519390914958, + "blockID": "p1250", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1286972", + "who": "All users who have this plugin installed.", + "why": "Old versions of the Adobe Reader plugin have known vulnerabilities. All users are strongly recommended to check for updates on our plugin check page.", + "name": "Adobe Reader (Continuous) 15.016.20045", + "created": "2016-07-21T22:22:11Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/reader", + "versionRange": [ + { + "severity": 0, + "maxVersion": "15.016.20045", + "minVersion": "15.016.20045", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(nppdf32\\.dll)|(AdobePDFViewerNPAPI\\.plugin)", + "id": "b9f6998a-7a45-7d83-7ee6-897e6b193e42", + "last_modified": 1519390916826 + }, + { + "schema": 1519390914958, + "blockID": "p1053", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1222130", + "who": "All users who have these versions of the Real Player plugin. Affected users can visit our plugin check page and check for updates.", + "why": "Old versions of the Real Player plugin have serious security problems that could lead to system compromise.", + "name": "Real Player for Windows, 17.0.10.7 and lower, click-to-play", + "created": "2015-11-12T09:07:04Z" + }, + "enabled": true, + "infoURL": "https://real.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "17.0.10.7", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "nprpplugin\\.dll", + "id": "2a6a7300-fac0-6518-c44a-35b3c4c714c3", + "last_modified": 1519390916801 + }, + { + "schema": 1519390914958, + "blockID": "p1141", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1259458", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 91 to 97 (click-to-play), Mac OS X", + "created": "2016-03-31T16:16:02Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 97", + "minVersion": "Java 7 Update 91", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "2a432d6e-9545-66be-9f93-aefe9793f450", + "last_modified": 1519390916777 + }, + { + "schema": 1519390914958, + "blockID": "p158", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=850745", + "who": "All Firefox users who have this plugin installed.", + "why": "This plugin is outdated and is potentially insecure. Affected users should go to the plugin check page and update to the latest version.", + "name": "Adobe Reader 10.0 to 10.1.5.*", + "created": "2012-10-05T10:40:20Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "10.1.5.9999", + "minVersion": "10.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "nppdf32\\.dll", + "id": "107c9a8c-2ef8-da26-75c3-bc26f8ae90fa", + "last_modified": 1519390916752 + }, + { + "schema": 1519390914958, + "blockID": "p960", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1183369", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 45 (click-to-play), Windows", + "created": "2015-07-15T10:50:25Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 8 U45(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "990fa997-1d4a-98ee-32de-a7471e81db42", + "last_modified": 1519390916727 + }, + { + "schema": 1519390914958, + "blockID": "p113", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=778686", + "who": "All Firefox users who have this plugin installed.", + "why": "Version 1.0.0.0 of the Ubisoft Uplay plugin has a security vulnerability that can be exploited by malicious websites to gain control of the user's system.", + "name": "Ubisoft Uplay", + "created": "2012-07-30T12:11:59Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.0.0.0", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "npuplaypc\\.dll", + "id": "ea3b767d-933b-3f54-f447-09bd2bfbc6e0", + "last_modified": 1519390916703 + }, + { + "schema": 1519390914958, + "blockID": "p366", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=879128", + "who": "All users who have Firefox 18 or above installed, and the Sibelius Scorch plugin.", + "why": "The Sibelius Scorch plugin is not compatible with Firefox 18 and above, and is crashing whenever loaded on affected versions of Firefox. ", + "name": "Sibelius Scorch plugin", + "created": "2013-06-11T13:20:32Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "6.2.0", + "minVersion": "6.2.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "Scorch\\.plugin", + "id": "43ec430a-75ec-6853-f62b-1a54489fea5f", + "last_modified": 1519390916680 + }, + { + "schema": 1519390914958, + "blockID": "p964", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1183369", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 45 (click-to-play), Linux", + "created": "2015-07-15T10:51:57Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 11\\.45(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "c2431673-3478-f1e1-5555-28e2d5377adc", + "last_modified": 1519390916656 + }, + { + "schema": 1519390914958, + "blockID": "p1002", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=974012", + "who": "All users who have these versions of the plugin installed.", + "why": "These versions of the Unity Web Player plugin have known vulnerabilities that can put users at risk.", + "name": "Unity Web Player 5.0 to 5.0.3f1 (click-to-play), Windows", + "created": "2015-09-09T14:07:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "5.0.3f1", + "minVersion": "5.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npUnity3D32\\.dll", + "id": "48e45eea-fd32-2304-2776-fe75211a69e5", + "last_modified": 1519390916633 + }, + { + "schema": 1519390914958, + "blockID": "p902", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1159917", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 45 to 78 (click-to-play), Mac OS X", + "created": "2015-05-19T09:00:27Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 78", + "minVersion": "Java 7 Update 45", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "72de1aa2-6afe-2f5b-a93e-baa4c8c4578d", + "last_modified": 1519390916609 + }, + { + "schema": 1519390914958, + "blockID": "p154", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=850744", + "who": "All Firefox users who have this plugin installed.", + "why": "This plugin is outdated and is potentially insecure. Affected users should go to the plugin check page and update to the latest version.", + "name": "Silverlight 5.0 to 5.1.20124.*", + "created": "2012-10-05T10:35:55Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "5.1.20124.9999", + "minVersion": "5.0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npctrl\\.dll", + "id": "64b50946-53f8-182d-a239-bd585b0e0b0e", + "last_modified": 1519390916585 + }, + { + "schema": 1519390914958, + "blockID": "p1247", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1286972", + "who": "All users who have this plugin installed.", + "why": "Old versions of the Adobe Reader plugin have known vulnerabilities. All users are strongly recommended to check for updates on our plugin check page.", + "name": "Adobe Reader (Classic) 15.006.30174", + "created": "2016-07-21T16:21:52Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/reader", + "versionRange": [ + { + "severity": 0, + "maxVersion": "15.006.30174", + "minVersion": "15.006.30174", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(nppdf32\\.dll)|(AdobePDFViewerNPAPI\\.plugin)", + "id": "eba24802-0c93-b8d4-ca2d-c9c194c7462b", + "last_modified": 1519390916562 + }, + { + "schema": 1519242099615, + "blockID": "p31", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=449062", + "who": "All users of MyWebSearch for all Mozilla applications.", + "why": "This plugin causes a high volume of Firefox crashes.", + "name": "MyWebSearch", + "created": "2011-03-31T16:28:26Z" + }, + "enabled": true, + "versionRange": [ + { + "maxVersion": "*", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "NPMySrch.dll", + "id": "2b2b85e9-4f64-9894-e3fa-3c05ead892b3", + "last_modified": 1519390914951 + }, + { + "schema": 1519242099615, + "blockID": "p572", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=992976", + "who": "All Firefox users who have this plugin installed. Updated versions can be found on this site.", + "why": "Versions 6.1.4.27993 and earlier of this plugin are known to have security vulnerabilities.", + "name": "DjVu Plugin Viewer 6.1.4.27993 and earlier (Windows)", + "created": "2014-04-08T14:42:05Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "6.1.4.27993", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npdjvu\\.dll", + "id": "8a416fb1-29bf-ace9-02de-605d805b6e79", + "last_modified": 1519390914928 + }, + { + "schema": 1519242099615, + "blockID": "p592", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=980355", + "who": "All Firefox users who have installed a version of the Cisco Web Communicator plugin lower than 3.0.6.", + "why": "Versions lower than 3.0.6 of the Cisco Web Communicator plugin are known to have security issues and should not be used. All users should update to version 3.0.6 or later. Find updates here.", + "name": "Cisco Web Communicator < 3.0.6 (Mac OS X)", + "created": "2014-06-11T16:48:48Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "3.0.5.99999999999999", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "CiscoWebCommunicator\\.plugin", + "id": "1f9f9b90-e733-3929-821f-1b78a8698747", + "last_modified": 1519390914905 + }, + { + "schema": 1519242099615, + "blockID": "p912", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1159917", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 44 and lower (click-to-play), Linux", + "created": "2015-05-19T09:05:55Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 11\\.(\\d|[1-3]\\d|4[0-4])(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "f0a40537-9a13-1b4b-60e5-b9121835c1ca", + "last_modified": 1519390914882 + }, + { + "schema": 1519242099615, + "blockID": "p908", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1159917", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 44 and lower (click-to-play), Windows", + "created": "2015-05-19T09:03:44Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 8( U([1-3]?\\d|4[0-4]))?(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "79e165b4-599b-433d-d618-146f0c121ead", + "last_modified": 1519390914856 + }, + { + "schema": 1519242099615, + "blockID": "p1146", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1259458", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 64 to 76 (click-to-play), Linux", + "created": "2016-03-31T16:19:31Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 11\\.(6[4-9]|7[0-6])(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "bc99ed93-b527-12e2-c6e6-c61668a2e7d0", + "last_modified": 1519390914833 + }, + { + "schema": 1519242099615, + "blockID": "p958", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1183369", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 79 to 80 (click-to-play), Windows", + "created": "2015-07-15T10:49:33Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 7 U(79|80)(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "2cfa79ef-a2ab-d868-467d-d182242136a5", + "last_modified": 1519390914810 + }, + { + "schema": 1519242099615, + "blockID": "p156", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=797378", + "who": "All Firefox users who have this plugin installed.", + "why": "This plugin is outdated and is potentially insecure. Affected users should go to the plugin check page and update to the latest version.", + "name": "Adobe Reader 9.5.1 and lower", + "created": "2012-10-05T10:38:46Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "9.5.1", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "nppdf32\\.dll", + "id": "0dce3611-19e7-e8e4-5b5c-13593230f83c", + "last_modified": 1519390914787 + }, + { + "schema": 1519242099615, + "blockID": "p574", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=992976", + "who": "All Firefox users who have this plugin installed. Updated versions can be found on this site.", + "why": "Versions 6.1.1 and earlier of this plugin are known to have security vulnerabilities.\r\n", + "name": "DjVu Plugin Viewer 6.1.1 and earlier (Mac OS)", + "created": "2014-04-08T14:43:54Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "6.1.1", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "NPDjVu\\.plugin", + "id": "c841472f-f976-c49a-169e-0c751012c3b1", + "last_modified": 1519390914764 + }, + { + "schema": 1519242099615, + "blockID": "p556", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=974012", + "who": "All Firefox users who have these versions of the plugin installed.", + "why": "Current versions of the Unity Web Player plugin have known vulnerabilities that can put users at risk.", + "name": "Unity Web Player 4.6.6f1 and lower (click-to-play), Windows", + "created": "2014-02-25T08:29:41Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "4.6.6f1", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npUnity3D32\\.dll", + "id": "b90eb83c-1314-5b27-687b-98d8115b6106", + "last_modified": 1519390914741 + }, + { + "schema": 1519242099615, + "blockID": "p956", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1183369", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 45 (click-to-play), Mac OS X", + "created": "2015-07-15T10:48:44Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 8 Update 45", + "minVersion": "Java 8 Update 45", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "111881f9-a5bd-4919-4bab-9d7581d959d3", + "last_modified": 1519390914718 + }, + { + "schema": 1519242099615, + "blockID": "p80", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=739955", + "who": "All Firefox users who have installed the Java plugin, JRE versions below 1.6.0_31 or between 1.7.0 and 1.7.0_2.", + "why": "Outdated versions of the Java plugin are vulnerable to an actively exploited security issue. All users are strongly encouraged to update their Java plugin. For more information, please read our blog post or Oracle's Advisory.", + "name": "Java Plugin", + "created": "2012-04-02T15:18:50Z" + }, + "enabled": true, + "matchName": "\\(TM\\)", + "versionRange": [ + { + "severity": 1, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "(npjp2\\.dll)|(libnpjp2\\.so)", + "matchDescription": "[^\\d\\._]((0(\\.\\d+(\\.\\d+([_\\.]\\d+)?)?)?)|(1\\.(([0-5](\\.\\d+([_\\.]\\d+)?)?)|(6(\\.0([_\\.](0?\\d|1\\d|2\\d|30))?)?)|(7(\\.0([_\\.][0-2])?)?))))([^\\d\\._]|$)", + "id": "34deed93-d9d9-81b4-7983-0594fb829ae7", + "last_modified": 1519390914684 + }, + { + "schema": 1519242099615, + "blockID": "p1052", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=829111", + "who": "All users who have this version of the plugin installed.", + "why": "The Java plugin is causing significant security problems. All users are strongly recommended to keep the plugin disabled unless necessary.", + "name": "Java Plugin 7 update 11 (click-to-play), Mac OS X", + "created": "2015-11-06T13:30:02Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 11", + "minVersion": "Java 7 Update 11", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "c62a171f-7564-734f-0310-bae88c3baf85", + "last_modified": 1519390914657 + }, + { + "schema": 1519242099615, + "blockID": "p910", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1159917", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 45 to 78 (click-to-play), Linux", + "created": "2015-05-19T09:04:46Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 10\\.(4[5-9]|(5|6)\\d|7[0-8])(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "f322be12-1b08-0d57-ed51-f7a6d6ec2c17", + "last_modified": 1519390914634 + }, + { + "schema": 1519242099615, + "blockID": "p428", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=636633", + "who": "All Firefox users who have this plugin installed.", + "why": "The Java Deployment Toolkit plugin is known to be insecure and is unnecessary in most cases. Users should keep it disabled unless strictly necessary.", + "name": "Java Deployment Toolkit (click-to-play)", + "created": "2013-07-18T15:39:12Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 2 + } + ], + "matchFilename": "np[dD]eployJava1\\.dll", + "id": "d30a5b90-b84a-dfec-6147-fc04700a0d8b", + "last_modified": 1519390914610 + }, + { + "schema": 1519242099615, + "blockID": "p594", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=980355", + "who": "All Firefox users who have installed a version of the Cisco Web Communicator plugin lower than 3.0.6.", + "why": "Versions lower than 3.0.6 of the Cisco Web Communicator plugin are known to have security issues and should not be used. All users should update to version 3.0.6 or later. Find updates here.", + "name": "Cisco Web Communicator < 3.0.6 (Windows)", + "created": "2014-06-11T16:49:00Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 0, + "maxVersion": "3.0.5.99999999999999", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npCiscoWebCommunicator\\.dll", + "id": "f11f91d6-f65e-8a05-310b-ad20494a868a", + "last_modified": 1519390914588 + }, + { + "schema": 1519242099615, + "blockID": "p1064", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1217932", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 46 to 64 (click-to-play), Linux", + "created": "2015-12-02T12:42:34Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 11\\.(4[6-9]|5\\d|6[0-4])(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "d5cb1745-d144-5375-f3ff-cd603d4c2cee", + "last_modified": 1519390914565 + }, + { + "schema": 1519242099615, + "blockID": "p906", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1159917", + "who": "All users who have these versions of the plugin installed in Firefox.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 45 to 78 (click-to-play), Windows", + "created": "2015-05-19T09:02:45Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 7 U(4[5-9]|(5|6)\\d|7[0-8])(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "f254e5bc-12c7-7954-fe6b-8f1fdab0ae88", + "last_modified": 1519390914542 + }, + { + "schema": 1519242099615, + "blockID": "p240", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=821972", + "who": "All Firefox users who have versions of the DivX Web Player plugin lower than 1.4.", + "why": "Old versions of the DivX Web Player plugin are causing significant stability problems on Firefox 18 and above, on Mac OS X. All users should update their DivX software to the latest available version, available at divx.com.", + "name": "DivX Web Player 1.4 (DivX 7) and below", + "created": "2012-12-19T13:03:27Z" + }, + "enabled": true, + "versionRange": [ + { + "severity": 1, + "maxVersion": "1.4", + "minVersion": "0", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ] + } + ], + "matchFilename": "DivXBrowserPlugin\\.plugin", + "id": "d7f644bb-61aa-4594-f9fc-8dba7d9f3306", + "last_modified": 1519390914518 + }, + { + "schema": 1519242099615, + "blockID": "p962", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1183369", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 79 to 80 (click-to-play), Linux", + "created": "2015-07-15T10:51:12Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 10\\.(79|80)(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "7120e3c2-b293-65b5-2498-6451eab1e2c5", + "last_modified": 1519390914495 + }, + { + "schema": 1519242099615, + "blockID": "p1062", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1217932", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 8 update 46 to 64 (click-to-play), Windows", + "created": "2015-12-02T12:40:28Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 8 U(4[6-9]|5\\d|6[0-4])(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "a20c77af-721b-833b-8fa8-49091d1c69d4", + "last_modified": 1519390914472 + }, + { + "schema": 1519242099615, + "blockID": "p1143", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1259458", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 91 to 97 (click-to-play), Windows", + "created": "2016-03-31T16:17:32Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java\\(TM\\) Platform SE 7 U(9[1-7])(\\s[^\\d\\._U]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "npjp2\\.dll", + "id": "be53f658-e719-fa77-e7fe-6bd6086f888e", + "last_modified": 1519390914448 + }, + { + "schema": 1519242099615, + "blockID": "p954", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1183369", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 79 to 80 (click-to-play), Mac OS X", + "created": "2015-07-15T10:47:55Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "Java 7 Update 80", + "minVersion": "Java 7 Update 79", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "JavaAppletPlugin\\.plugin", + "id": "9b6830b4-a4b0-ee63-ee21-e0bd6ba0d6fe", + "last_modified": 1519390914424 + }, + { + "schema": 1519242099615, + "blockID": "p1246", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1286972", + "who": "All users who have this plugin installed.", + "why": "Old versions of the Adobe Reader plugin have known vulnerabilities. All users are strongly recommended to check for updates on our plugin check page.", + "name": "Adobe Reader 10.1.6 to 11.0.16", + "created": "2016-07-20T19:19:58Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/reader", + "versionRange": [ + { + "severity": 0, + "maxVersion": "11.0.16 ", + "minVersion": "10.1.6", + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "(nppdf32\\.dll)|(AdobePDFViewerNPAPI\\.plugin)", + "id": "4e29bd83-d074-bd40-f238-5ea38ff0d8d0", + "last_modified": 1519390914400 + }, + { + "schema": 1519242099615, + "blockID": "p1063", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1217932", + "who": "All users who have these versions of the Java plugin installed.", + "why": "Old versions of the Java plugin are potentially insecure and unstable. All users are strongly recommended to update on our plugin check page.", + "name": "Java Plugin 7 update 81 to 90 (click-to-play), Linux", + "created": "2015-12-02T12:41:34Z" + }, + "enabled": true, + "infoURL": "https://java.com/", + "matchName": "Java(\\(TM\\))? Plug-in 10\\.(8[1-9]|90)(\\.[0-9]+)?([^\\d\\._]|$)", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "57.0.*", + "minVersion": "0" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libnpjp2\\.so", + "id": "b6b9c6b9-b8c4-66a9-ed07-125b32e7a5ef", + "last_modified": 1519390914376 + }, + { + "schema": 1480349144394, + "blockID": "p1055", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1218880", + "who": "All users who have these versions of this plugin installed.", + "why": "Versions 12.2.0.162 and earlier of this plugin are affected by a critical security vulnerability that puts users at risk.", + "name": "Shockwave for Director 12.2.0.162 and earlier, Mac OS X (click-to-play)", + "created": "2015-11-13T14:17:24Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/shockwave/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "12.2.0.162", + "minVersion": "0", + "targetApplication": [], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "DirectorShockwave\\.plugin", + "id": "da3808c0-b460-e177-1c78-0496b3671480", + "last_modified": 1480349148897 + }, + { + "schema": 1480349144394, + "blockID": "p1054", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1218880", + "who": "All users who have these versions of this plugin installed.", + "why": "Versions 12.2.0.162 and earlier of this plugin are affected by a critical security vulnerability that puts users at risk.", + "name": "Shockwave for Director 12.2.0.162 and earlier, Windows (click-to-play)", + "created": "2015-11-13T14:15:12Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/shockwave/", + "versionRange": [ + { + "severity": 0, + "maxVersion": "12.2.0.162", + "minVersion": "0", + "targetApplication": [], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "np32dsw_[0-9]+\\.dll", + "id": "5cc4739b-ba83-7a19-4d85-c5362f5e0ccd", + "last_modified": 1480349148638 + }, + { + "schema": 1480349144394, + "blockID": "p330", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=854550", + "who": "All Firefox users who have these versions on the Abode Flash plugin installed.", + "why": "Old versions of the Adobe Flash plugin are potentially insecure and unstable. All users are recommended to visit our plugin check page and check for updates.", + "name": "Adobe Flash for Linux 10.3.182.* and lower (click-to-play)", + "created": "2013-03-26T09:43:25Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/flashplayer/", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "19.0a1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.16a1" + }, + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "17.0.*", + "minVersion": "17.0.4" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libflashplayer\\.so", + "matchDescription": "^Shockwave Flash (([1-9]\\.[0-9]+)|(10\\.([0-2]|(3 r(([0-9][0-9]?)|1(([0-7][0-9])|8[0-2]))))))( |$)", + "id": "abaff0eb-314f-e882-b96c-5c5a4755688f", + "last_modified": 1480349147797 + }, + { + "schema": 1480349144394, + "blockID": "p332", + "details": { + "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=854550", + "who": "All Firefox users who have these versions of the Adobe Flash Player plugin installed.", + "why": "Old versions of the Adobe Flash Player plugin are potentially insecure and unstable. All users are recommended to visit our plugin check to check for updates.", + "name": "Adobe Flash for Linux 11.0 to 11.1.* (click-to-play)", + "created": "2013-03-26T09:46:14Z" + }, + "enabled": true, + "infoURL": "https://get.adobe.com/flashplayer/", + "versionRange": [ + { + "severity": 0, + "targetApplication": [ + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "*", + "minVersion": "19.0a1" + }, + { + "guid": "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + "maxVersion": "*", + "minVersion": "2.16a1" + }, + { + "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "maxVersion": "17.0.*", + "minVersion": "17.0.4" + } + ], + "vulnerabilityStatus": 1 + } + ], + "matchFilename": "libflashplayer\\.so", + "matchDescription": "^Shockwave Flash 11.(0|1) r[0-9]{1,3}$", + "id": "38797744-cb92-1a49-4c81-2a900691fdba", + "last_modified": 1480349146047 + } + ], + "timestamp": 1603126502200 +} diff --git a/services/settings/dumps/gen_last_modified.py b/services/settings/dumps/gen_last_modified.py new file mode 100644 index 0000000000..d03f8fc096 --- /dev/null +++ b/services/settings/dumps/gen_last_modified.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# 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 glob +import json +import os + +import buildconfig +import mozpack.path as mozpath + + +def get_last_modified(full_path_to_remote_settings_dump_file): + """ + Get the last_modified for the given file name. + - File must exist + - Must be a JSON dictionary with a data list and a timestamp, + e.g. `{"data": [], "timestamp": 42}` + - Every element in `data` should contain a "last_modified" key. + - The first element must have the highest "last_modified" value. + """ + with open(full_path_to_remote_settings_dump_file, "r", encoding="utf-8") as f: + changeset = json.load(f) + + records = changeset["data"] + assert isinstance(records, list) + last_modified = changeset.get("timestamp") + assert isinstance( + last_modified, int + ), f"{full_path_to_remote_settings_dump_file} is missing the timestamp. See Bug 1725660" + + return last_modified + + +def main(output): + """ + Generates a JSON file that maps "bucket/collection" to the last_modified + value within. + + Returns a set of the file locations of the recorded RemoteSettings dumps, + so that the build backend can invoke this script again when needed. + + The validity of the JSON file is verified through unit tests at + services/settings/test/unit/test_remote_settings_dump_lastmodified.js + """ + # The following build targets currently use RemoteSettings dumps: + # Firefox https://searchfox.org/mozilla-central/rev/94d6086481754e154b6f042820afab6bc9900a30/browser/installer/package-manifest.in#281-285 # NOQA: E501 + # Firefox for Android https://searchfox.org/mozilla-central/rev/94d6086481754e154b6f042820afab6bc9900a30/mobile/android/installer/package-manifest.in#88-91 # NOQA: E501 + # Thunderbird https://searchfox.org/comm-central/rev/89f957706bbda77e5f34e64e117e7ce121bb5d83/mail/installer/package-manifest.in#280-285 # NOQA: E501 + # SeaMonkey https://searchfox.org/comm-central/rev/89f957706bbda77e5f34e64e117e7ce121bb5d83/suite/installer/package-manifest.in#307-309 # NOQA: E501 + assert buildconfig.substs["MOZ_BUILD_APP"] in ( + "browser", + "mobile/android", + "comm/mail", + "comm/suite", + ), ( + "Cannot determine location of Remote Settings " + f"dumps for platform {buildconfig.substs['MOZ_BUILD_APP']}" + ) + + dumps_locations = [] + if buildconfig.substs["MOZ_BUILD_APP"] == "browser": + dumps_locations += ["services/settings/dumps/"] + elif buildconfig.substs["MOZ_BUILD_APP"] == "mobile/android": + dumps_locations += ["services/settings/dumps/"] + elif buildconfig.substs["MOZ_BUILD_APP"] == "comm/mail": + dumps_locations += ["services/settings/dumps/"] + dumps_locations += ["comm/mail/app/settings/dumps/"] + elif buildconfig.substs["MOZ_BUILD_APP"] == "comm/suite": + dumps_locations += ["services/settings/dumps/"] + + remotesettings_dumps = {} + for dumps_location in dumps_locations: + dumps_root_dir = mozpath.join(buildconfig.topsrcdir, *dumps_location.split("/")) + for path in glob.iglob(mozpath.join(dumps_root_dir, "*", "*.json")): + folder, filename = os.path.split(path) + bucket = os.path.basename(folder) + collection, _ = os.path.splitext(filename) + remotesettings_dumps[f"{bucket}/{collection}"] = path + + output_dict = {} + input_files = set() + + for key, input_file in remotesettings_dumps.items(): + input_files.add(input_file) + output_dict[key] = get_last_modified(input_file) + + json.dump(output_dict, output, sort_keys=True) + return input_files diff --git a/services/settings/dumps/main/anti-tracking-url-decoration.json b/services/settings/dumps/main/anti-tracking-url-decoration.json new file mode 100644 index 0000000000..dd336cbd96 --- /dev/null +++ b/services/settings/dumps/main/anti-tracking-url-decoration.json @@ -0,0 +1,11 @@ +{ + "data": [ + { + "token": "fbclid", + "schema": 1564436129080, + "id": "60e82333-914d-4cfa-95b1-5f034b5a704b", + "last_modified": 1564511755134 + } + ], + "timestamp": 1564511755134 +} diff --git a/services/settings/dumps/main/cookie-banner-rules-list.json b/services/settings/dumps/main/cookie-banner-rules-list.json new file mode 100644 index 0000000000..602b34a4b9 --- /dev/null +++ b/services/settings/dumps/main/cookie-banner-rules-list.json @@ -0,0 +1,9234 @@ +{ + "data": [ + { + "click": { + "optIn": "button#onetrust-accept-btn-handler", + "optOut": "button.onetrust-close-btn-handler", + "presence": "div#onetrust-consent-sdk" + }, + "schema": 1710174339269, + "cookies": {}, + "domains": [ + "fnac.be", + "fnac.ch", + "fnac.com", + "fnac.pt" + ], + "id": "2d821158-5945-4134-a078-56c6da4f678d", + "last_modified": 1710331175432 + }, + { + "click": { + "optIn": ".moove-gdpr-infobar-allow-all", + "optOut": ".moove-gdpr-infobar-reject-btn", + "presence": "#moove_gdpr_cookie_info_bar" + }, + "schema": 1710174339269, + "cookies": { + "optIn": [ + { + "name": "moove_gdpr_popup", + "value": "%7B%22strict%22%3A%221%22%2C%22thirdparty%22%3A%221%22%2C%22advanced%22%3A%221%22%7D" + } + ], + "optOut": [ + { + "name": "moove_gdpr_popup", + "value": "%7B%22strict%22%3A%221%22%2C%22thirdparty%22%3A%220%22%2C%22advanced%22%3A%220%22%7D" + } + ] + }, + "domains": [ + "endorfy.com" + ], + "id": "1be1e4d7-bcad-4fc8-a348-ae64af8bcac7", + "last_modified": 1710331175428 + }, + { + "click": { + "optIn": "#popin_tc_privacy_button", + "optOut": "#popin_tc_privacy_button_3", + "presence": "#tc-privacy-wrapper" + }, + "schema": 1710174339269, + "domains": [ + "dpd.com", + "dpdgroup.com" + ], + "id": "43ad2b6b-a57b-4f7a-9d76-e32e696ddc10", + "last_modified": 1710331175424 + }, + { + "click": { + "optIn": "#popin_tc_privacy_button", + "optOut": "#popin_tc_privacy_button_2", + "presence": "#tc-privacy-wrapper" + }, + "schema": 1710174339269, + "domains": [ + "but.fr" + ], + "id": "3030D307-A610-4F3D-B589-D2BE133850D7", + "last_modified": 1710331175420 + }, + { + "click": { + "optIn": "#popin_tc_privacy_button_3", + "optOut": "#popin_tc_privacy_button_2", + "presence": "#tc-privacy-wrapper" + }, + "schema": 1710174339269, + "domains": [ + "fortuneo.fr", + "lcl.fr" + ], + "id": "98D89E26-F4B6-4C2D-BABF-4295B922E433", + "last_modified": 1710331175416 + }, + { + "click": { + "optIn": ".ei_btn_typ_validate", + "optOut": ".ei_lb_btnskip", + "presence": "#cookieLB" + }, + "schema": 1710174339269, + "domains": [ + "creditmutuel.fr" + ], + "id": "1B309A53-16B4-4BBE-91F4-86260A15B8E7", + "last_modified": 1710331175412 + }, + { + "click": { + "optIn": "#wt-cli-accept-all-btn", + "presence": "#cookie-law-info-bar" + }, + "schema": 1710174339269, + "cookies": { + "optOut": [ + { + "name": "viewed_cookie_policy", + "value": "yes" + } + ] + }, + "domains": [ + "krux.tech", + "silentiumpc.com", + "spcgear.com" + ], + "id": "04e919eb-13c2-4b37-bf7f-888767888640", + "last_modified": 1710331175407 + }, + { + "click": { + "optIn": "#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "optOut": "#CybotCookiebotDialogBodyLevelButtonLevelOptinDeclineAll", + "presence": "#cookiebanner" + }, + "schema": 1710174339269, + "domains": [ + "action.com", + "gog.com" + ], + "id": "04afc564-14b2-4c56-b72d-47a26e121f3b", + "last_modified": 1710331175402 + }, + { + "click": { + "optIn": ".cc-allow", + "optOut": ".cc-deny", + "presence": ".cc-window" + }, + "schema": 1710174339269, + "cookies": { + "optIn": [ + { + "name": "eclipse_cookieconsent_status", + "value": "allow" + } + ], + "optOut": [ + { + "name": "eclipse_cookieconsent_status", + "value": "deny" + } + ] + }, + "domains": [ + "adoptium.net", + "eclipse.dev", + "eclipse.org", + "glassfish.org", + "jakarta.ee", + "mbse-capella.org", + "oniroproject.org", + "open-vsx.org", + "openmdm.org", + "osgi.org", + "planeteclipse.org" + ], + "id": "92361e84-664e-46b3-ae55-95bc185dc88e", + "last_modified": 1710331175397 + }, + { + "click": { + "optIn": ".cc-allow", + "optOut": ".cc-dismiss", + "presence": ".cc-window" + }, + "schema": 1710174339269, + "domains": [ + "dell.com", + "dell.eu", + "delltechnologies.com" + ], + "id": "f1849b07-95e8-4ae0-a99d-24df5abbb3cb", + "last_modified": 1710331175393 + }, + { + "click": { + "optIn": "#btn-allowAllCookie", + "optOut": "#btn-rejectAllCookie", + "presence": "#cc-window" + }, + "schema": 1710174339269, + "cookies": { + "optIn": [ + { + "name": "_youtube_vimeo_vid", + "value": "allow" + }, + { + "name": "cookieControllerStatus", + "value": "allow" + }, + { + "name": "functionalCookieStatus", + "value": "allow" + }, + { + "name": "googleAnalyticsCookieStatus", + "value": "allow" + } + ], + "optOut": [ + { + "name": "_youtube_vimeo_vid", + "value": "deny" + }, + { + "name": "cookieControllerStatus", + "value": "deny" + }, + { + "name": "functionalCookieStatus", + "value": "deny" + }, + { + "name": "googleAnalyticsCookieStatus", + "value": "deny" + } + ] + }, + "domains": [ + "verbatim.co.il" + ], + "id": "3c0e4924-29ee-4d9a-99ec-e4805a7ffed9", + "last_modified": 1710331175385 + }, + { + "click": { + "optIn": "[data-cookiebanner=\"accept_button\"]", + "optOut": "[data-cookiebanner=\"accept_only_essential_button\"]", + "presence": "[data-testid=\"cookie-policy-manage-dialog\"]" + }, + "schema": 1710174339269, + "domains": [ + "facebook.com", + "messenger.com", + "oculus.com", + "workplace.com" + ], + "id": "d1d8ba36-ced7-4453-8b17-2e051e0ab1eb", + "last_modified": 1710331175381 + }, + { + "click": { + "optIn": "#cookiescript_accept", + "optOut": "#cookiescript_reject", + "presence": "#cookiescript_injected_wrapper" + }, + "schema": 1710174339269, + "domains": [ + "deskmodder.de", + "zyxel.com" + ], + "id": "7AF1C34C-750E-44FC-A3C4-31FB61EBF71B", + "last_modified": 1710331175376 + }, + { + "click": { + "optIn": ".sp_choice_type_11", + "presence": ".message-container > #notice", + "runContext": "child" + }, + "schema": 1710174339269, + "cookies": {}, + "domains": [ + "thetimes.co.uk", + "qz.com", + "privacy-mgmt.com", + "independent.co.uk", + "gizmodo.com", + "e24.no", + "thesun.co.uk", + "thesun.ie", + "focus.de", + "bild.de", + "computerbild.de", + "t-online.de", + "wetteronline.de", + "chip.de", + "n-tv.de", + "newsnow.co.uk", + "telegraph.co.uk", + "theguardian.com", + "faz.net", + "sueddeutsche.de", + "rtl.de", + "gutefrage.net", + "express.de", + "tvspielfilm.de", + "finanzen.net", + "tag24.de", + "kino.de", + "heise.de", + "bunte.de", + "golem.de", + "meinestadt.de", + "berliner-zeitung.de", + "karlsruhe-insider.de", + "wetter.de" + ], + "id": "d42bbaee-f96e-47e7-8e81-efc642518e97", + "last_modified": 1710331175368 + }, + { + "click": { + "optIn": "button#didomi-notice-agree-button", + "optOut": "span.didomi-continue-without-agreeing", + "presence": "div#didomi-popup" + }, + "schema": 1710174339269, + "cookies": {}, + "domains": [ + "theconversation.com", + "leparisien.fr", + "lesechos.fr", + "numerama.com", + "jofogas.hu", + "orange.fr", + "meteofrance.com", + "subito.it", + "hasznaltauto.hu", + "zdnet.de", + "intersport.fr", + "decathlon.fr", + "leboncoin.fr", + "boursorama.com", + "boursobank.com", + "intermarche.com" + ], + "id": "c1d7be10-151e-4a66-b83b-31a762869a97", + "last_modified": 1710331175364 + }, + { + "click": { + "optOut": ".sp_choice_type_13", + "presence": ".message-container > #notice", + "runContext": "child" + }, + "schema": 1710174339269, + "cookies": {}, + "domains": [ + "aktuality.sk", + "sky.it", + "azet.sk", + "bloomberg.com" + ], + "id": "ae8f7761-35ff-45b2-92df-7868ca288ad2", + "last_modified": 1710331175359 + }, + { + "click": { + "optIn": "button#didomi-notice-agree-button", + "presence": "div#didomi-host" + }, + "schema": 1710174339269, + "cookies": {}, + "domains": [ + "echo24.cz", + "jeuxvideo.com", + "24sata.hr", + "nova.cz", + "lidovky.cz", + "jutarnji.hr", + "vecernji.hr", + "sport.es", + "elespanol.com", + "in-pocasi.cz", + "20minutes.fr", + "actu.fr", + "hbvl.be", + "naszemiasto.pl", + "rtbf.be", + "20minutos.es", + "sudinfo.be", + "elpais.com", + "sinoptik.bg", + "lequipe.fr", + "abc.es", + "gva.be", + "eltiempo.es", + "eldiario.es", + "larazon.es", + "extra.cz", + "ladepeche.fr", + "marmiton.org", + "poslovni.hr", + "softonic.com", + "sydsvenskan.se", + "telecinco.es", + "giphy.com", + "filmstarts.de", + "funda.nl", + "idnes.cz", + "aktualne.cz", + "blesk.cz", + "centrum.cz", + "denik.cz", + "csfd.cz", + "hn.cz", + "moviepilot.de" + ], + "id": "af4c5b38-d210-472b-9a07-21cbe53c85ab", + "last_modified": 1710331175354 + }, + { + "click": { + "optIn": "button#didomi-notice-agree-button", + "optOut": "button#didomi-notice-disagree-button", + "presence": "div#didomi-host" + }, + "schema": 1710174339269, + "cookies": {}, + "domains": [ + "doctolib.fr", + "pravda.sk", + "topky.sk", + "zoznam.sk", + "tvnoviny.sk", + "aukro.cz", + "krone.at", + "cas.sk", + "heureka.sk", + "free.fr", + "markiza.sk", + "willhaben.at", + "francetvinfo.fr", + "france24.com", + "opodo.at", + "opodo.ch", + "opodo.co.uk", + "opodo.de", + "opodo.dk", + "opodo.fi", + "opodo.fr", + "opodo.it", + "opodo.nl", + "opodo.no", + "opodo.pl", + "opodo.pt", + "radiofrance.fr" + ], + "id": "690aa076-4a8b-48ec-b52c-1443d44ff008", + "last_modified": 1710331175349 + }, + { + "click": { + "optOut": "button[data-js-item=\"privacy-protection-default\"]", + "presence": ".c-privacy-protection-banner" + }, + "schema": 1708699541450, + "cookies": {}, + "domains": [ + "bundeswehr.de" + ], + "id": "52ad1edd-5696-482c-855d-a8d669f9e7f5", + "last_modified": 1708772697295 + }, + { + "click": {}, + "schema": 1708699541450, + "cookies": { + "optOut": [ + { + "name": "cookiehint", + "value": "{\"matomo\":0}" + } + ] + }, + "domains": [ + "dataport.de" + ], + "id": "1ae88c1b-2b26-49b2-98d0-f6b14a85e376", + "last_modified": 1708772697291 + }, + { + "click": {}, + "schema": 1708699541450, + "cookies": { + "optOut": [ + { + "name": "dp-cookie-consent", + "value": "false" + } + ] + }, + "domains": [ + "bob-sh.de", + "bolapla-sh.de" + ], + "id": "a70791ff-0001-4238-b68f-9ba8f1c00c96", + "last_modified": 1708772697287 + }, + { + "click": {}, + "schema": 1708699541450, + "cookies": { + "optOut": [ + { + "name": "BayernMatomo", + "value": "deaktiviert" + } + ] + }, + "domains": [ + "bayern.de" + ], + "id": "07228673-9d62-4c00-b18d-37e0fcfd37e7", + "last_modified": 1708772697283 + }, + { + "click": {}, + "schema": 1708699541450, + "cookies": { + "optOut": [ + { + "name": "gsbbanner", + "value": "closed" + } + ] + }, + "domains": [ + "bmbf.de", + "bmj.de", + "bva.bund.de", + "bundesrechnungshof.de", + "schleswig-holstein.de", + "zoll.de" + ], + "id": "b2a17900-dc1f-4273-bb0e-10e73c6f63bd", + "last_modified": 1708772697279 + }, + { + "click": {}, + "schema": 1708699541450, + "cookies": { + "optOut": [ + { + "name": "isTrackingConsentGiven", + "value": "false" + } + ] + }, + "domains": [ + "bund.de" + ], + "id": "431be341-e09a-4fb5-beac-3c4366caaaa0", + "last_modified": 1708772697274 + }, + { + "click": { + "optIn": "#privacy-init-wall-button-accept", + "optOut": "#privacy-init-wall-button-deny", + "presence": "#privacy-init-wall" + }, + "schema": 1708699541450, + "domains": [ + "comdirect.de" + ], + "id": "5DC4F89B-2A66-4B91-AE73-87954F3A0B4A", + "last_modified": 1708772697270 + }, + { + "click": { + "optIn": "#ccmgt_explicit_accept", + "presence": "#GDPRConsentManagerContainer" + }, + "schema": 1708699541450, + "domains": [ + "stepstone.de" + ], + "id": "3FD9FDA8-5F69-46C4-BA33-35FE3C804B4C", + "last_modified": 1708772697256 + }, + { + "click": { + "optIn": "[data-testid=\"as24-cmp-accept-all-button\"]", + "presence": "#as24-cmp-popup" + }, + "schema": 1708699541450, + "domains": [ + "autoscout24.at", + "autoscout24.de" + ], + "id": "46FECFD3-2384-450A-8CCC-53BCFE65DF4C", + "last_modified": 1708772697251 + }, + { + "click": { + "optIn": ".lnc-acceptCookiesButton", + "optOut": ".lnc-declineCookiesButton", + "presence": ".lnc-firstRunPopup" + }, + "schema": 1708699541450, + "domains": [ + "jw.org" + ], + "id": "45AEAD27-6328-4B26-A235-4EA4D74A829E", + "last_modified": 1708772697247 + }, + { + "click": { + "optIn": ".cmp-accept", + "presence": "#cmp-modal" + }, + "schema": 1708699541450, + "domains": [ + "gamestar.de" + ], + "id": "7D7FA535-719F-4637-8DE0-9F90B17AD7A7", + "last_modified": 1708772697243 + }, + { + "click": { + "optIn": "#consent_wall_optin", + "optOut": "#consent_wall_optout", + "presence": "#consent-wall" + }, + "schema": 1708699541450, + "domains": [ + "1und1.de" + ], + "id": "C553BBDC-632F-4F18-AA7D-D6C090E7A7A2", + "last_modified": 1708772697238 + }, + { + "click": { + "optIn": ".ccm--save-settings", + "optOut": ".ccm--decline-cookies", + "presence": "#ccm-widget" + }, + "schema": 1708699541450, + "domains": [ + "kaufmich.com" + ], + "id": "72A1389D-943D-4BF0-93D5-F274F7FD3CF2", + "last_modified": 1708772697234 + }, + { + "click": { + "optIn": ".kick__data-grid__main .kick__btn", + "presence": "#kick__logi-container" + }, + "schema": 1708699541450, + "domains": [ + "kicker.de" + ], + "id": "E220B441-CAE6-4E4B-9F5D-BA167AE06812", + "last_modified": 1708772697230 + }, + { + "click": { + "optIn": "#cookieBtnAll", + "optOut": "#cookieBtnContinue", + "presence": ".cookie_consent_withings" + }, + "schema": 1708699541450, + "domains": [ + "withings.com" + ], + "id": "0132691F-247B-4CB9-BF9B-0EB61B7435F3", + "last_modified": 1708772697226 + }, + { + "click": { + "optIn": "button#onetrust-accept-btn-handler", + "optOut": ".ot-pc-refuse-all-handler, #onetrust-reject-all-handler", + "presence": "div#onetrust-consent-sdk" + }, + "schema": 1708699541450, + "cookies": {}, + "domains": [ + "espncricinfo.com", + "blackboard.com", + "roche.com", + "apnews.com", + "nationalgeographic.com", + "espn.com", + "hotjar.com", + "marriott.com", + "hootsuite.com", + "wattpad.com", + "gamespot.com", + "apa.org", + "opendns.com", + "epicgames.com", + "zendesk.com", + "drei.at", + "ikea.com", + "search.ch", + "centrum.sk", + "zoom.us", + "pluska.sk", + "hp.com", + "ceskatelevize.cz", + "telenet.be", + "adobe.com", + "rottentomatoes.com", + "dhl.com", + "dhl.de", + "nvidia.com", + "cloudflare.com", + "webex.com", + "indeed.com", + "discord.com", + "sport.ro", + "ricardo.ch", + "stirileprotv.ro", + "1177.se", + "meinbezirk.at", + "orange.ro", + "ica.se", + "flashscore.pl", + "kuleuven.be", + "tutti.ch", + "post.at", + "rezultati.com", + "nbg.gr", + "behance.net", + "zemanta.com", + "grammarly.com", + "usatoday.com", + "cnet.com", + "npr.org", + "binance.com", + "linktr.ee", + "time.com", + "cisco.com", + "udemy.com", + "shutterstock.com", + "investopedia.com", + "cbsnews.com", + "okta.com", + "appsflyer.com", + "typepad.com", + "calendly.com", + "verisign.com", + "outbrain.com", + "zdnet.com", + "deloitte.com", + "hdfcbank.com", + "media.net", + "docker.com", + "avast.com", + "bluehost.com", + "nba.com", + "hostgator.com", + "scientificamerican.com", + "aljazeera.com", + "sahibinden.com", + "rackspace.com", + "namecheap.com", + "people.com", + "branch.io", + "tv2.dk", + "criteo.com", + "trustpilot.com", + "hm.com", + "mailchimp.com", + "surveymonkey.com", + "mckinsey.com", + "rollingstone.com", + "slate.com", + "dictionary.com", + "coursera.org", + "msn.com", + "chegg.com", + "variety.com", + "cnn.com", + "proximus.be", + "adevarul.ro", + "cnbc.com", + "oe24.at", + "reuters.com", + "booking.com", + "bluewin.ch", + "viaplay.dk", + "aib.ie", + "hbomax.com", + "rtlnieuws.nl", + "buienradar.be", + "viaplay.se", + "antena3.ro", + "statista.com", + "pixabay.com", + "constantcontact.com", + "atlassian.com", + "bmj.com", + "trendyol.com", + "meetup.com", + "vmware.com", + "bitbucket.org", + "viaplay.no", + "asana.com", + "freepik.com", + "heute.at", + "mtvuutiset.fi", + "buienradar.nl", + "nypost.com", + "panasonic.com", + "safeway.com", + "amd.com", + "atg.se", + "brother.de", + "brother.eu", + "brother.fr", + "corsair.com", + "crucial.com", + "dc.com", + "dn.no", + "epson.de", + "epson.es", + "epson.eu", + "epson.fr", + "epson.it", + "evga.com", + "fortnite.com", + "fujitsu.com", + "global.canon", + "gpuopen.com", + "info.lidl", + "inpost.es", + "inpost.eu", + "inpost.it", + "intel.com", + "kaufland.de", + "lg.com", + "lidl.co.uk", + "lidl.com", + "lidl.cz", + "lidl.de", + "lidl.fr", + "lidl.it", + "lidl.pl", + "lifewire.com", + "logitech.com", + "micron.com", + "mythomson.com", + "oki.com", + "otto.de", + "razer.com", + "rightmove.co.uk", + "sbb.ch", + "seagate.com", + "soundcloud.com", + "trello.com", + "unrealengine.com", + "askubuntu.com", + "mathoverflow.net", + "serverfault.com", + "stackapps.com", + "stackexchange.com", + "stackoverflow.com", + "superuser.com" + ], + "id": "6c7366a0-4762-47b9-8eeb-04e86cc7a0cc", + "last_modified": 1708772697220 + }, + { + "click": { + "optIn": ".cc-btn.cc-allow", + "optOut": ".cc-btn.cc-deny", + "presence": ".cc-window.cc-banner" + }, + "schema": 1708732808372, + "cookies": { + "optOut": [ + { + "name": "cookieconsent_status", + "value": "deny" + } + ] + }, + "domains": [ + "magnite.com", + "mecklenburg-vorpommern.de", + "omv.com", + "omv.at", + "omv.bg", + "omv.de", + "omv.nz", + "omv.no", + "omv.ro", + "omv.co.rs", + "omv.sk", + "omv.cz", + "omv.tn", + "omv.ae", + "omv.hu", + "omv.si", + "omv-gas.com", + "omv-gas.at", + "omv-gas.be", + "omv-gas.de", + "omv-gas.hu", + "omv-gas.nl", + "omv-gas.com.tr", + "omv-gas-storage.com", + "omvpetrom.com", + "petrom.ro", + "petrom.md", + "avanti.at", + "avanti-tankstellen.de", + "diskonttanken.at", + "tocimceneje.si" + ], + "id": "8f401b10-02b6-4e05-88fa-c37012d4c8c0", + "last_modified": 1708772697214 + }, + { + "click": {}, + "schema": 1708732808372, + "cookies": { + "optOut": [ + { + "name": "cookiebanner", + "value": "closed" + } + ] + }, + "domains": [ + "thw.de", + "service.bund.de" + ], + "id": "58226c30-e975-42f3-99e4-ca140b91e96c", + "last_modified": 1708772697207 + }, + { + "click": { + "optIn": ".js-accept-cookies", + "optOut": ".js-reject-cookies", + "presence": ".js-consent-banner" + }, + "schema": 1708699541450, + "cookies": { + "optOut": [ + { + "name": "OptanonAlertBoxClosed", + "value": "2030-01-01T00:00:00.000Z" + } + ] + }, + "domains": [ + "stackoverflow.blog", + "stackoverflow.co", + "stackoverflowteams.com" + ], + "id": "71443ce9-15b8-4e07-b51b-1f2158521ea9", + "last_modified": 1708772697196 + }, + { + "schema": 1708732808372, + "cookies": { + "optOut": [ + { + "name": "js-cookie-opt-in__consent", + "value": "needed" + } + ] + }, + "domains": [ + "anexia.com", + "netcup.com", + "netcup.de", + "netcup.eu", + "netcup-news.de", + "netcup-sonderangebote.de" + ], + "id": "dea34d82-9c05-4c08-9262-18a7f62be91e", + "last_modified": 1708772697190 + }, + { + "click": {}, + "schema": 1708699541450, + "cookies": { + "optOut": [ + { + "name": "cookie-allow-necessary", + "value": "1" + }, + { + "name": "cookie-allow-tracking", + "value": "0" + }, + { + "name": "cookie-banner", + "value": "hide" + } + ] + }, + "domains": [ + "bundesfinanzministerium.de", + "bundesregierung.de" + ], + "id": "84f68b1a-18a2-478c-bc8e-9ec32d4e3e80", + "last_modified": 1708772697179 + }, + { + "click": { + "optIn": "a.cc-dismiss", + "presence": "div.cc-compliance" + }, + "schema": 1708699541450, + "cookies": { + "optIn": [ + { + "name": "cookieconsent_status", + "value": "dismiss" + } + ] + }, + "domains": [ + "021.rs", + "photobucket.com", + "brandenburg.de" + ], + "id": "5371fb3e-3242-4864-9443-62116afe5f3c", + "last_modified": 1708772697173 + }, + { + "click": { + "optIn": "a.cmpboxbtnyes ", + "presence": "div#cmpbox" + }, + "schema": 1708699541450, + "cookies": {}, + "domains": [ + "hoerzu.de", + "nzz.ch" + ], + "id": "a2404864-0163-4f71-ab4c-915713f8f349", + "last_modified": 1708772697167 + }, + { + "click": { + "optIn": ".btn-ok", + "optOut": ".btn-reject", + "presence": "#cookie-policy-info" + }, + "schema": 1708699541450, + "domains": [ + "asus.com" + ], + "id": "8c949b75-4c7b-4559-8ade-780064af370a", + "last_modified": 1708772697161 + }, + { + "click": { + "optIn": "[data-cookieman-accept-all]", + "optOut": "[data-cookieman-accept-none]", + "presence": "#cookieman-modal" + }, + "schema": 1704727928035, + "domains": [ + "hfm-frankfurt.de" + ], + "id": "5FF5EEB9-1045-4ACF-8A12-0630B71063F6", + "last_modified": 1704881609345 + }, + { + "click": {}, + "schema": 1704727928035, + "cookies": { + "optOut": [ + { + "name": "paydirektCookieAllowed", + "value": "false" + }, + { + "name": "paydirektCookieAllowedPWS", + "value": "{%22necessary%22:true,%22analytics%22:false}" + } + ] + }, + "domains": [ + "paydirekt.de" + ], + "id": "769dcf5b-afd1-438d-940d-3069ff4b2f51", + "last_modified": 1704881609341 + }, + { + "click": { + "optIn": "button.iubenda-cs-accept-btn", + "optOut": "button.iubenda-cs-reject-btn", + "presence": "div#iubenda-cs-banner" + }, + "schema": 1704727928035, + "cookies": {}, + "domains": [ + "giallozafferano.it", + "virgilio.it", + "upfit.de", + "treedom.net" + ], + "id": "65638975-8222-425c-9be0-3f41a51db13c", + "last_modified": 1704881609336 + }, + { + "click": { + "optOut": "#zdf-cmp-deny-btn", + "presence": ".zdf-cmp-modal-content" + }, + "schema": 1704727928035, + "cookies": {}, + "domains": [ + "zdf.de", + "3sat.de" + ], + "id": "91484461-01AD-4D78-9ED8-D17C688F47E7", + "last_modified": 1704881609324 + }, + { + "click": { + "optIn": ".root__OblK1:not(.secondaryButton__N1rJw)", + "optOut": ".root__OblK1.secondaryButton__N1rJw", + "presence": ".root__XMKIj" + }, + "schema": 1704727928035, + "cookies": {}, + "domains": [ + "forbes.com" + ], + "id": "30293090-f064-473a-ae0f-cd390507c1c7", + "last_modified": 1704881609320 + }, + { + "click": { + "optIn": "[href=\"#accept\"]", + "optOut": "[href=\"#refuse\"]", + "presence": "div#cookie-consent-banner" + }, + "schema": 1704727928035, + "cookies": { + "optOut": [ + { + "name": "cck1", + "value": "%7B%22cm%22%3Atrue%2C%22all1st%22%3Afalse%2C%22closed%22%3Atrue%7D" + } + ] + }, + "domains": [ + "europa.eu" + ], + "id": "0c74749a-8c53-4bb0-b31a-ab2f89b7f493", + "last_modified": 1704881609316 + }, + { + "click": { + "optIn": ".CookieBanner_buttonContainer__NOZxH:nth-child(1) button", + "optOut": ".CookieBanner_buttonContainer__NOZxH:nth-child(2) button", + "presence": ".CookieBanner_cookieBanner__R_BOh" + }, + "schema": 1702080004246, + "domains": [ + "cbinsights.com" + ], + "id": "4CE0ADCF-E232-4F3B-981B-CA83A0C40874", + "last_modified": 1702633230760 + }, + { + "click": { + "optIn": ".modal-box__buttons--save-all", + "presence": ".new-policy-box" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "polityka15", + "value": "security_storage%3Dtrue%26functionality_storage%3Dtrue%26analytics_storage%3Dtrue%26personalization_storage%3Dtrue%26ad_storage%3Dtrue" + } + ], + "optOut": [ + { + "name": "polityka15", + "value": "security_storage%3Dtrue%26functionality_storage%3Dfalse%26analytics_storage%3Dfalse%26personalization_storage%3Dfalse%26ad_storage%3Dfalse" + } + ] + }, + "domains": [ + "nazwa.pl" + ], + "id": "ddff9528-161c-471e-bd2d-ba4d874a3931", + "last_modified": 1702633230742 + }, + { + "click": { + "optIn": ".cc-dismiss", + "optOut": ".cc-deny", + "presence": ".cc-window" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "cookieconsent_status", + "value": "dismiss" + } + ], + "optOut": [ + { + "name": "cookieconsent_status", + "value": "deny" + } + ] + }, + "domains": [ + "zotac.com" + ], + "id": "4159a3d2-f331-4d56-b051-a753c7e1308a", + "last_modified": 1702633230734 + }, + { + "click": { + "optIn": "[onclick*=\"cookie_Agree()\"]", + "optOut": "[onclick*=\"cookie_Disagree()\"]", + "presence": "#legal_notice" + }, + "schema": 1702080004246, + "domains": [ + "transcend-info.com" + ], + "id": "5e0387bb-1f19-4f61-b587-49d995a691c9", + "last_modified": 1702633230725 + }, + { + "click": { + "optIn": ".tp-cookie-accept-all", + "presence": "#tp-cookie" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "tp_privacy_base", + "value": "1" + }, + { + "name": "tp_privacy_marketing", + "value": "1" + } + ], + "optOut": [ + { + "name": "tp_privacy_base", + "value": "1" + } + ] + }, + "domains": [ + "tp-link.com" + ], + "id": "48e9b863-c642-4a7a-9ee5-c085d337233e", + "last_modified": 1702633230721 + }, + { + "click": { + "optIn": ".acceptAll", + "presence": ".privacyArea" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "privacy", + "value": "{\"necessary\":\"ok\",\"functional\":\"ok\",\"marketing\":\"ok\"}" + } + ], + "optOut": [ + { + "name": "privacy", + "value": "{\"necessary\":\"ok\"}" + } + ] + }, + "domains": [ + "teamgroupinc.com" + ], + "id": "9034f6e4-09ca-42a2-a8da-4f65968b8b36", + "last_modified": 1702633230717 + }, + { + "click": { + "optIn": ".btn_accept", + "presence": ".syno_cookie_element" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "syno_confirm_v4_answer", + "value": "{\"necessary\":true,\"performance\":true,\"functionality\":true,\"targeting\":true}" + } + ], + "optOut": [ + { + "name": "syno_confirm_v4_answer", + "value": "{\"necessary\":true,\"performance\":false,\"functionality\":false,\"targeting\":false}" + } + ] + }, + "domains": [ + "synology.cn", + "synology.com" + ], + "id": "d01204bd-8a94-4e6e-8ce4-d155b0681053", + "last_modified": 1702633230713 + }, + { + "click": { + "optIn": "#cp-yes", + "optOut": "#cp-no", + "presence": "#cp-overlay" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "cookiepermission", + "value": "yes" + } + ], + "optOut": [ + { + "name": "cookiepermission", + "value": "no" + } + ] + }, + "domains": [ + "seasonic.com" + ], + "id": "6c9b123a-ec42-4128-91a2-a4cdd65059bc", + "last_modified": 1702633230709 + }, + { + "click": { + "optIn": "[value=\"Accept\"]", + "optOut": "[value=\"Reject\"]", + "presence": "#pp_info" + }, + "schema": 1702080004246, + "domains": [ + "gainward.com", + "palit.com" + ], + "id": "c79fc6da-0143-46a0-abfc-debbd4d05f4b", + "last_modified": 1702633230705 + }, + { + "click": { + "optIn": "#Footer_butAccept", + "optOut": "#Footer_butCancel", + "presence": "#Footer_Cookie" + }, + "schema": 1702080004246, + "domains": [ + "cablexpert.be", + "cablexpert.com", + "cablexpert.de", + "cablexpert.gr", + "cablexpert.nl", + "energenie.com", + "gembird.be", + "gembird.com", + "gembird.com.pl", + "gembird.es", + "gembird.nl", + "gembird3.com", + "gembird3.nl", + "gmb-online.nl", + "gmb.nl" + ], + "id": "f7aa3175-3c2a-4458-b822-da0bd57c2524", + "last_modified": 1702633230701 + }, + { + "click": { + "optIn": "#Cookies_consent_btn", + "optOut": "#Cookies_consent_internal_btn", + "presence": "#qkies_info" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "cookiesconsent", + "value": "true" + } + ], + "optOut": [ + { + "name": "cookiesconsent", + "value": "internal" + } + ] + }, + "domains": [ + "dreammachines.by", + "dreammachines.eu", + "dreammachines.io", + "dreammachines.nl", + "dreammachines.pl", + "dreammachines.ru" + ], + "id": "c8cb50a3-7604-4de0-a0ac-c2e7b0ad45c0", + "last_modified": 1702633230692 + }, + { + "click": { + "optIn": "[name=\"accept\"]", + "presence": ".ck-notiz" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "cookieSettings", + "value": "%7B%22necessary%22%3Atrue%2C%22analytics%22%3Atrue%2C%22thirdParty%22%3Atrue%2C%22dismissed%22%3Atrue%7D" + } + ], + "optOut": [ + { + "name": "cookieSettings", + "value": "%7B%22necessary%22%3Atrue%2C%22analytics%22%3Afalse%2C%22thirdParty%22%3Afalse%2C%22dismissed%22%3Atrue%7D" + } + ] + }, + "domains": [ + "creative.com" + ], + "id": "32fc1292-e26f-49c8-8de8-c41966c0bd34", + "last_modified": 1702633230688 + }, + { + "click": { + "optIn": ".cookie-accept", + "optOut": "#cookie-accept-technical", + "presence": ".cookie-banner" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "_CookiePolicyHint", + "value": "true" + }, + { + "name": "cookie_functional", + "value": "on" + }, + { + "name": "cookie_marketing", + "value": "on" + } + ], + "optOut": [ + { + "name": "_CookiePolicyHint", + "value": "true" + } + ] + }, + "domains": [ + "bequiet.com" + ], + "id": "de38e8a0-25d2-42d2-974d-04684e54b7ce", + "last_modified": 1702633230684 + }, + { + "click": { + "optIn": "#cookiesPrivacyPolicyAllowBtn", + "optOut": "#cookiesPrivacyPolicyDenyBtn", + "presence": "#cookiesPrivacyPolicyContainerWrapper" + }, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "cookiesPrivacyPolicy", + "value": "1" + }, + { + "name": "cookiesPrivacyPolicyExtended", + "value": "1" + } + ], + "optOut": [ + { + "name": "cookiesPrivacyPolicy", + "value": "1" + }, + { + "name": "cookiesPrivacyPolicyExtended", + "value": "0" + } + ] + }, + "domains": [ + "akyga.com" + ], + "id": "3bf04e3c-efe8-49af-bf80-506f12ba2da4", + "last_modified": 1702633230681 + }, + { + "click": { + "optIn": "button.fcQwZX", + "optOut": "button.fLZgds", + "presence": ".kDNyTh.hbTFXs" + }, + "schema": 1702598407647, + "domains": [ + "androidpolice.com" + ], + "id": "5B57603A-0CE0-4511-B324-18D34F60EC51", + "last_modified": 1702633230676 + }, + { + "click": { + "optIn": "button[data-testid=\"allow-all-cookies-button\"]", + "optOut": "div[data-testid=\"cookie-popover\"] button:nth-child(2)", + "presence": "div[data-testid=\"cookie-popover\"]" + }, + "schema": 1702598407647, + "domains": [ + "ondo.finance" + ], + "id": "50105C82-FEDE-424E-884D-430FA7BCBED", + "last_modified": 1702633230671 + }, + { + "click": { + "optIn": "#gdpr-cookie-accept", + "presence": "#gdpr-cookie-message" + }, + "schema": 1702598407647, + "cookies": { + "optIn": [ + { + "name": "cookieControl", + "value": "true" + }, + { + "name": "cookieControlPrefs", + "value": "[\"analytics\",\"marketing\"]" + } + ], + "optOut": [ + { + "name": "cookieControl", + "value": "true" + }, + { + "name": "cookieControlPrefs", + "value": "[]" + } + ] + }, + "domains": [ + "teamspeak.com" + ], + "id": "8005fd7b-26ec-43c0-ad94-1c6834cc7905", + "last_modified": 1702633230667 + }, + { + "click": {}, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "happycow-cookie-policy", + "value": "1" + } + ], + "optOut": [ + { + "name": "happycow-cookie-policy", + "value": "0" + } + ] + }, + "domains": [ + "happycow.net" + ], + "id": "4710a874-0ff4-4072-8476-36a22d7f698e", + "last_modified": 1702633230662 + }, + { + "click": {}, + "schema": 1702080004246, + "cookies": { + "optIn": [ + { + "name": "cookiebanner_accepted", + "value": "1" + } + ] + }, + "domains": [ + "raspberrypi.com" + ], + "id": "61dfb6a1-21b4-4f95-aa6d-946eb09f8511", + "last_modified": 1702633230658 + }, + { + "click": { + "optOut": "button.js-decline-all-cookies", + "presence": "div.cookie-consent-spice" + }, + "schema": 1702080004246, + "cookies": {}, + "domains": [ + "thomann.de" + ], + "id": "5aa2d4df-2a5d-4abf-bb5a-bd714951f790", + "last_modified": 1702633230653 + }, + { + "click": { + "optIn": "button[data-testid=\"gdpr-btn-accept-all\"]", + "optOut": "button[data-testid=\"gdpr-btn-refuse-all\"]", + "presence": "[data-testid=\"cookie-banner\"]" + }, + "schema": 1702080004246, + "domains": [ + "deezer.com" + ], + "id": "5bdcb3ce-6270-4270-af7b-d1fdef5cecb4", + "last_modified": 1702633230649 + }, + { + "click": { + "optIn": "button[data-cookie_consent=\"1\"]", + "optOut": "button[data-cookie_consent=\"0\"]", + "presence": "#js_reveal_cookie_content" + }, + "schema": 1702080004246, + "cookies": {}, + "domains": [ + "voelkner.de" + ], + "id": "bcf09922-64d7-4879-974a-119e8bd05fee", + "last_modified": 1702633230645 + }, + { + "click": { + "optIn": "#onetrust-accept-btn-handler", + "presence": "div#onetrust-consent-sdk" + }, + "schema": 1702598407647, + "cookies": {}, + "domains": [ + "gitlab.com", + "speedtest.net", + "name.com", + "mlb.com", + "qualtrics.com", + "tim.it", + "hotnews.ro", + "mashable.com", + "pcmag.com", + "barnesandnoble.com", + "politico.com", + "quillbot.com", + "newyorker.com", + "upwork.com", + "mediafax.ro", + "elisa.fi", + "blick.ch", + "tvn24.pl", + "olx.pl", + "olx.bg", + "gsp.ro", + "fastly.com", + "spotify.com", + "20min.ch", + "olx.ro", + "olx.pt", + "sportal.bg", + "gazeta.pl", + "romaniatv.net", + "teamviewer.com", + "ted.com", + "tripadvisor.com", + "webmd.com", + "cambridge.org", + "investing.com", + "businesswire.com", + "istockphoto.com", + "iso.org", + "quizlet.com", + "genius.com", + "jstor.org", + "trendmicro.com", + "duolingo.com", + "sophos.com", + "rte.ie", + "euro.com.pl", + "wired.com", + "arstechnica.com", + "gartner.com", + "thelancet.com", + "weebly.com", + "irishtimes.com", + "libertatea.ro", + "otomoto.pl", + "sport.pl", + "novini.bg", + "stiripesurse.ro", + "suomi24.fi", + "ziare.com", + "irishexaminer.com", + "tripadvisor.it", + "thejournal.ie", + "superbet.ro", + "g4media.ro", + "wyborcza.pl", + "nachrichten.at", + "tt.com", + "three.ie", + "tripadvisor.co.uk", + "dcnews.ro", + "vol.at", + "plotek.pl", + "howstuffworks.com", + "tripadvisor.de", + "acer.com", + "allaboutcookies.org", + "bankier.pl", + "brother.co.uk", + "brother.es", + "brother.it", + "digicert.com", + "epson.co.uk", + "fiverr.com", + "frontiersin.org", + "glassdoor.com", + "global.brother", + "gq-magazine.co.uk", + "ingka.com", + "inpost.pl", + "instructure.com", + "komputronik.pl", + "mediaexpert.pl", + "oleole.pl", + "ookla.com", + "otodom.pl", + "play.pl", + "prnewswire.com", + "salesforce.com", + "slack.com", + "thawte.com", + "ui.com", + "uisp.com" + ], + "id": "256259D5-28AA-44C4-A837-8A30424005BB", + "last_modified": 1702633230640 + }, + { + "click": { + "optIn": "button[data-qa=\"privacy-settings-action-info\"]", + "optOut": "button[data-qa=\"privacy-settings-action-close\"]", + "presence": "[data-qa=\"privacy-settings\"]" + }, + "schema": 1702598407647, + "domains": [ + "lieferando.de", + "lieferando.at", + "just-eat.ch" + ], + "id": "31d9971f-e23d-4dd9-a891-99a85d97ad19", + "last_modified": 1702633230631 + }, + { + "click": { + "optIn": "button.osano-cm-accept-all", + "optOut": ".osano-cm-denyAll", + "presence": "div.osano-cm-dialog__buttons" + }, + "schema": 1702598407647, + "cookies": {}, + "domains": [ + "slideshare.net", + "ieee.org", + "linuxfoundation.eu" + ], + "id": "23710ccf-85e6-450e-953d-7ffc3f80bbf0", + "last_modified": 1702633230618 + }, + { + "click": { + "optIn": "button#popin_tc_privacy_button_2", + "presence": "div#popin_tc_privacy_container_button" + }, + "schema": 1702598407647, + "cookies": { + "optIn": [], + "optOut": [ + { + "name": "TC_PRIVACY", + "value": "1@006%7C86%7C3315@@@1665406595598%2C1665406595598%2C1680958595598@" + } + ] + }, + "domains": [ + "credit-agricole.fr", + "sparkasse.at" + ], + "id": "850ca0e7-372f-4c9f-bfbd-76d38a076cf7", + "last_modified": 1702633230606 + }, + { + "click": { + "optIn": "button#ccc-recommended-settings", + "optOut": "button#ccc-reject-settings", + "presence": "div#ccc" + }, + "schema": 1702598407647, + "cookies": {}, + "domains": [ + "metoffice.gov.uk", + "footballmanager.com", + "sigames.com" + ], + "id": "9e4d932a-eaf9-44e4-a3ac-55861d66a7c3", + "last_modified": 1702633230599 + }, + { + "click": {}, + "schema": 1700826062965, + "cookies": { + "optOut": [ + { + "name": "cookieDeclined", + "value": "1" + } + ] + }, + "domains": [ + "grundstoff.net" + ], + "id": "ca62d977-4e2e-48ab-a186-055f9f3277e4", + "last_modified": 1701423955158 + }, + { + "click": {}, + "schema": 1700826062965, + "cookies": { + "optOut": [ + { + "name": "onleiheTracking", + "value": "false" + } + ] + }, + "domains": [ + "onleihe.de" + ], + "id": "c19b1009-d609-438c-8d7c-82fc7144eeaa", + "last_modified": 1701423955154 + }, + { + "click": { + "optIn": "button.cm-btn-success", + "optOut": "button.cm-btn-danger", + "presence": "div#klaro" + }, + "schema": 1700826062965, + "cookies": {}, + "domains": [ + "karlsruhe.de" + ], + "id": "63b5c74c-67ae-47f2-b0ba-10c03132ad6f", + "last_modified": 1701423955150 + }, + { + "click": { + "optIn": "button.js-cookie-accept", + "optOut": "button.js-cookie-decline", + "presence": "div#toast-container" + }, + "schema": 1700826062965, + "domains": [ + "startnext.com" + ], + "id": "312f32e1-a6bf-4e87-b7a6-7480345823cf", + "last_modified": 1701423955146 + }, + { + "click": { + "optIn": ".consentAgree", + "optOut": "#consentDisagree", + "presence": "div.consent" + }, + "schema": 1700826062965, + "cookies": {}, + "domains": [ + "strato.de" + ], + "id": "18b1dd86-aee5-4697-b601-4fa320a75dbe", + "last_modified": 1701423955143 + }, + { + "click": {}, + "schema": 1700826062965, + "cookies": { + "optOut": [ + { + "name": "hidecookie", + "value": "true" + } + ] + }, + "domains": [ + "vrn.de" + ], + "id": "0ae7f461-8705-4222-b8ee-afa03e5150ff", + "last_modified": 1701423955139 + }, + { + "click": {}, + "schema": 1700826062965, + "cookies": { + "optOut": [ + { + "name": "consent_functional", + "value": "DENY" + }, + { + "name": "consent_marketing", + "value": "DENY" + }, + { + "name": "consent_technical", + "value": "ALLOW" + }, + { + "name": "consent_version", + "value": "2.6" + } + ] + }, + "domains": [ + "huk24.de" + ], + "id": "0fbacecc-fbda-4e4b-883f-9424790ccc74", + "last_modified": 1701423955135 + }, + { + "click": {}, + "schema": 1700826062965, + "cookies": { + "optOut": [ + { + "name": "cookie-preference", + "value": "1" + } + ] + }, + "domains": [ + "korodrogerie.de" + ], + "id": "cdd5646d-06b3-4fdf-8530-b7d8a93f03df", + "last_modified": 1701423955126 + }, + { + "click": { + "optIn": "button.cl-consent__btn", + "optOut": "button.cl-consent__close-link", + "presence": "div#cl-consent" + }, + "schema": 1700826062965, + "cookies": {}, + "domains": [ + "hdblog.it", + "hdmotori.it" + ], + "id": "342bd7ca-6502-4df7-bd3a-0b884b51aaa7", + "last_modified": 1701423955123 + }, + { + "click": {}, + "schema": 1700826062965, + "cookies": { + "optOut": [ + { + "name": "cookies_accepted", + "value": "false" + } + ] + }, + "domains": [ + "mindfactory.de" + ], + "id": "aa077f01-0574-4f1b-ad1b-3225c4dc59f7", + "last_modified": 1701423955119 + }, + { + "click": { + "optIn": "button#cookieok", + "optOut": "button#cookiecancel", + "presence": "div#cookieconsent" + }, + "schema": 1700826062965, + "cookies": {}, + "domains": [ + "reichelt.de" + ], + "id": "b81fc066-5cdd-4af6-b288-49de860e369a", + "last_modified": 1701423955111 + }, + { + "click": { + "optIn": "button#cookie-consent-button", + "presence": "dialog.consent" + }, + "schema": 1700826062965, + "domains": [ + "computerbase.de" + ], + "id": "2c73714d-0e9b-41a1-ad12-6bd15ddade67", + "last_modified": 1701423955107 + }, + { + "click": { + "optIn": "button.cm-btn-accept-all", + "optOut": "button.cm-btn-accept", + "presence": "div#klaro" + }, + "schema": 1700826062965, + "cookies": {}, + "domains": [ + "lancom-systems.de" + ], + "id": "efc6f62d-8d53-4f52-9dd3-172d9b04f5de", + "last_modified": 1701423955104 + }, + { + "click": { + "optIn": "button.iubenda-cs-accept-btn", + "presence": "div#iubenda-cs-banner" + }, + "schema": 1700826062965, + "cookies": {}, + "domains": [ + "ansa.it" + ], + "id": "8e55f0f4-262f-4d35-a461-47afea6e9069", + "last_modified": 1701423955100 + }, + { + "click": { + "optIn": "button[data-component-name=\"consent\"]", + "optOut": "button[data-component-name=\"reject\"]", + "presence": ".cookie-notice-banner" + }, + "schema": 1700826062965, + "cookies": {}, + "domains": [ + "shopify.com" + ], + "id": "531324c9-83ba-4ba3-a488-3ebde87b10af", + "last_modified": 1701423955091 + }, + { + "click": { + "optOut": "#tmart-cookie-layer-only-necessary", + "presence": ".cookieLayer" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "sparda-hessen.de" + ], + "id": "0EAF9E99-36C6-4165-87BE-A62EF1751E1D", + "last_modified": 1700826062722 + }, + { + "click": { + "optIn": "[data-testid=\"accept-all-cookies-button\"]", + "presence": "[data-testid=\"cookie-banner\"]" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "elsevier.com" + ], + "id": "0AB3A01E-10A9-4509-9350-6EF61AB223F3", + "last_modified": 1700826062716 + }, + { + "click": { + "optIn": "[data-testid=\"uc-accept-all-button\"]", + "optOut": "[data-testid=\"uc-deny-all-button\"]", + "presence": "#usercentrics-root" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "rts.ch" + ], + "id": "9f5f0c06-5221-45b2-a174-7d70fd128eb3", + "last_modified": 1700826062713 + }, + { + "click": { + "optIn": ".cky-btn.cky-btn-accept", + "optOut": ".cky-btn.cky-btn-reject", + "presence": ".cky-consent-bar" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "met.ie" + ], + "id": "536f8027-111f-4798-a9ef-745b30fe65c8", + "last_modified": 1700826062709 + }, + { + "click": { + "optIn": "#didomi-notice-agree-button", + "optOut": ".didomi-continue-without-agreeing", + "presence": "div#didomi-notice" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "orange.sk", + "hnonline.sk" + ], + "id": "688d29a8-e1c7-4d62-b3d4-53b451ff5a48", + "last_modified": 1700826062705 + }, + { + "click": { + "optIn": ".sp_choice_type_11", + "presence": ".message-container > #notice", + "runContext": "child", + "skipPresenceVisibilityCheck": true + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "hln.be", + "nu.nl", + "vg.no", + "aftonbladet.se", + "voetbalzone.nl", + "ad.nl", + "finn.no", + "sporza.be", + "derstandard.at", + "tori.fi", + "vrt.be", + "spiegel.de", + "aftenposten.no", + "vglive.no", + "oikotie.fi", + "klart.se", + "zeit.de", + "gala.de", + "gala.fr", + "volkskrant.nl", + "tek.no", + "omni.se", + "dpgmediagroup.com", + "economist.com", + "chefkoch.de", + "sport1.de", + "tagesspiegel.de", + "stern.de", + "sport.de", + "giga.de", + "2dehands.be", + "handelsblatt.com" + ], + "id": "c58a0bac-3a5f-43af-93e9-8b5462a6bcb5", + "last_modified": 1700826062689 + }, + { + "click": { + "optIn": "button.cb_accept", + "presence": "[data-module=\"cookieBanner\"]" + }, + "schema": 1700784006705, + "cookies": { + "optIn": [ + { + "host": "ok.ru", + "name": "cookieChoice", + "value": "\"PRIVACY,1,2,3\"", + "unsetValue": "\"\"" + } + ], + "optOut": [ + { + "host": "ok.ru", + "name": "cookieChoice", + "value": "PRIVACY", + "unsetValue": "\"\"" + } + ] + }, + "domains": [ + "ok.ru" + ], + "id": "384b70c3-2458-4dc9-ac6f-d9c5e90cc62a", + "last_modified": 1700826062685 + }, + { + "click": { + "optIn": "button#didomi-notice-agree-button", + "presence": "div#didomi-host" + }, + "schema": 1700784006705, + "cookies": { + "optOut": [ + { + "name": "euconsent-v2", + "value": "CPgejgAPgejgAAHABBENCjCgAAAAAAAAAATIAAAAAACBoAMAAQSCEQAYAAgkEKgAwABBIIZABgACCQQA.YAAAAAAAAAAA" + } + ] + }, + "domains": [ + "b92.net", + "index.hr", + "abv.bg", + "orf.at", + "expressen.se", + "nieuwsblad.be", + "independent.ie", + "telegraaf.nl", + "as.com", + "njuskalo.hr", + "slobodnadalmacija.hr", + "dnevnik.hr", + "nova.bg", + "mundodeportivo.com", + "dn.se", + "ouest-france.fr", + "standaard.be", + "heureka.cz", + "bfmtv.com", + "filmweb.pl", + "expresso.pt", + "lavanguardia.com", + "idealista.com", + "idealista.pt", + "di.se", + "hola.com", + "lesoir.be", + "vesti.bg", + "gloria.hr", + "huffingtonpost.es", + "rtl.be", + "gong.bg", + "okdiario.com", + "dhnet.be", + "lecturas.com", + "elperiodico.com", + "dumpert.nl" + ], + "id": "77885432-826b-4d03-bb3d-dfdd36015a82", + "last_modified": 1700826062681 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons-container" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "24chasa.bg" + ], + "id": "97ba1ec3-c8e7-45a0-845f-c732b71bd213", + "last_modified": 1700826062677 + }, + { + "click": { + "optIn": ".qxOn2zvg.e1sXLPUy", + "presence": ".ulheJb0a" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "wykop.pl" + ], + "id": "0caa99d5-2b67-44e1-bd45-509d8e785071", + "last_modified": 1700826062661 + }, + { + "click": { + "optIn": ".sc-1olg58b-0.bXsYmb.sc-1olg58b-1.KXemC", + "optOut": ".sc-1olg58b-0.jHiTgL.sc-1olg58b-1.sc-1hth3pd-7.KXemC.kSupJA", + "presence": ".sc-mgoo3k-0.jFxlDH" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "galaxus.de" + ], + "id": "cc78c082-2dc6-4287-9a7c-168c591810fd", + "last_modified": 1700826062657 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons-container" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "plovdiv24.bg" + ], + "id": "1dbf0b53-df44-472c-a126-13740e9d2e39", + "last_modified": 1700826062654 + }, + { + "click": { + "optIn": "button#gdpr-consent-banner-accept-button", + "presence": "div.gdpr-consent-modal-footer" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "marktplaats.nl" + ], + "id": "2cb11bb1-1d52-45c7-a764-9d2e2be6c310", + "last_modified": 1700826062650 + }, + { + "click": { + "optIn": ".css-15sam98", + "optOut": ".css-1tpwlwl", + "presence": ".css-1243dq3" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "figma.com" + ], + "id": "e0dd9ba6-6514-4618-a405-3b6458c13272", + "last_modified": 1700826062646 + }, + { + "click": { + "optIn": "[data-testid=\"cookie-wall-accept\"]", + "optOut": "[data-testid=\"cookie-wall-reject\"]", + "presence": "[data-testid=\"cookie-wall-modal\"]" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "change.org" + ], + "id": "1c3ab910-8635-4007-8ea3-8aeea0c56143", + "last_modified": 1700826062642 + }, + { + "click": { + "optIn": ".dialog-actions-accept-btn", + "optOut": ".dialog-actions-decline-btn", + "presence": "[data-testid=\"cookie-dialog-root\"]" + }, + "schema": 1700784006705, + "cookies": { + "optIn": [ + { + "name": "sq", + "value": "3" + } + ], + "optOut": [ + { + "name": "sq", + "value": "0" + } + ] + }, + "domains": [ + "nike.com" + ], + "id": "719a0a85-a706-4393-9668-0b7123891058", + "last_modified": 1700826062638 + }, + { + "click": { + "optIn": ".agree-button.eu-cookie-compliance-default-button", + "optOut": ".eu-cookie-compliance-default-button.eu-cookie-compliance-reject-button", + "presence": ".eu-cookie-compliance-banner.eu-cookie-compliance-banner-info.eu-cookie-compliance-banner--categories" + }, + "schema": 1700784006705, + "cookies": { + "optIn": [ + { + "name": "_hjFirstSeen", + "value": "1" + } + ] + }, + "domains": [ + "nokia.com" + ], + "id": "c18ccca7-1b7d-4022-b9ca-c725e6030c80", + "last_modified": 1700826062635 + }, + { + "click": { + "optIn": "#accept_all_cookies_button", + "optOut": "#decline_cookies_button", + "presence": "#ccpa_consent_banner", + "runContext": "child" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "dropbox.com" + ], + "id": "7f82a27a-eaf8-4c16-b15e-66b5efe62fa2", + "last_modified": 1700826062631 + }, + { + "click": { + "optIn": "div._2j0fmugLb1FgYz6KPuB91w > button", + "optOut": "div._2j0fmugLb1FgYz6KPuB91w > button + button", + "presence": "div#cookie-banner, #wcpConsentBannerCtrl" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "microsoft.com", + "office.com" + ], + "id": "f899d35e-20af-4cfb-9856-143a80b86ba9", + "last_modified": 1700826062627 + }, + { + "click": { + "optIn": "#cookie-disclosure-accept", + "optOut": "#cookie-disclosure-reject", + "presence": "#cookie-disclosure" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "netflix.com" + ], + "id": "6037802d-9a37-4df2-bf35-9ad60c478725", + "last_modified": 1700826062623 + }, + { + "click": { + "optIn": "#bbccookies-continue-button", + "presence": "#bbccookies" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "bbc.com", + "bbc.co.uk" + ], + "id": "e9a07bcd-28b5-4034-a663-c017e6a8208e", + "last_modified": 1700826062619 + }, + { + "click": { + "optIn": ".css-1e382ig", + "optOut": ".css-7gbax3", + "presence": "#qc-cmp2-container" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "nemzetisport.hu" + ], + "id": "da1104e6-0ce0-4956-bd53-fd13c5c2c90b", + "last_modified": 1700826062613 + }, + { + "click": { + "optIn": "button.css-1kpvtti", + "presence": "div#qc-cmp2-container" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "voetbalprimeur.nl" + ], + "id": "17c60799-e59b-475f-8007-fec046609121", + "last_modified": 1700826062609 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons-container" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "rtl.hr" + ], + "id": "1d9f6559-cbc8-4cdb-b5f5-ca24844621d3", + "last_modified": 1700826062605 + }, + { + "click": { + "optIn": "a.wscrOk", + "presence": "div.wscrBannerContent" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "nordea.com" + ], + "id": "fe09c786-b60e-455e-abd6-212c8878f06a", + "last_modified": 1700826062602 + }, + { + "click": { + "optIn": ".css-ofc9r3", + "optOut": ".css-1jlb8eq", + "presence": "#qc-cmp2-container" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "car.gr" + ], + "id": "690ce1d3-e595-4181-a109-8f55c1039c81", + "last_modified": 1700826062598 + }, + { + "click": { + "optIn": "button#minf-privacy-accept-btn-screen1-id", + "presence": "div.minf-privacy-rationale" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "mediaset.it" + ], + "id": "a54d7325-5847-4f20-815f-a938dacd81b4", + "last_modified": 1700826062588 + }, + { + "click": { + "optIn": "button#accept-ufti", + "presence": "div.Modal__Content-sc-1sm9281-2" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "blocket.se" + ], + "id": "12b031c4-abfd-4156-a83c-5e8418f4a866", + "last_modified": 1700826062585 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "abola.pt" + ], + "id": "8e8f6719-0350-4310-8cc1-c00bb51b72b0", + "last_modified": 1700826062581 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-dialog-container" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "sme.sk", + "ripost.hu", + "fanatik.ro", + "net.hr", + "freemail.hu", + "marica.bg", + "dn.pt", + "imhd.sk" + ], + "id": "5fd67d61-aaa7-4431-af6f-0f1c31c849fc", + "last_modified": 1700826062572 + }, + { + "click": { + "optIn": ".fc-button-label", + "presence": ".fc-consent-root" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "framar.bg" + ], + "id": "ff5b2d52-6cc0-47e5-a515-4a31c9207b81", + "last_modified": 1700826062569 + }, + { + "click": { + "optIn": "#pt-accept-all", + "optOut": ".pt-k2L", + "presence": ".pt-VnJ" + }, + "schema": 1700784006705, + "cookies": {}, + "domains": [ + "dagospia.com" + ], + "id": "794449a7-94aa-402a-a32c-3859bec7eff8", + "last_modified": 1700826062558 + }, + { + "click": { + "optIn": "button#button_i_accept", + "presence": "div#consent-info" + }, + "schema": 1700313507200, + "cookies": { + "optIn": [ + { + "name": "cookiesconsent", + "value": "eyJwZXJzb25hbGFkIjp0cnVlLCJsYXN0dmlzaXRlZCI6dHJ1ZX0" + } + ] + }, + "domains": [ + "alo.bg" + ], + "id": "9dcffb37-fccc-4c6f-9127-e9d2b0db7521", + "last_modified": 1700826062554 + }, + { + "click": { + "optIn": ".css-2rlcm4", + "presence": "#qc-cmp2-container" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "borsonline.hu" + ], + "id": "a1d99e05-e32c-4cf1-931c-25c8baf538ae", + "last_modified": 1700826062551 + }, + { + "click": { + "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "optOut": "button#CybotCookiebotDialogBodyButtonDecline", + "presence": "div#CybotCookiebotDialog" + }, + "schema": 1700784006705, + "cookies": { + "optOut": [ + { + "name": "CookieConsent", + "value": "{stamp:%277wlvZfVgG/Pfkj1y6Hfoz636NePkdUWVOV+lQLpJefS1O2ny+RqIdg==%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cmethod:%27explicit%27%2Cver:5%2Cutc:1699462925195%2Ciab2:%27CP07BsAP07BsACGABBENDeCgAAAAAH_AAAAAAAAS4gGgAgABkAEQAJ4AbwA_AEWAJ2AYoBMgC8wGCAS4AAAA.YAAAAAAAAAAA%27%2Cgacm:%271~%27%2Cregion:%27de%27}" + } + ] + }, + "domains": [ + "politiken.dk" + ], + "id": "1006f951-cd51-47cc-9527-5036cef4b85a", + "last_modified": 1700826062547 + }, + { + "click": { + "optIn": "#iubenda-cs-accept-btn iubenda-cs-btn-primary", + "presence": "div#iubenda-cs-banner" + }, + "schema": 1700313507200, + "cookies": {}, + "domains": [ + "3bmeteo.com" + ], + "id": "2625ca4e-dd77-4e72-a6d0-c959f3575ad8", + "last_modified": 1700826062543 + }, + { + "schema": 1700130283852, + "domains": [ + "tumblr.com", + "paypal.com", + "amazon.se", + "amazon.fr", + "amazon.nl", + "amazon.es", + "amazon.co.uk", + "amazon.de", + "amazon.it" + ], + "id": "disabled", + "last_modified": 1700313506967 + }, + { + "click": { + "optOut": ".CookiesAlert_cookiesAlert__3qSl1 .Button_button__3Me73", + "presence": ".CookiesAlert_cookiesAlert__3qSl1" + }, + "schema": 1700130283852, + "domains": [ + "traderjoes.com" + ], + "id": "87cbcbb6-b531-4c9c-889a-aaa5678c06c5", + "last_modified": 1700313506964 + }, + { + "click": { + "hide": "section[aria-label=\"GDPR\"]", + "optOut": "#cancel", + "presence": "#gdpr" + }, + "schema": 1700130283852, + "cookies": { + "optOut": [ + { + "name": "cookies_ok", + "value": "false" + } + ] + }, + "domains": [ + "project529.com" + ], + "id": "a029bf7e-3274-4877-967e-e7517457ac18", + "last_modified": 1700313506961 + }, + { + "click": { + "optIn": ".cookielaw-banner .btn-default", + "presence": ".cookielaw-banner" + }, + "schema": 1700130283852, + "cookies": { + "optIn": [ + { + "name": "cookielaw", + "value": "1" + } + ] + }, + "domains": [ + "openwrt.org" + ], + "id": "14ea41fa-68ac-4c12-b906-7d2a259d5fb7", + "last_modified": 1700313506957 + }, + { + "click": { + "optIn": "div[data-name=\"comp-banner\"] button[aria-label=\"Dismiss\"]", + "presence": "div[data-name=\"comp-banner\"]" + }, + "schema": 1700130283852, + "cookies": { + "optIn": [ + { + "name": "cookieNotification", + "value": "true" + } + ] + }, + "domains": [ + "vegetology.com" + ], + "id": "f7633fd9-05a0-4c21-b9e7-acfcd5df5234", + "last_modified": 1700313506954 + }, + { + "click": { + "optIn": "button#didomi-notice-agree-button", + "presence": "div#didomi-host" + }, + "schema": 1700130283852, + "domains": [], + "id": "didomi", + "last_modified": 1700313506945 + }, + { + "schema": 1699660804881, + "cookies": { + "optOut": [ + { + "name": "CookieConsentDeclined", + "value": "1" + } + ] + }, + "domains": [ + "hetzner.com" + ], + "id": "a8222cf2-8f6c-48df-8215-05a15d4ac592", + "last_modified": 1700130283634 + }, + { + "click": { + "optIn": ".banner-actions-container > #onetrust-accept-btn-handler, #onetrust-button-group > #onetrust-accept-btn-handler, .onetrust-banner-options > #onetrust-accept-btn-handler", + "optOut": ".banner-actions-container > #onetrust-reject-all-handler, #onetrust-button-group > #onetrust-reject-all-handler, .onetrust-banner-options > #onetrust-reject-all-handler", + "presence": "#onetrust-consent-sdk" + }, + "schema": 1699660804881, + "domains": [], + "id": "onetrust", + "last_modified": 1700130283629 + }, + { + "click": {}, + "schema": 1699660804881, + "cookies": { + "optOut": [ + { + "name": "consentLevel", + "value": "1" + }, + { + "name": "euconsent-bypass", + "value": "1" + } + ] + }, + "domains": [ + "web.de", + "gmx.net" + ], + "id": "290db348-ced2-4f16-9954-da476d0aef01", + "last_modified": 1700130283620 + }, + { + "click": { + "optIn": "div.duet--cta--cookie-banner button.py-12", + "optOut": "div.duet--cta--cookie-banner button.text-blurple", + "presence": "div.duet--cta--cookie-banner" + }, + "schema": 1699660804881, + "cookies": {}, + "domains": [ + "theverge.com" + ], + "id": "323bae6b-e146-4318-b8ba-1f819c0cfc53", + "last_modified": 1700130283616 + }, + { + "click": { + "optIn": ".fc-cta-consent", + "optOut": ".fc-cta-do-not-consent", + "presence": ".fc-dialog-container" + }, + "schema": 1697557061727, + "domains": [ + "accuweather.com" + ], + "id": "10B17126-C30B-4EE5-9CF5-A4CAFD361AEB", + "last_modified": 1697710931649 + }, + { + "click": { + "optIn": ".js-disc-cp-accept-all", + "optOut": ".js-disc-cp-deny-all", + "presence": ".disc-cp-modal__modal" + }, + "schema": 1697557061727, + "cookies": { + "optOut": [ + { + "name": "obiConsent", + "value": "c1-1|c2-0|c3-0|c4-0|c5-0|ts-4127464398701|consent-true" + }, + { + "name": "miCookieOptOut", + "value": "1" + } + ] + }, + "domains": [ + "obi.de" + ], + "id": "02BB875E-D588-4C12-9BE8-FD467C050B86", + "last_modified": 1697710931646 + }, + { + "click": { + "optIn": "#cmpwelcomebtnyes a", + "presence": "#cmpbox" + }, + "schema": 1697557061727, + "domains": [ + "mail.ru", + "blic.rs", + "hrt.hr", + "eventim.de" + ], + "id": "EA3C8C1C-8B1A-46A4-8631-750ECC29425A", + "last_modified": 1697710931642 + }, + { + "click": { + "optIn": "#cmpwelcomebtnyes a", + "optOut": "#cmpwelcomebtnno a", + "presence": "#cmpbox" + }, + "schema": 1697557061727, + "domains": [ + "adac.de" + ], + "id": "E46708B7-B378-4AA5-A700-12D7221537A0", + "last_modified": 1697710931639 + }, + { + "schema": 1697557061727, + "cookies": { + "optOut": [ + { + "name": "cookie_consent", + "value": "denied" + }, + { + "name": "marketing_consent", + "value": "denied" + }, + { + "name": "personalization_consent", + "value": "denied" + } + ] + }, + "domains": [ + "arbeitsagentur.de" + ], + "id": "A04DD123-A10F-4665-9C2B-2FB5CC293F42", + "last_modified": 1697710931635 + }, + { + "click": { + "optIn": "button[data-test=\"pwa-consent-layer-accept-all\"]", + "optOut": "button[data-test=\"pwa-consent-layer-deny-all\"]", + "presence": "#mms-consent-portal-container" + }, + "schema": 1697557061727, + "cookies": { + "optOut": [ + { + "name": "pwaconsent", + "value": "v:1.0~required:1&clf:1,cli:1,gfb:1,gtm:1,jot:1,ocx:1|comfort:0&baz:0,cne:0,con:0,fix:0,gfa:0,goa:0,gom:0,grc:0,grv:0,inm:0,lib:0,lob:0,opt:0,orc:0,ore:0,pcl:0,sen:0,sis:0,spe:0,sst:0,swo:0,twi:0,usw:0,usz:0,yte:0|marketing:0&cri:0,eam:0,fab:0,fbc:0,fcm:0,gac:0,gad:0,gcl:0,gcm:0,gdv:0,gos:0,gse:0,msb:0,omp:0,pin:0,ttd:0,twt:0|" + } + ] + }, + "domains": [ + "mediamarkt.de", + "saturn.de" + ], + "id": "2A2B8102-D276-4ECE-AFBD-005E8E917D18", + "last_modified": 1697710931632 + }, + { + "click": { + "optIn": "div[data-tracking-opt-in-accept=\"true\"]", + "presence": "div[data-tracking-opt-in-overlay=\"true\"]" + }, + "schema": 1697557061727, + "domains": [ + "fandom.com" + ], + "id": "D168AF87-F481-4AD7-BE78-28A59F798406", + "last_modified": 1697710931628 + }, + { + "click": { + "optIn": "#consentAcceptAll", + "optOut": "#rejectAll", + "presence": "#__tealiumGDPRecModal" + }, + "schema": 1697557061727, + "domains": [ + "telekom.com", + "telekom.de" + ], + "id": "8907164E-17D8-4D27-B0A3-EDDA59F53DBE", + "last_modified": 1697710931625 + }, + { + "click": { + "optIn": "#dip-consent-summary-accept-all", + "optOut": "#dip-consent-summary-reject-all", + "presence": "#dip-consent" + }, + "schema": 1697557061727, + "cookies": {}, + "domains": [ + "vodafone.de" + ], + "id": "B85F7D67-D6D4-40EE-8472-A32B6CD01E0E", + "last_modified": 1697710931621 + }, + { + "schema": 1697557061727, + "cookies": { + "optOut": [ + { + "name": "CONSENTMGR", + "value": "c1:1%7Cc2:0%7Cc3:0%7Cc4:0%7Cc5:0%7Cc6:0%7Cc7:0%7Cc8:0%7Cc9:0%7Cc10:0%7Cc11:0%7Cc12:0%7Cc13:0%7Cc14:0%7Cc15:0%7Cts:111697211775123%7Cconsent:true" + }, + { + "name": "request_consent_v", + "value": "3" + } + ] + }, + "domains": [ + "bahn.de" + ], + "id": "D4AD5843-DDBD-4D93-BFBA-F53DD8B53111", + "last_modified": 1697710931618 + }, + { + "schema": 1697557061727, + "cookies": { + "optIn": [ + { + "name": "consent_status", + "value": "true" + } + ], + "optOut": [ + { + "name": "consent_status", + "value": "false" + } + ] + }, + "domains": [ + "immobilienscout24.de" + ], + "id": "3BD1F0DA-BABB-4F75-9B56-C92A1CFD114B", + "last_modified": 1697710931613 + }, + { + "click": { + "optIn": ".cmpboxbtnyes", + "optOut": ".cmpboxbtnsave", + "presence": "#cmpwrapper" + }, + "schema": 1697557061727, + "domains": [ + "gls-pakete.de" + ], + "id": "1D4DDA28-C6FC-489F-B2F3-07EAAB4F5AFE", + "last_modified": 1697710931609 + }, + { + "click": { + "optIn": "button[data-test-id=\"cookie-notice-accept\"]", + "optOut": "button[data-test-id=\"cookie-notice-reject\"]", + "presence": ".cookie-notice" + }, + "schema": 1697557061727, + "cookies": { + "optOut": [ + { + "name": "ECCC", + "value": "e" + } + ] + }, + "domains": [ + "ecosia.org" + ], + "id": "7BA8E4AF-438A-40FE-8941-F921AA1FEA2A", + "last_modified": 1697710931606 + }, + { + "click": { + "optIn": "button[data-testid=\"cookie-banner-strict-accept-all\"]", + "optOut": "button[data-testid=\"cookie-banner-strict-accept-selected\"]", + "presence": "div[data-testid=\"dl-cookieBanner\"]" + }, + "schema": 1697557061727, + "cookies": { + "optOut": [ + { + "name": "privacySettings", + "value": "%7B%22v%22%3A%221%22%2C%22t%22%3A4127362064%2C%22m%22%3A%22STRICT%22%2C%22consent%22%3A%5B%22NECESSARY%22%5D%7D" + } + ] + }, + "domains": [ + "deepl.com" + ], + "id": "e3968548-ebca-4d6b-b8cf-5707e90fb612", + "last_modified": 1697710931573 + }, + { + "click": { + "optIn": "button#uc-btn-accept-banner", + "optOut": "button#uc-btn-deny-banner", + "presence": "div#uc-banner-modal" + }, + "schema": 1697557061727, + "cookies": {}, + "domains": [ + "zalando.ch", + "zalando.dk", + "zalando.be", + "zalando.de", + "zalando.at", + "zalando.co.uk", + "zalando.com", + "zalando.cz", + "zalando.ee", + "zalando.es", + "zalando.fi", + "zalando.fr", + "zalando.hr" + ], + "id": "3203ac4e-2454-4022-90fb-d4f51467ce20", + "last_modified": 1697710931567 + }, + { + "schema": 1696497904676, + "cookies": { + "optOut": [ + { + "name": "c24consent", + "value": "f" + } + ] + }, + "domains": [ + "check24.de" + ], + "id": "d4f2492e-c801-4687-84c8-e2cd17b4f115", + "last_modified": 1696579709256 + }, + { + "click": { + "optIn": ".qc-cmp2-summary-buttons > :nth-child(3)", + "optOut": ".qc-cmp2-summary-buttons > :nth-child(2)", + "presence": "#qc-cmp2-container" + }, + "schema": 1696497904676, + "cookies": {}, + "domains": [ + "ign.com" + ], + "id": "FB43FF71-9546-43D1-8B52-D208D1347095", + "last_modified": 1696579709252 + }, + { + "click": { + "optIn": "#gdpr-banner-accept", + "presence": "#gdpr-banner-container" + }, + "schema": 1696497904676, + "cookies": { + "optIn": [], + "optOut": [ + { + "name": "ekConsentTcf2", + "value": "{%22customVersion%22:6%2C%22encodedConsentString%22:%22CPzMR3KPzMR3KE1ABADEC4CgAAAAAAAAAAYgJNwLgAXAA4ACWAFMAPwAzYCLAIuAZ8A14B0gD7AI8ASKAlcBMgCmwFhALqAXeAvoBggDBgGfANGAaaA1UBtADggHHgOUAc6A58B2wDuQHggPJAfaA_YCCIEFAI0gR2Aj6BIiCSIJJgSbAAAASMgAwABBKEdABgACCUJCADAAEEoSUAGAAIJQlIAMAAQShCQAYAAglCGgAwABBKERABgACCUIqADAAEEoQA%22%2C%22googleConsentGiven%22:false%2C%22consentInterpretation%22:{%22googleAdvertisingFeaturesAllowed%22:false%2C%22googleAnalyticsAllowed%22:false%2C%22infonlineAllowed%22:false%2C%22theAdexAllowed%22:false%2C%22criteoAllowed%22:false%2C%22facebookAllowed%22:false%2C%22amazonAdvertisingAllowed%22:false%2C%22rtbHouseAllowed%22:false}}" + } + ] + }, + "domains": [ + "kleinanzeigen.de" + ], + "id": "dbc0dde3-600b-40ab-ab8b-b07e3e3c7af8", + "last_modified": 1696579709220 + }, + { + "click": { + "optIn": ".implicit_privacy_prompt > .close_btn_thick", + "presence": ".implicit_privacy_prompt" + }, + "schema": 1694908806092, + "cookies": { + "optOut": [ + { + "name": "HASSEENNOTICE", + "value": "TRUE" + }, + { + "name": "CONSENTMGR", + "value": "c1:0%7Cc3:0%7Cc5:0%7Cc6:0%7Cc7:0%7Cc8:0" + } + ] + }, + "domains": [ + "ups.com" + ], + "id": "f462b0ee-4d17-4dcb-a951-1a23249227f6", + "last_modified": 1695037595332 + }, + { + "click": {}, + "schema": 1694908806092, + "cookies": { + "optOut": [ + { + "name": "CONSENT", + "path": "/", + "value": "PENDING+742", + "sameSite": 0 + }, + { + "name": "SOCS", + "path": "/", + "value": "CAESNQgDEitib3FfaWRlbnRpdHlmcm9udGVuZHVpc2VydmVyXzIwMjMwOTEyLjA4X3AwGgJlbiACGgYIgOCTqAY", + "sameSite": 0 + } + ] + }, + "domains": [ + "youtube.com" + ], + "id": "788ed05e-cf93-41a8-b2e5-c5571d855232", + "last_modified": 1695037595328 + }, + { + "click": { + "optIn": "#footer_tc_privacy_button", + "optOut": "#footer_tc_privacy_button_2", + "presence": ".tc-privacy-wrapper.tc-privacy-override.tc-privacy-wrapper" + }, + "schema": 1690070404721, + "domains": [ + "arte.tv" + ], + "id": "cc818b41-7b46-46d3-9b17-cf924cbe87d1", + "last_modified": 1690359097318 + }, + { + "schema": 1690070404721, + "cookies": { + "optIn": [ + { + "name": "cb", + "value": "1_2055_07_11_" + } + ], + "optOut": [ + { + "name": "cb", + "value": "1_2055_07_11_2-3" + } + ] + }, + "domains": [ + "threads.net" + ], + "id": "C232EAB8-F55A-436A-8033-478746D05D98", + "last_modified": 1690359097314 + }, + { + "click": { + "optIn": "#didomi-notice-agree-button", + "presence": "#didomi-popup" + }, + "schema": 1690070404721, + "cookies": { + "optOut": [ + { + "name": "euconsent-v2", + "value": "CRolnUARolnUAAHABBENDHCgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA" + } + ] + }, + "domains": [ + "reverso.net" + ], + "id": "05157ed1-12c2-4f84-9dff-718fae5bc096", + "last_modified": 1690359097310 + }, + { + "click": { + "optIn": "#acceptAllButton", + "optOut": "#rejectAllButton", + "presence": "#cookiePrefPopup" + }, + "schema": 1686614405615, + "cookies": { + "optIn": [ + { + "name": "cookieSettings", + "value": "%7B%22version%22%3A1%2C%22preference_state%22%3A1%2C%22content_customization%22%3Anull%2C%22valve_analytics%22%3Anull%2C%22third_party_analytics%22%3Anull%2C%22third_party_content%22%3Anull%2C%22utm_enabled%22%3Atrue%7D" + } + ], + "optOut": [ + { + "name": "cookieSettings", + "value": "%7B%22version%22%3A1%2C%22preference_state%22%3A2%2C%22content_customization%22%3Anull%2C%22valve_analytics%22%3Anull%2C%22third_party_analytics%22%3Anull%2C%22third_party_content%22%3Anull%2C%22utm_enabled%22%3Atrue%7D" + } + ] + }, + "domains": [ + "steamcommunity.com", + "steampowered.com" + ], + "id": "63104024-41b5-4be5-8019-2c59eaaf612c", + "last_modified": 1686654909145 + }, + { + "click": { + "optIn": "button.accept-all", + "optOut": "button.reject-all", + "presence": "div#consent-page" + }, + "schema": 1678898205499, + "cookies": {}, + "domains": [ + "yahoo.com" + ], + "id": "2f9123b1-7d6b-460f-8da8-988f82238ec7", + "last_modified": 1679487994744 + }, + { + "click": { + "optIn": "#truste-consent-button", + "optOut": "#truste-consent-required", + "presence": "#truste-consent-content, .truste-consent-content, #truste-consent-track" + }, + "schema": 1678092903414, + "domains": [], + "id": "trustarcbar", + "last_modified": 1678276263380 + }, + { + "click": { + "optIn": ".qc-cmp2-buttons-desktop button[mode=\"primary\"], .qc-cmp2-summary-buttons button[mode=\"primary\"]", + "presence": ".qc-cmp2-container" + }, + "schema": 1678092903414, + "domains": [], + "id": "quantcast", + "last_modified": 1678276263374 + }, + { + "click": { + "optIn": "#CookieBoxSaveButton", + "optOut": "._brlbs-refuse-btn a", + "presence": "#BorlabsCookieBox" + }, + "schema": 1678092903414, + "domains": [], + "id": "borlabs", + "last_modified": 1678276263369 + }, + { + "click": { + "optIn": "#cmpwelcomebtnyes a", + "optOut": "#cmpwelcomebtnno a", + "presence": "#cmpbox" + }, + "schema": 1678092903414, + "domains": [], + "id": "consentmanagernet", + "last_modified": 1678276263363 + }, + { + "click": { + "optIn": "#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "optOut": "#CybotCookiebotDialogBodyButtonDecline", + "presence": "#CybotCookiebotDialog" + }, + "schema": 1678092903414, + "domains": [], + "id": "cookiebot", + "last_modified": 1678276263357 + }, + { + "click": { + "optIn": ".cmplz-btn.cmplz-accept", + "optOut": ".cmplz-btn.cmplz-deny", + "presence": "#cmplz-cookiebanner-container" + }, + "schema": 1678092903414, + "domains": [], + "id": "complianz", + "last_modified": 1678276263351 + }, + { + "click": { + "optIn": ".sp_choice_type_11", + "presence": ".message-container > #notice", + "runContext": "child", + "skipPresenceVisibilityCheck": true + }, + "schema": 1678092903414, + "domains": [], + "id": "sourcepoint", + "last_modified": 1678276263334 + }, + { + "click": { + "optIn": "button#hs-eu-confirmation-button", + "optOut": "button#hs-eu-decline-button", + "presence": "div#hs-eu-cookie-confirmation" + }, + "schema": 1678092903414, + "cookies": {}, + "domains": [ + "hubspot.com" + ], + "id": "8308357f-6a66-433d-bfc3-de401410c350", + "last_modified": 1678276263322 + }, + { + "click": {}, + "schema": 1678092903414, + "cookies": { + "optIn": [ + { + "name": "cookie-consent", + "value": "{%22ga%22:true%2C%22af%22:true%2C%22fbp%22:true%2C%22lip%22:true%2C%22bing%22:true%2C%22ttads%22:true%2C%22reddit%22:true%2C%22criteo%22:true%2C%22version%22:%22v9%22}" + } + ], + "optOut": [ + { + "name": "cookie-consent", + "value": "{%22ga%22:false%2C%22af%22:false%2C%22fbp%22:false%2C%22lip%22:false%2C%22bing%22:false%2C%22ttads%22:false%2C%22reddit%22:false%2C%22criteo%22:false%2C%22version%22:%22v9%22}" + } + ] + }, + "domains": [ + "tiktok.com" + ], + "id": "d9166ae8-dcc7-4ca2-8b02-0884fb1d6f70", + "last_modified": 1678276263311 + }, + { + "click": {}, + "schema": 1678236486316, + "cookies": { + "optOut": [ + { + "name": "_s.cookie_consent", + "value": "marketing=0:analytics=0:version=2021-07-01:timestamp=1678179320141" + } + ] + }, + "domains": [ + "smallpdf.com" + ], + "id": "bb1114eb-12ef-4465-8990-b33bbb270d3f", + "last_modified": 1678276263300 + }, + { + "click": { + "optIn": "button.cc-banner__button-accept", + "optOut": "button.cc-banner__button-reject", + "presence": "div.cc-banner__content" + }, + "schema": 1678236486316, + "cookies": {}, + "domains": [ + "biomedcentral.com" + ], + "id": "d5220250-697b-4b60-8284-f87e8ec2565c", + "last_modified": 1678276263295 + }, + { + "click": { + "optIn": "a.o-cookie-message__button", + "presence": "div.o-cookie-message" + }, + "schema": 1678236486316, + "cookies": {}, + "domains": [ + "ft.com" + ], + "id": "b485c89f-130e-4d5a-94f8-99eacad96e5f", + "last_modified": 1678276263290 + }, + { + "click": { + "optOut": "button.TCF2Popup__continueWithoutAccepting___1fMWW", + "presence": "div.TCF2Popup__container___1TN_W" + }, + "schema": 1678092903414, + "cookies": {}, + "domains": [ + "dailymotion.com" + ], + "id": "e5d31bce-4a62-4c6d-a40f-94fda7c41c9d", + "last_modified": 1678276263284 + }, + { + "click": { + "optIn": "button#L2AGLb", + "optOut": "button#W0wltc", + "presence": "div.spoKVd" + }, + "schema": 1678092903414, + "cookies": { + "optIn": [ + { + "name": "CONSENT", + "value": "PENDING" + } + ], + "optOut": [ + { + "name": "SOCS", + "value": "CAESHAgBEhJnd3NfMjAyMjA5MjktMF9SQzEaAnJvIAEaBgiAkvOZBg" + } + ] + }, + "domains": [ + "google.ad", + "google.ae", + "google.al", + "google.am", + "google.as", + "google.at", + "google.az", + "google.ba", + "google.be", + "google.bf", + "google.bg", + "google.bi", + "google.bj", + "google.bs", + "google.bt", + "google.by", + "google.ca", + "google.cat", + "google.cd", + "google.cf", + "google.cg", + "google.ch", + "google.ci", + "google.cl", + "google.cm", + "google.cn", + "google.co.ao", + "google.co.bw", + "google.co.ck", + "google.co.cr", + "google.co.id", + "google.co.il", + "google.co.in", + "google.co.jp", + "google.co.ke", + "google.co.kr", + "google.co.ls", + "google.co.ma", + "google.co.mz", + "google.co.nz", + "google.co.th", + "google.co.tz", + "google.co.ug", + "google.co.uk", + "google.co.uz", + "google.co.ve", + "google.co.vi", + "google.co.za", + "google.co.zm", + "google.co.zw", + "google.com", + "google.com.af", + "google.com.ag", + "google.com.ai", + "google.com.ar", + "google.com.au", + "google.com.bd", + "google.com.bh", + "google.com.bn", + "google.com.bo", + "google.com.br", + "google.com.bz", + "google.com.co", + "google.com.cu", + "google.com.cy", + "google.com.do", + "google.com.ec", + "google.com.eg", + "google.com.et", + "google.com.fj", + "google.com.gh", + "google.com.gi", + "google.com.gt", + "google.com.hk", + "google.com.jm", + "google.com.kh", + "google.com.kw", + "google.com.lb", + "google.com.ly", + "google.com.mm", + "google.com.mt", + "google.com.mx", + "google.com.my", + "google.com.na", + "google.com.ng", + "google.com.ni", + "google.com.np", + "google.com.om", + "google.com.pa", + "google.com.pe", + "google.com.pg", + "google.com.ph", + "google.com.pk", + "google.com.pr", + "google.com.py", + "google.com.qa", + "google.com.sa", + "google.com.sb", + "google.com.sg", + "google.com.sl", + "google.com.sv", + "google.com.tj", + "google.com.tr", + "google.com.tw", + "google.com.ua", + "google.com.uy", + "google.com.vc", + "google.com.vn", + "google.cv", + "google.cz", + "google.de", + "google.dj", + "google.dk", + "google.dm", + "google.dz", + "google.ee", + "google.es", + "google.fi", + "google.fm", + "google.fr", + "google.ga", + "google.ge", + "google.gg", + "google.gl", + "google.gm", + "google.gr", + "google.gy", + "google.hn", + "google.hr", + "google.ht", + "google.hu", + "google.ie", + "google.im", + "google.iq", + "google.is", + "google.it", + "google.je", + "google.jo", + "google.kg", + "google.ki", + "google.kz", + "google.la", + "google.li", + "google.lk", + "google.lt", + "google.lu", + "google.lv", + "google.md", + "google.me", + "google.mg", + "google.mk", + "google.ml", + "google.mn", + "google.ms", + "google.mu", + "google.mv", + "google.mw", + "google.ne", + "google.nl", + "google.no", + "google.nr", + "google.nu", + "google.pl", + "google.pn", + "google.ps", + "google.pt", + "google.ro", + "google.rs", + "google.ru", + "google.rw", + "google.sc", + "google.se", + "google.sh", + "google.si", + "google.sk", + "google.sm", + "google.sn", + "google.so", + "google.sr", + "google.st", + "google.td", + "google.tg", + "google.tl", + "google.tm", + "google.tn", + "google.to", + "google.tt", + "google.vg", + "google.vu", + "google.ws" + ], + "id": "4a37ab17-dbd7-45cf-83d8-42b84af9a0df", + "last_modified": 1678276263277 + }, + { + "click": { + "optIn": "button#bnp_btn_accept", + "optOut": "button#bnp_btn_reject", + "presence": "div#bnp_cookie_banner" + }, + "schema": 1678092903414, + "cookies": { + "optOut": [ + { + "name": "BCP", + "value": "AD=0&AL=0&SM=0" + } + ] + }, + "domains": [ + "bing.com" + ], + "id": "31dc6160-3495-4f4e-8c67-594527bd4051", + "last_modified": 1678276263272 + }, + { + "click": { + "optIn": "button.css-1d0m171", + "presence": "div#qc-cmp2-container" + }, + "schema": 1678236486316, + "cookies": {}, + "domains": [ + "in.gr" + ], + "id": "77896946-2dbf-4b65-9641-9958f9e2a228", + "last_modified": 1678276263267 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-consent-root" + }, + "schema": 1678092903414, + "cookies": {}, + "domains": [ + "dnes.bg", + "dennikn.sk", + "jn.pt", + "ojogo.pt", + "klik.hr" + ], + "id": "ed097835-3d75-4864-8cdf-ea8fb19b033c", + "last_modified": 1678276263256 + }, + { + "click": { + "optIn": "button.css-7c2c0h", + "presence": "div#qc-cmp2-container" + }, + "schema": 1678236486316, + "cookies": {}, + "domains": [ + "deviantart.com" + ], + "id": "489a59fb-1054-4d7a-ae1f-c6c561d2cd81", + "last_modified": 1678276263214 + }, + { + "click": { + "optIn": "div#gdpr_accept", + "presence": "div#gdpr" + }, + "schema": 1678236486316, + "cookies": {}, + "domains": [ + "wikihow.com" + ], + "id": "90b68b2d-46eb-4500-8cfd-9ee794653aaa", + "last_modified": 1678276263208 + }, + { + "click": { + "optIn": "button.css-1lgi3ta", + "presence": "div#qc-cmp2-container" + }, + "schema": 1678092903414, + "cookies": {}, + "domains": [ + "ethnos.gr", + "sportdog.gr" + ], + "id": "570920f9-6a8a-4a26-a68a-8fc773ba30a9", + "last_modified": 1678276263197 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "optOut": "button.fc-cta-do-not-consent", + "presence": "div.fc-consent-root" + }, + "schema": 1678092903414, + "cookies": {}, + "domains": [ + "coolinarika.com", + "ndtv.com", + "hbr.org" + ], + "id": "12552893-278a-43e6-83ba-1db5267b3d27", + "last_modified": 1678276263190 + }, + { + "click": { + "optIn": "button.snow-ali-kit_Button-Floating__button__ph4zrl", + "presence": "div.SnowPrivacyPolicyBanner_SnowPrivacyPolicyBanner__privacyPolicyBanner__1jg07" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "aliexpress.ru" + ], + "id": "407fe21e-7730-47f9-b83f-51d19dddc700", + "last_modified": 1676903765550 + }, + { + "click": { + "optIn": "a.dismiss", + "presence": "div#cookie-disclaimer" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "arukereso.hu" + ], + "id": "7cbce78d-ec34-4e9b-a4f5-5b401f155e36", + "last_modified": 1676903765539 + }, + { + "click": { + "optIn": "div.cookieinfo-close", + "presence": "div.cookieinfo" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "glasistre.hr" + ], + "id": "266c0df8-d8bc-4824-88d8-06d3970f0c2a", + "last_modified": 1676903765535 + }, + { + "click": { + "optIn": "button.css-wum6af", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "flash.pt" + ], + "id": "f081b651-7065-46a9-8d71-d7531850382e", + "last_modified": 1676903765531 + }, + { + "click": { + "optIn": "label.c-notifier-btn", + "presence": "div.c-notifier-container" + }, + "schema": 1676828360243, + "cookies": { + "optOut": [ + { + "name": "gdpr", + "value": "[]" + } + ] + }, + "domains": [ + "ria.com" + ], + "id": "3d2936df-3ca5-42fc-924b-1144c3d5b424", + "last_modified": 1676903765523 + }, + { + "click": { + "optIn": "button#acceptAllCookiesBtn", + "presence": "div#cookie-popover" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "sverigesradio.se" + ], + "id": "aa18156e-646e-4c5c-b550-6979d181f192", + "last_modified": 1676903765516 + }, + { + "click": { + "optIn": "a.btn", + "presence": "div#gdpr-box" + }, + "schema": 1676828360243, + "cookies": { + "optIn": [ + { + "name": "privacy_accepted", + "value": "1" + } + ] + }, + "domains": [ + "naslovi.net" + ], + "id": "87d2bcfa-8685-4289-8c0a-d0fc31c74cc5", + "last_modified": 1676903765512 + }, + { + "click": { + "optIn": "button.orange-gradient", + "presence": "div.cookie-bar-wrap" + }, + "schema": 1676828360243, + "cookies": { + "optIn": [ + { + "name": "politica_cookie", + "value": "1" + } + ] + }, + "domains": [ + "dedeman.ro" + ], + "id": "84a2ec9b-bf4a-4638-aa94-d1b13652aae8", + "last_modified": 1676903765508 + }, + { + "click": { + "optIn": "button.jad_cmp_paywall_button-cookies", + "presence": "div#cmp-main" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "allocine.fr" + ], + "id": "7d70347b-fe27-4c60-a9f2-6e0638f0ce37", + "last_modified": 1676903765505 + }, + { + "click": { + "optIn": "button.primary", + "presence": "div.cookie-consent-overlay" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "komplett.no" + ], + "id": "be728b6d-8eeb-4179-beac-c1234fc0ae9f", + "last_modified": 1676903765501 + }, + { + "click": { + "optIn": "div.cookieButton", + "presence": "div#cookieConsentContainer" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "chmi.cz" + ], + "id": "d8923166-cf05-4f6c-8829-270fc9b2efe1", + "last_modified": 1676903765497 + }, + { + "click": { + "optIn": "button.PONCHO-button--decide", + "presence": "div#TRCO-application" + }, + "schema": 1676828360243, + "cookies": { + "optOut": [ + { + "name": "cookie-policy-agreement", + "value": "%7B%22revision%22%3A20%2C%22consentLevel%22%3A1%7D" + } + ] + }, + "domains": [ + "anwb.nl" + ], + "id": "e63a688a-7eca-49b1-9b0f-bebae773a6ec", + "last_modified": 1676903765493 + }, + { + "click": { + "optIn": "button.shared-module_w-full_3cBSk", + "presence": "div#consent-modal-yr73k" + }, + "schema": 1676828360243, + "cookies": { + "optOut": [ + { + "name": "euconsent-v2", + "value": "CPmmxEAPmmxEADhADBPLC1CgAAAAAAAAAB5YAAAAAAAA" + } + ] + }, + "domains": [ + "se.pl" + ], + "id": "d0b8bc2c-c633-4628-aa39-e840060efc2e", + "last_modified": 1676903765490 + }, + { + "click": {}, + "schema": 1676828360243, + "cookies": { + "optOut": [ + { + "name": "OPTOUTCONSENT", + "value": "1:1&2:0&3:0&4:0" + } + ] + }, + "domains": [ + "vodafone.pt" + ], + "id": "522a2d72-8131-406e-b058-b27ec07808fc", + "last_modified": 1676903765486 + }, + { + "click": { + "optIn": "div#cookiescript_accept", + "optOut": "div#cookiescript_reject ", + "presence": "div#cookiescript_injected" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "idealmedia.io" + ], + "id": "7f8a8bfd-343c-4fa5-969b-35a0690f6f4f", + "last_modified": 1676903765482 + }, + { + "click": { + "optIn": "a.cookieBarConsentButton", + "presence": "div#cookieBar" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "pki.goog" + ], + "id": "259f5d9f-b127-47c2-a42e-161cb70a0a81", + "last_modified": 1676903765475 + }, + { + "click": { + "optIn": "button.css-11uejzu", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676828360243, + "cookies": {}, + "domains": [ + "huffpost.com" + ], + "id": "e9d772f8-92fd-4e0e-b742-0bfd6555877e", + "last_modified": 1676903765471 + }, + { + "click": {}, + "schema": 1676900247175, + "cookies": { + "optIn": [ + { + "name": "cookiesAccepted", + "value": "essential" + } + ] + }, + "domains": [ + "onlyfans.com" + ], + "id": "647013d0-7a3f-4e80-8c26-00f3389ba551", + "last_modified": 1676903765463 + }, + { + "click": { + "optIn": "button#cookie-accept", + "presence": "div.banner" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "tandfonline.com" + ], + "id": "73c5220c-3cdb-446b-b2f6-ce9e0f1d9a8b", + "last_modified": 1676903765459 + }, + { + "click": { + "optIn": "button.banner-lgpd-consent__accept", + "presence": "div.banner-lgpd-consent-container" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "uol.com.br" + ], + "id": "a491dd76-8759-4c10-a4fa-2e87b1034f23", + "last_modified": 1676903765456 + }, + { + "click": {}, + "schema": 1676900247175, + "cookies": { + "optIn": [ + { + "name": "uw_marketo_opt_in", + "value": "true" + } + ], + "optOut": [ + { + "name": "mkto_opt_out", + "value": "id:true" + } + ] + }, + "domains": [ + "washington.edu" + ], + "id": "1de8aa61-94f1-49b4-a05d-d19a57829c71", + "last_modified": 1676903765452 + }, + { + "click": { + "optIn": "button.css-2wu0d3", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "buzzfeed.com" + ], + "id": "a4186ce7-bb7a-4c55-aec6-34f265943367", + "last_modified": 1676903765448 + }, + { + "click": { + "optIn": "button.cky-btn-accept", + "optOut": "button.cky-btn-reject", + "presence": "div.cky-consent-bar" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "hugedomains.com" + ], + "id": "b3b80b81-90f0-497f-b78c-53b611d4dfaf", + "last_modified": 1676903765444 + }, + { + "click": {}, + "schema": 1676900247175, + "cookies": { + "optIn": [ + { + "name": "trackingconsent", + "value": "{\"marketingenabled\":true}" + } + ], + "optOut": [ + { + "name": "trackingconsent", + "value": "{\"marketingenabled\":false}" + } + ] + }, + "domains": [ + "abc.net.au" + ], + "id": "c213b36a-4893-46e2-bb0e-f98a03d58287", + "last_modified": 1676903765437 + }, + { + "click": { + "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "presence": "div#CookieBanner" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "jotform.com" + ], + "id": "6efa8f4a-97a3-4677-8cde-8727f270d267", + "last_modified": 1676903765433 + }, + { + "click": { + "optIn": "button.bcpConsentButton", + "presence": "div.bcpNotificationBar" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "inc.com" + ], + "id": "990f03a5-c180-407b-9a0e-f6db1278330d", + "last_modified": 1676903765429 + }, + { + "click": {}, + "schema": 1676900247175, + "cookies": { + "optIn": [ + { + "name": "cookiePolicy", + "value": "accept" + } + ] + }, + "domains": [ + "pnas.org" + ], + "id": "a327ad8a-daa9-4d7c-abb5-0f3e3f3c59db", + "last_modified": 1676903765426 + }, + { + "click": {}, + "schema": 1676900247175, + "cookies": { + "optOut": [ + { + "name": "consent", + "value": "{\"Marketing\":false,\"created_time\":\"2023-02-14T12:27:52.910Z\"}" + } + ] + }, + "domains": [ + "science.org" + ], + "id": "67702151-14e2-41c7-af4b-1f22de8185a5", + "last_modified": 1676903765422 + }, + { + "click": {}, + "schema": 1676900247175, + "cookies": { + "optIn": [ + { + "name": "ks-cookie-consent", + "value": "allow" + } + ], + "optOut": [ + { + "name": "ks-cookie-consent", + "value": "deny" + } + ] + }, + "domains": [ + "roku.com" + ], + "id": "08513283-d50d-4a85-93fc-cd0568b09976", + "last_modified": 1676903765410 + }, + { + "click": { + "optIn": "button.gdpr-banner__accept", + "presence": "div.gdpr-banner" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "scmp.com" + ], + "id": "68aff357-d47c-4dc2-b7fa-27ac4b982257", + "last_modified": 1676903765406 + }, + { + "click": { + "optIn": "a.cc-dismiss", + "presence": "div.cc-window" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "braze.com" + ], + "id": "28bee54e-47d3-4192-8044-0bef6870a7c1", + "last_modified": 1676903765403 + }, + { + "click": { + "optIn": "a#cookie-allow-all", + "optOut": "a#cookie-necessary-only", + "presence": "div#cookiebanner" + }, + "schema": 1676900247175, + "cookies": {}, + "domains": [ + "adjust.com" + ], + "id": "ca37bf31-bf4e-4172-bf7e-91baca32c9e2", + "last_modified": 1676903765399 + }, + { + "click": { + "optIn": "button.css-hxv78t", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "rsvplive.ie" + ], + "id": "5a256bf7-29a7-485e-8305-fcafd7a52af9", + "last_modified": 1676903765394 + }, + { + "click": { + "optIn": "button.css-1euwp1t", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "enikos.gr" + ], + "id": "20e1f1c3-0914-42b4-9022-3ce80b79a480", + "last_modified": 1676903765387 + }, + { + "click": { + "optIn": "button.css-fz9f1h", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "dikaiologitika.gr" + ], + "id": "1c742d88-b3a7-43da-b205-3ba4afd92e63", + "last_modified": 1676903765376 + }, + { + "click": { + "optIn": "button.css-v43ltw", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "filmaffinity.com" + ], + "id": "ef8548ec-5dd9-4201-a075-a32893f09953", + "last_modified": 1676903765369 + }, + { + "click": { + "optIn": "button#consent_prompt_submit", + "presence": "div.consent_prompt_footer" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "argos.co.uk" + ], + "id": "88e79126-8072-4079-aa60-1d71d9ddb65b", + "last_modified": 1676903765366 + }, + { + "click": { + "optIn": "button.iubenda-cs-accept-btn", + "presence": "div#iubenda-cs-banner" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "ilmessaggero.it" + ], + "id": "c839d2c3-ee29-4015-a990-8cfe9884a4c4", + "last_modified": 1676903765358 + }, + { + "click": { + "optIn": "button#btn-agree-all", + "presence": "div#rgpd" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "rtp.pt" + ], + "id": "46e07f9b-9ce4-4fea-8be7-48a089bdb8d0", + "last_modified": 1676903765354 + }, + { + "click": { + "optIn": "span.inline-block", + "presence": "div#notice-cookie-block" + }, + "schema": 1676900249813, + "cookies": { + "optIn": [ + { + "name": "user_allowed_save_cookie", + "value": "true" + } + ] + }, + "domains": [ + "altex.ro" + ], + "id": "32139cbb-7e13-4cec-ba54-7bfb4f46277c", + "last_modified": 1676903765351 + }, + { + "click": { + "optIn": "button.css-47sehv", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "timeanddate.com", + "livescience.com", + "cancan.ro", + "evz.ro", + "gandul.ro", + "kurir.rs", + "capital.ro" + ], + "id": "5a3bf87a-62c2-40b2-bfbb-b2dcf8c23f84", + "last_modified": 1676903765347 + }, + { + "click": { + "optIn": "button#kc-acceptAndHide", + "presence": "div#kconsent" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "k-ruoka.fi" + ], + "id": "282ff551-ce28-4b7f-9633-eaaa7ce89890", + "last_modified": 1676903765343 + }, + { + "click": { + "optIn": "button.css-tzlaik", + "presence": "div#qc-cmp2-ui" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "nit.pt" + ], + "id": "30542b9b-2225-4c22-bbd9-0e9d8f6273df", + "last_modified": 1676903765339 + }, + { + "click": { + "optIn": "a#cookies-agree", + "presence": "div.cookies-buttons" + }, + "schema": 1676900249813, + "cookies": { + "optOut": [ + { + "name": "cookiesPolicy", + "value": "10000" + } + ] + }, + "domains": [ + "elcorteingles.es" + ], + "id": "90b22cfd-00a8-4467-9334-96773066c2c1", + "last_modified": 1676903765336 + }, + { + "click": { + "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "presence": "div#CybotCookiebotDialog" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "ingatlan.com" + ], + "id": "009f2741-56cb-485e-af5c-3102f8e9cf55", + "last_modified": 1676903765328 + }, + { + "click": { + "optIn": "button.jad_cmp_paywall_button-cookies", + "presence": "div#didomi-host" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "purepeople.com" + ], + "id": "b933bc7f-27b7-44a7-9127-66f98c93ea8f", + "last_modified": 1676903765322 + }, + { + "click": { + "optIn": "button.css-1xqnplm", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "lifo.gr" + ], + "id": "4f86f2e9-ea7f-4ee7-a4d2-b2253a5f1c1d", + "last_modified": 1676903765318 + }, + { + "click": { + "optIn": "a.close-accept", + "presence": "div.woahbar" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "meteomedia.com" + ], + "id": "8e772dc2-be57-4cf6-ad51-574b2accf86c", + "last_modified": 1676903765311 + }, + { + "click": { + "optOut": "button.accept-button", + "presence": "div.gdpr-content" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "mozzartbet.com" + ], + "id": "b8b2cf50-d9cf-432b-947c-894350121024", + "last_modified": 1676903765308 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "optOut": "button.fc-cta-do-not-consent", + "presence": "div.fc-footer-buttons-container" + }, + "schema": 1676900247175, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPgbQkAPgbQkAEsABBITCjCgAAAAAH_AABCYAAAO9QD2F2K2kKEkfjSUeYAQBCujIEIBUAAAAEKBIAAAAUgQAgFIIAgABlACEAAAABAQAQCAgAQABAAAoICgACAAAAAAAAAQAAQQAABAAIAAAAAAAAEAQAAAAAQAAAAAAAhEhCAAQQAEIAAAAAAAAAAAAAAAAAABAAAAEAA%22%2C%221~%22%2C%22A83BB90B-53A8-400D-8DAA-F4EF44E9B970%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "ilmeteo.it" + ], + "id": "71d0715b-3e8e-4077-969e-7b7722f78910", + "last_modified": 1676903765304 + }, + { + "click": { + "optIn": "button.css-78i25x", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "mirror.co.uk" + ], + "id": "21593dac-f469-4206-87d9-044e6d69404b", + "last_modified": 1676903765300 + }, + { + "click": { + "optIn": "a.js-cookies-info-accept", + "optOut": "a.js-cookies-info-reject", + "presence": "div.cookies-info__buttons" + }, + "schema": 1676900248050, + "cookies": { + "optOut": [ + { + "name": "CBARIH", + "value": "1" + } + ] + }, + "domains": [ + "alza.cz" + ], + "id": "c7a067c4-a365-4497-a990-c042545dfd6c", + "last_modified": 1676903765296 + }, + { + "click": { + "optIn": "button.css-k8o10q", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "femina.hu" + ], + "id": "d1b9f7af-26d3-4f1b-812c-9f8cf41da900", + "last_modified": 1676903765293 + }, + { + "click": { + "optIn": "a#truste-consent-button", + "optOut": "a#truste-consent-required2", + "presence": "div#truste-consent-buttons" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "poste.it" + ], + "id": "450e91f1-22ee-4718-9e98-05f831adfeff", + "last_modified": 1676903765289 + }, + { + "click": { + "optIn": "button#almacmp-modalConfirmBtn", + "presence": "div.almacmp-controls" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "kauppalehti.fi" + ], + "id": "abb44f4b-bc8a-49bf-8f8c-204fe33806e9", + "last_modified": 1676903765285 + }, + { + "click": { + "optIn": "button.css-1j8kkja", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "news247.gr" + ], + "id": "1b1bdbb4-089c-45dc-9d42-e87a1fa85882", + "last_modified": 1676903765281 + }, + { + "click": { + "optIn": "a#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "optOut": "a#CybotCookiebotDialogBodyLevelButtonLevelOptinDeclineAll", + "presence": "div#CybotCookiebotDialog" + }, + "schema": 1676900248050, + "cookies": { + "optIn": [], + "optOut": [ + { + "name": "CookieConsent", + "value": "{stamp:%27aQlJVTwi41tewItmxlt/w7TRvv2UDwd39lOxJC/s1TG9dFvEWjL4jQ==%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cver:1%2Cutc:1665481193123%2Cregion:%27ro%27}" + } + ] + }, + "domains": [ + "rip.ie" + ], + "id": "051ccd77-b9b3-4558-9c27-5293d8d735f4", + "last_modified": 1676903765278 + }, + { + "click": { + "optIn": "button.sfc-button--primary-white", + "presence": "div.sfc-col-lg-3" + }, + "schema": 1676900248050, + "cookies": { + "optOut": [ + { + "name": "RABO_PSL", + "value": "1" + } + ] + }, + "domains": [ + "rabobank.nl" + ], + "id": "fe8a3adb-ce3c-4dd6-83db-772e71674da3", + "last_modified": 1676903765267 + }, + { + "click": { + "optIn": "a.cmptxt_btn_yes", + "presence": "div.cmpboxbtns" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "informer.rs" + ], + "id": "27572d79-fc44-49f0-b271-bd44e76f3a3f", + "last_modified": 1676903765263 + }, + { + "click": { + "optIn": "button.eu-cookie-compliance-default-button", + "presence": "div#popup-buttons" + }, + "schema": 1676900248050, + "cookies": { + "optIn": [ + { + "name": "cookie-agreed", + "value": "2" + } + ], + "optOut": [] + }, + "domains": [ + "eluniversal.com.mx" + ], + "id": "34e9d8ee-5e8d-462c-ab39-d25260124cf6", + "last_modified": 1676903765259 + }, + { + "click": { + "optIn": "button.css-k8o10q", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "athensmagazine.gr" + ], + "id": "c0e69dcc-9602-4d7a-89b8-a4a06f0432ca", + "last_modified": 1676903765255 + }, + { + "click": { + "optIn": "button.cookie-banner-lgpd_accept-button", + "presence": "div#cookie-banner-lgpd" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "globo.com" + ], + "id": "90b8eda3-d964-4301-94fc-646e92d33d56", + "last_modified": 1676903765251 + }, + { + "click": { + "optIn": "button.css-1x99van", + "presence": "div.qc-cmp2-summary-buttons" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "express.co.uk" + ], + "id": "edaa2c50-1fbc-4287-8d15-68af2b5dc1bf", + "last_modified": 1676903765248 + }, + { + "click": { + "optIn": "button.css-1lgi3ta", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "pronews.gr" + ], + "id": "9e16f526-4d46-4c2e-a3ed-d3b43d67b4fb", + "last_modified": 1676903765244 + }, + { + "click": { + "optIn": "button#almacmp-modalConfirmBtn", + "presence": "div.almacmp-controls" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "nettiauto.com" + ], + "id": "7e5d072f-ea5a-4a82-9ea4-3bbf06d5cc8d", + "last_modified": 1676903765237 + }, + { + "click": { + "optIn": "button.js-accept", + "presence": "div.cookie-banner-buttons" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "emag.hu" + ], + "id": "e78a3fdb-bcba-402c-b2da-63994aba1b30", + "last_modified": 1676903765229 + }, + { + "click": { + "optIn": "button.css-b2defl", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "ilgiornale.it" + ], + "id": "362b64e3-c12d-4d5e-9c8e-169df9782b28", + "last_modified": 1676903765225 + }, + { + "click": {}, + "schema": 1676900248923, + "cookies": { + "optOut": [ + { + "name": "CookieConsent", + "value": "{stamp:%27hLskZi6GtaCNeWwvKfJCnNdwijLGPiuqz0obMpsw2C4o6inSM80MLQ==%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cmethod:%27explicit%27%2Cver:1%2Cutc:1675088694646%2Ciab2:%27CPmZlUAPmZlUACGABBENC1CgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA%27%2Cgacm:%271~%27%2Cregion:%27ro%27}" + } + ] + }, + "domains": [ + "worten.pt" + ], + "id": "905b5c20-000d-4b7b-a8d6-d84d2311dfdf", + "last_modified": 1676903765222 + }, + { + "click": { + "optIn": "button.css-14ubilm", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "tradera.com" + ], + "id": "deca4013-d55c-424b-9a46-2df8139fd415", + "last_modified": 1676903765218 + }, + { + "click": { + "optIn": "a.cmptxt_btn_yes", + "presence": "div.cmpboxbtns" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "alo.rs" + ], + "id": "4dce4250-5d45-4cf8-bb0c-f485de812859", + "last_modified": 1676903765214 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons-container" + }, + "schema": 1676900248923, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPg1oEAPg1oEAEsABCBGClCgAAAAAAAAAAIwAAAOhQD2F2K2kKEkfjSUWYAQBCujKEIhUAAAAECBIAAAAUgQAgFIIAgAAlACAAAAABAQAQCAgAQABAAAoACgAAAAAAAAAAAAAAQQAABAAIAAAAAAAAEAQAAIAAQAAAAAAABEhCAAQQAEAAAAAAAAAAAAAAAAAAABAAA%22%2C%221~%22%2C%22A7193324-AE65-4BF2-B769-43CD7980AA9B%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "mobile.bg" + ], + "id": "3708e6f7-9237-4ef1-8aca-bec748cc676e", + "last_modified": 1676903765210 + }, + { + "click": { + "optIn": "button.css-mn27n0", + "presence": "div.qc-cmp2-summary-buttons" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "observador.pt" + ], + "id": "c49fa28b-a67c-42b8-a4e5-4d48d34fba04", + "last_modified": 1676903765207 + }, + { + "click": { + "optIn": "div.boton_cookies", + "presence": "div#popup_cookies" + }, + "schema": 1676900248923, + "cookies": { + "optIn": [ + { + "name": "accept_cookies", + "value": "ok" + } + ] + }, + "domains": [ + "aemet.es" + ], + "id": "60ea2eca-10af-47d1-a240-cbdf44519e9d", + "last_modified": 1676903765203 + }, + { + "click": { + "optIn": "button.css-k8o10q", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "napi.hu" + ], + "id": "2b12c85e-4ec0-4a3d-9cbe-9892db3f5850", + "last_modified": 1676903765199 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons" + }, + "schema": 1676900248923, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPhCz0APhCz0AEsABCFIClCgAAAAAAAAAApAAAAOhQD2F2K2kKEkfjSUWYAQBCujIEIhUAAAAECBIAAAAUgQAgFIIAgAAlACAAAAABAQAQCAgAQABAAAoACgAAAAAAAAAAAAAAQQAABAAIAAAAAAAAEAQAAIAAQAAAAAAABEhCAAQQAEAAAAAAAAAAAAAAAAAAABAAA.YAAAAAAAAAA%22%2C%221~%22%2C%22EE7DBC34-5A3A-4088-90DB-A679D6C3A244%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "telsu.fi" + ], + "id": "0d950828-684c-48f3-8b0c-3c9523c05fb6", + "last_modified": 1676903765196 + }, + { + "click": { + "optIn": "button.css-k8o10q", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "gossip-tv.gr" + ], + "id": "ba9c17cd-19aa-4fe3-ac0c-9c3055133c5b", + "last_modified": 1676903765192 + }, + { + "click": { + "optIn": "button#pt-accept-all", + "optOut": "div#pt-close", + "presence": "div#pubtech-cmp" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "liberoquotidiano.it" + ], + "id": "531d8b09-8fce-4019-9f96-31d5add26a43", + "last_modified": 1676903765188 + }, + { + "click": { + "optIn": "button.css-19ukivv", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "vi.nl" + ], + "id": "5026561f-ffec-437e-bad8-820ac9d55659", + "last_modified": 1676903765185 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons" + }, + "schema": 1676900248923, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPhCz0APhCz0AEsABCBGClCgAAAAAAAAAAIwAAAQvQD-F2K2lKGkfjaUeYIQBKujOEIhUBgEAEKBIQAEA0gQAgFIIAgADlgCUAAAABARCQCAgAQABAAAoICgAAAAACAAAABAASQQAABAAIAAICAABAUBQAAIAARAAgAAMBBEhDAASSAECAAAAAACAAIAAAAAAAABAAAAAAAAAIAAAAAAAAAAAAAEAAA.YAAAAAAAAAA%22%2C%221~%22%2C%22924A9049-E66F-4E81-92F6-E6AE4366B6B6%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "news.bg" + ], + "id": "1a473c52-96a0-4d12-a8cd-d12d1cc8edca", + "last_modified": 1676903765181 + }, + { + "click": { + "optIn": "button.css-k8o10q", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "zougla.gr" + ], + "id": "0587a3cf-e084-40bf-b01a-2586c2313f3b", + "last_modified": 1676903765177 + }, + { + "click": { + "optIn": "button.css-1al1vdb", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "calciomercato.com" + ], + "id": "68c6fe41-fb1d-41ed-a8c6-55fde975f0ab", + "last_modified": 1676903765174 + }, + { + "click": { + "optIn": "a#consent_prompt_submit", + "presence": "div#__tealiumGDPRecModal" + }, + "schema": 1676900248923, + "cookies": { + "optOut": [ + { + "name": "CONSENTMGR", + "value": "c1:0%7Cc6:0%7Cts:1666183304450%7Cconsent:false" + } + ] + }, + "domains": [ + "telenor.no" + ], + "id": "4ed03d1e-155c-45fa-a8c9-fbccc9ac0f33", + "last_modified": 1676903765170 + }, + { + "click": { + "optIn": "button#accept-privacy-policies", + "presence": "div.emb38ba0" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "walmart.ca" + ], + "id": "0485cf43-5df0-41aa-83da-92cd3b82d6f7", + "last_modified": 1676903765166 + }, + { + "click": { + "optIn": "button.css-1hzdrx2", + "presence": "div.qc-cmp2-summary-buttons" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "newsbeast.gr" + ], + "id": "b9fd5871-876d-4931-9f76-dd695e613919", + "last_modified": 1676903765162 + }, + { + "click": { + "optIn": "button.rodo-popup-agree", + "presence": "div.rodo-popup-buttons" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "pomponik.pl" + ], + "id": "397fdd1a-d6b7-4bab-a105-3f1f36f755ff", + "last_modified": 1676903765159 + }, + { + "click": { + "optIn": "button.accept-cookie-button", + "presence": "div.cookie-banner-content" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "avanza.se" + ], + "id": "803fbd5e-3734-4cf6-a878-5ffe24ec6809", + "last_modified": 1676903765155 + }, + { + "click": { + "optIn": "button#consent_prompt_submit", + "presence": "div#__tealiumGDPRecModal" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "post.ch" + ], + "id": "c40f3982-0372-4cdd-8aea-c150afd8328e", + "last_modified": 1676903765151 + }, + { + "click": { + "optIn": "button#didomi-notice-agree-button", + "presence": "div#buttons" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "zerozero.pt" + ], + "id": "1a37cced-b12a-44d9-a576-d1b259312471", + "last_modified": 1676903765145 + }, + { + "click": { + "optIn": "input.btn-secondary", + "presence": "div.stampenCookieContainer" + }, + "schema": 1676900248923, + "cookies": {}, + "domains": [ + "gp.se" + ], + "id": "e4e59ff1-b18c-453b-a4e0-6c0ec4008d81", + "last_modified": 1676903765138 + }, + { + "click": { + "optIn": "a#wt-cli-accept-all-btn", + "presence": "div.cli-bar-container" + }, + "schema": 1676900249813, + "cookies": { + "optIn": [ + { + "name": "viewed_cookie_policy", + "value": "yes" + } + ] + }, + "domains": [ + "katalozi.net" + ], + "id": "caddc16b-c8ad-404a-bd29-6383977695cb", + "last_modified": 1676903765135 + }, + { + "click": { + "optIn": "button.rounded-button--tertiary", + "presence": "div.legal-consent" + }, + "schema": 1676900249813, + "cookies": { + "optOut": [ + { + "name": "mal_consent_gdpr_remarketing", + "value": "f" + }, + { + "name": "mal_consent_gdpr_personalization", + "value": "f" + } + ] + }, + "domains": [ + "mall.cz" + ], + "id": "6bc9ceec-b225-4284-b924-589c3ff4f249", + "last_modified": 1676903765131 + }, + { + "click": { + "optIn": "a.wscrOk", + "optOut": "a.wscrOk2", + "presence": "div#CookieReportsPanel" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "nordea.fi" + ], + "id": "cf282f72-5333-48a8-9097-cbc89ea26634", + "last_modified": 1676903765127 + }, + { + "click": { + "optIn": "button#grantPermissionButton", + "presence": "div#cookie-bar" + }, + "schema": 1676900249813, + "cookies": { + "optOut": [ + { + "name": "CookiePermissionInfo", + "value": "%7B%22LastModifiedDate%22%3A%22%5C%2FDate(1666257491849)%5C%2F%22%2C%22ExpirationDate%22%3A%22%5C%2FDate(1697793491849)%5C%2F%22%2C%22Allow%22%3Afalse%2C%22CategoryPermission%22%3A%5B%7B%22Category%22%3A%22Cat.8%22%2C%22Permission%22%3Atrue%7D%2C%7B%22Category%22%3A%22Cat.9%22%2C%22Permission%22%3Atrue%7D%2C%7B%22Category%22%3A%22Cat.10%22%2C%22Permission%22%3Afalse%7D%2C%7B%22Category%22%3A%22Cat.11%22%2C%22Permission%22%3Afalse%7D%2C%7B%22Category%22%3A%22Cat.12%22%2C%22Permission%22%3Afalse%7D%5D%7D" + } + ] + }, + "domains": [ + "postnl.nl" + ], + "id": "e4a9e084-a37e-4f10-969c-c872f578dd15", + "last_modified": 1676903765124 + }, + { + "click": { + "optIn": "button.pr-riqlv6", + "optOut": "button.pr-1l8klmj", + "presence": "div#consent" + }, + "schema": 1676900249813, + "cookies": { + "optOut": [ + { + "name": "data_consent", + "value": "1.1.0.0.0.1675177081479.Default" + } + ] + }, + "domains": [ + "pricerunner.dk" + ], + "id": "be039e17-f095-46f4-a809-c00699799db8", + "last_modified": 1676903765114 + }, + { + "click": { + "optIn": "div.tvp-covl__ab", + "presence": "div#ip" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "tvp.pl" + ], + "id": "fb333123-4f00-4b45-acf9-21cb038c76ba", + "last_modified": 1676903765107 + }, + { + "click": { + "optIn": "button.css-1g5s5vy", + "presence": "div#qc-cmp2-ui" + }, + "schema": 1676900249813, + "cookies": {}, + "domains": [ + "meo.pt" + ], + "id": "992a91e5-9d29-411a-a8e6-e689fb75c2b7", + "last_modified": 1676903765103 + }, + { + "click": { + "optIn": "span#banner-cookie--button", + "presence": "div#banner-cookie" + }, + "schema": 1676900247175, + "cookies": { + "optIn": [ + { + "name": "cookie_banner", + "value": "1" + } + ] + }, + "domains": [ + "bitly.com" + ], + "id": "84a097a3-5cd1-448a-afcf-4be950bc5756", + "last_modified": 1676903765100 + }, + { + "click": { + "optIn": "button.ch2-allow-all-btn", + "optOut": "button.ch2-deny-all-btn", + "presence": "div.ch2-container" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "semrush.com" + ], + "id": "2f54e492-0f33-4496-ab08-99af50bf6f22", + "last_modified": 1676903765096 + }, + { + "click": { + "optIn": "button.btn-success", + "presence": "div.vue-component" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "remove.bg" + ], + "id": "86bc6c6f-c082-466f-ac77-994a2296fbb0", + "last_modified": 1676903765093 + }, + { + "click": { + "optIn": "button#CybotCookiebotDialogBodyButtonAccept", + "presence": "div#CybotCookiebotDialog" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "sectigo.com" + ], + "id": "7078621c-2701-40e0-83f1-109c8ea0f82f", + "last_modified": 1676903765089 + }, + { + "click": { + "optOut": "button.eu-cookie-compliance-save-preferences-button", + "presence": "div.eu-cookie-compliance-banner" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "cam.ac.uk" + ], + "id": "f5f827d0-60c9-4fe6-af97-a901d0d96ddc", + "last_modified": 1676903765081 + }, + { + "click": { + "optIn": "button#_evidon-accept-button", + "optOut": "button#_evidon-decline-button", + "presence": "div#_evidon-message" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "playstation.com" + ], + "id": "23df4915-5954-41ea-8d5c-e76a1d98e70b", + "last_modified": 1676903765077 + }, + { + "click": { + "optIn": "button#truste-consent-button", + "presence": "div#consent_blackbar" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "fortune.com" + ], + "id": "c0908337-da46-4155-8178-43e4a67693ec", + "last_modified": 1676903765069 + }, + { + "click": { + "optIn": "button#truste-consent-button", + "presence": "div#truste-consent-track" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "mi.com" + ], + "id": "30a2b81a-b5a6-4c49-818d-de34ecddafbe", + "last_modified": 1676903765060 + }, + { + "click": { + "optIn": "button.css-1k47zha", + "presence": "div#qc-cmp2-ui" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "9gag.com" + ], + "id": "6c246e7d-2a12-4b13-94b4-d2908cd64aad", + "last_modified": 1676903765055 + }, + { + "click": { + "optIn": "a.a8c-cookie-banner__accept-all-button", + "presence": "form.a8c-cookie-banner" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "akismet.com" + ], + "id": "9a5038c6-31da-40e5-94d8-6eea3ecfa9ec", + "last_modified": 1676903765052 + }, + { + "click": { + "optIn": "button#truste-consent-button", + "optOut": "button#truste-consent-required", + "presence": "div#truste-consent-track" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "box.com", + "ea.com" + ], + "id": "b8e927a8-e0fc-4e7b-ad60-edca983accd3", + "last_modified": 1676903765048 + }, + { + "click": { + "optIn": "button.eu-cookie-compliance-default-button", + "presence": "div.cookies" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "unesco.org" + ], + "id": "689fe5d6-7792-4475-98fc-71aa1715d0d9", + "last_modified": 1676903765044 + }, + { + "click": { + "optIn": "div.zbc-cta-accept", + "presence": "div.zbottom-cookie-container" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "zoho.com" + ], + "id": "3656dd64-5840-4918-adc8-caa723503f20", + "last_modified": 1676903765040 + }, + { + "click": { + "optIn": "button.css-1k3tyyb", + "presence": "div#qc-cmp2-ui" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "theatlantic.com" + ], + "id": "43f34378-73c3-4851-a948-e073908edddd", + "last_modified": 1676903765033 + }, + { + "click": { + "optIn": "button#truste-consent-button", + "optOut": "button#truste-consent-required", + "presence": "div#truste-consent-content" + }, + "schema": 1676900250688, + "cookies": { + "optOut": [ + { + "name": "notice_gdpr_prefs", + "value": "0:" + } + ] + }, + "domains": [ + "squarespace.com" + ], + "id": "b17a2376-ae8b-40de-9b6f-61efdf25f862", + "last_modified": 1676903765030 + }, + { + "click": { + "optIn": "button.osano-cm-accept-all", + "presence": "div.osano-cm-window" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "scribd.com", + "chicagotribune.com" + ], + "id": "f6f48ce2-0487-4003-b1c7-dfcd37def8d7", + "last_modified": 1676903765026 + }, + { + "click": { + "optIn": "button.cc-banner__button-accept", + "presence": "section.cc-banner" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "springer.com", + "nature.com" + ], + "id": "9ab30eae-9592-47b1-b46e-7640c4316f14", + "last_modified": 1676903765022 + }, + { + "click": { + "optIn": "button#cookie-accept-all-secondary", + "optOut": "button.CookieConsentSecondary__RejectAllButton-sc-12do1ry-2", + "presence": "div.ModalInner__Backdrop-sc-1ghkydj-1" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "nordnet.no" + ], + "id": "9ea83ecf-3e82-4976-80e7-86872d7e4aeb", + "last_modified": 1676903765019 + }, + { + "click": { + "optIn": "button#accept_all_cookies", + "presence": "section#cookie_consent" + }, + "schema": 1676900250688, + "cookies": {}, + "domains": [ + "telekom.hu" + ], + "id": "80d51057-06fe-4469-be50-0438c9165020", + "last_modified": 1676903765015 + }, + { + "click": { + "optIn": "button.primary", + "presence": "div#camus-cookie-disclaimer" + }, + "schema": 1676900250688, + "cookies": { + "optIn": [ + { + "name": "accepts-cookies", + "value": "true" + } + ] + }, + "domains": [ + "milenio.com" + ], + "id": "befe1d72-f33a-4e63-be43-236bedc3b49a", + "last_modified": 1676903765008 + }, + { + "click": { + "optIn": "button.css-k8o10q", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900248050, + "cookies": {}, + "domains": [ + "newsit.gr", + "protothema.gr", + "newsbomb.gr", + "youweekly.gr", + "vrisko.gr", + "index.hu", + "meteo.gr", + "fantacalcio.it" + ], + "id": "25dd408a-0a98-4e93-992c-abd320255cd1", + "last_modified": 1676903764958 + }, + { + "click": { + "optIn": "button.css-47sehv", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "pik.bg", + "prosport.ro", + "sciencedaily.com" + ], + "id": "d96f380c-c76f-47d8-a1f7-bd4063792ade", + "last_modified": 1676903764933 + }, + { + "click": { + "optIn": "button.css-1lgi3ta", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "sdna.gr", + "makeleio.gr" + ], + "id": "af566db1-982c-4837-8f0b-96a341702520", + "last_modified": 1676903764929 + }, + { + "click": { + "optIn": "div.gdpr-trigger", + "presence": "div#gdpr" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "digisport.ro" + ], + "id": "91a1519c-51f4-4d9a-bea3-06a2fff7d892", + "last_modified": 1676903764926 + }, + { + "click": { + "optIn": "button.css-1tfx6ee", + "presence": "div#qc-cmp2-container" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "startlap.hu", + "nosalty.hu", + "24.hu", + "nlc.hu" + ], + "id": "4fc95ff9-b785-49ee-8a8c-e789665a1643", + "last_modified": 1676903764922 + }, + { + "click": { + "optIn": "button.NT2yCg", + "presence": "div.e4Qmjw" + }, + "schema": 1676900251552, + "cookies": {}, + "domains": [ + "canva.com" + ], + "id": "5a03b949-b86c-4ee5-9824-97fb59849cb8", + "last_modified": 1676903764913 + }, + { + "click": { + "optIn": "button#privacy-cp-wall-accept", + "presence": "div.privacy-cp-wall" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "corriere.it" + ], + "id": "b90700d6-32a6-44e2-adf4-46acd54089c9", + "last_modified": 1674560738379 + }, + { + "click": { + "optIn": "button#privacy-cp-wall-accept", + "presence": "div.privacy-cp-wall" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "gazzetta.it" + ], + "id": "32be6ed7-749d-448f-975d-10b4d1e6e482", + "last_modified": 1674560738375 + }, + { + "click": { + "optIn": "button.css-1u05hh5", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "researchgate.net" + ], + "id": "e060a13e-e0ae-4081-8d9f-29a0c17bec79", + "last_modified": 1674560738368 + }, + { + "click": { + "optIn": "button.primary_2xk2l", + "presence": "div.container_1gQfi" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "dailymail.co.uk" + ], + "id": "4d4b2924-6e28-4d39-9a02-e31690746ab2", + "last_modified": 1674560738364 + }, + { + "click": { + "optIn": "button.osano-cm-accept-all", + "presence": "div.osano-cm-dialog__buttons" + }, + "schema": 1674146203493, + "cookies": { + "optOut": [ + { + "name": "osano_consentmanager", + "value": "29z_AY1nTFjK9XMxsOgqw44fXSaxiin_dEBjbDS7Xgl2GM5AONcjcMtZVKd_7n7XCTO5hym727ANlN292apKnDkgIe8dhCSSZpmDhDN7guy_H9a0nFHng1IsTphnnPGMG-iLZ6sS9OmdxmAq8K_mehLsgQWFmfhi5K07-UmLlGoVGx19IxTDiqf-aJmp5dbLV8kxLYjgbTFnTJ2JWUcwJYR57TX9iezs-BprQopNCs5CGWW9NOFrv-uhWhwTsKNLygKH7cMwdnp7RHTSX41-FCLSh10=" + } + ] + }, + "domains": [ + "wiley.com" + ], + "id": "7e45fd5f-63b5-4090-9120-b467b0252358", + "last_modified": 1674560738354 + }, + { + "click": { + "optIn": "div.gdpr-agree-btn", + "optOut": "div.gdpr-reject-btn", + "presence": "div#GDPR-cookies-notice" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "alibaba.com" + ], + "id": "c35bdda6-611d-4742-8f6e-4aebb574e675", + "last_modified": 1674560738349 + }, + { + "click": { + "optIn": "button.kcLcLu", + "presence": "div.gmjWml" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "turbopages.org" + ], + "id": "6ecb9b79-3666-479c-8798-5cf46702799f", + "last_modified": 1674560738346 + }, + { + "click": { + "optIn": "span.nrk-sr", + "presence": "div.nrk-masthead__info-banner--cookie" + }, + "schema": 1674146203493, + "cookies": { + "optIn": [ + { + "name": "nrkno-cookie-information", + "value": "1" + } + ] + }, + "domains": [ + "nrk.no" + ], + "id": "15cdab7d-a018-4c88-9352-5eacd4c30d1d", + "last_modified": 1674560738342 + }, + { + "click": { + "optIn": "button.css-1v69bou", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "sapo.pt" + ], + "id": "f29b0658-24ab-40fd-92fb-11850b2aaab5", + "last_modified": 1674560738339 + }, + { + "click": {}, + "schema": 1674146203493, + "cookies": { + "optOut": [ + { + "name": "yleconsent", + "value": "v1|" + } + ] + }, + "domains": [ + "yle.fi" + ], + "id": "9a727363-cec6-4641-99ab-80550b89da5b", + "last_modified": 1674560738332 + }, + { + "click": { + "optIn": "div.ok", + "presence": "div#checkUPcookies" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "pravda.com.ua" + ], + "id": "7498f52b-4273-4cfb-a22a-c9df9b84adc2", + "last_modified": 1674560738328 + }, + { + "click": { + "optIn": "button.css-w0lt3s", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "iefimerida.gr" + ], + "id": "aeff05cc-c287-4596-89b6-3559eced1133", + "last_modified": 1674560738325 + }, + { + "click": { + "optIn": "button.css-wum6af", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "record.pt" + ], + "id": "8b2a32a7-5a38-4ffc-abc5-d5827e16008b", + "last_modified": 1674560738318 + }, + { + "click": { + "optIn": "div.gdpr-trigger", + "presence": "div#gdpr" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "digi24.ro" + ], + "id": "55ed573b-2cf3-42a3-8010-d397b161568b", + "last_modified": 1674560738315 + }, + { + "click": { + "optIn": "button.CookieConsent__primaryButton___Th47k", + "presence": "div.CookieConsent__root___3GjU8" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "svt.se" + ], + "id": "87b229aa-567e-4549-895f-b6c0065dfdc5", + "last_modified": 1674560738311 + }, + { + "click": { + "optIn": "button.css-1rx6u69", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "gazzetta.gr" + ], + "id": "7eed4367-8f21-4359-aae9-cf9017ae5efc", + "last_modified": 1674560738308 + }, + { + "click": { + "optIn": "button.css-k9c0va", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674146203493, + "cookies": {}, + "domains": [ + "foreca.fi" + ], + "id": "f35220db-4382-446a-af55-063f309ccf5a", + "last_modified": 1674560738304 + }, + { + "click": { + "optIn": "button.css-1tdle7a", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "iol.pt" + ], + "id": "290d1140-dc1b-4d97-85dd-d970753ae441", + "last_modified": 1674560738290 + }, + { + "click": { + "optIn": "button.allow-all-cookies-button", + "optOut": "button.mt-2", + "presence": "div.consent-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "ilmatieteenlaitos.fi" + ], + "id": "0cfab897-c0f5-4415-b5a1-bac258d1c764", + "last_modified": 1674560738287 + }, + { + "click": { + "optIn": "button.css-kc1iqc", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "telex.hu" + ], + "id": "24df6217-9ed9-4a67-9811-ff9c8f8faaf2", + "last_modified": 1674560738283 + }, + { + "click": { + "optIn": "button#c-p-bn", + "presence": "div#cc_div" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "shmu.sk" + ], + "id": "43135a56-c361-4d88-82dd-cf17a077a6a5", + "last_modified": 1674560738280 + }, + { + "click": { + "optIn": "button#cookieBtn", + "presence": "div.c-alert__box" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "tsn.ua" + ], + "id": "04608cf9-0f96-433b-bc26-6aa9f5ebdfc8", + "last_modified": 1674560738276 + }, + { + "click": { + "optIn": "button.css-8zhqum", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "hvg.hu" + ], + "id": "83758fdc-5bdc-4d8b-b783-840f16d30f1c", + "last_modified": 1674560738269 + }, + { + "click": { + "optIn": "button.css-wum6af", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "cmjornal.pt" + ], + "id": "e400d28c-7f73-4449-8749-5f96509eb688", + "last_modified": 1674560738266 + }, + { + "click": { + "optIn": "button.css-1ruupc0", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "444.hu" + ], + "id": "eaf913b1-b628-4a57-9a1c-65bcf27f33a9", + "last_modified": 1674560738259 + }, + { + "click": { + "optIn": "i.fa-times", + "presence": "div.cookieWarning" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "kukaj.io" + ], + "id": "8f251e48-f45b-4051-bc53-0851d0d411f5", + "last_modified": 1674560738255 + }, + { + "click": { + "optIn": "button.notificationButton", + "presence": "div.polopolyNotification" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "smhi.se" + ], + "id": "27d9194f-f908-4cb8-8f57-ddb899a06a4e", + "last_modified": 1674560738252 + }, + { + "click": { + "optIn": "button.css-fxmqu", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "publico.pt" + ], + "id": "36dc7cc5-a043-4259-a9bd-07913ff208bb", + "last_modified": 1674560738245 + }, + { + "click": { + "optIn": "button.js_cookie-monster-agree", + "optOut": "a.js_cookie-monster-disagree", + "presence": "div#js_cookie-monster" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "ceneo.pl" + ], + "id": "35db03d9-02c9-4558-a31b-aa4a20d6dbaa", + "last_modified": 1674560738241 + }, + { + "click": { + "optIn": "a.js-accept-cookies", + "presence": "div.jsGdprNoticeHolder" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "polovniautomobili.com" + ], + "id": "9a6465c6-7955-4dff-9483-70635ba5fbda", + "last_modified": 1674560738230 + }, + { + "click": { + "optOut": "button.css-zpqvhj ", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521303759, + "cookies": {}, + "domains": [ + "sorozatbarat.club" + ], + "id": "fbff28bd-84e5-4f82-8023-2f00d772e7e8", + "last_modified": 1674560738226 + }, + { + "click": { + "optIn": "button.RMlTST", + "optOut": "button.WEzN4V", + "presence": "div.aspXfg" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "wix.com" + ], + "id": "aa245048-507c-11ed-bdc3-0242ac120002", + "last_modified": 1674560738215 + }, + { + "click": { + "optIn": "button.submitAll", + "presence": "div.submitButton" + }, + "schema": 1674521304583, + "cookies": { + "optOut": [ + { + "name": "CookieConsent", + "value": "{stamp:%27D1AamT1V80siZwfIuWr5SREHguKw3sySW3+Q+Nb2mPwfRB9so2m1Sg==%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cver:5%2Cutc:1664957253372%2Cregion:%27ro%27}" + } + ] + }, + "domains": [ + "dr.dk" + ], + "id": "9def1218-9201-4b49-bf69-ea203aeab2b3", + "last_modified": 1674560738211 + }, + { + "click": { + "optIn": "button.css-1wjnr64", + "presence": "div.qc-cmp2-container" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "idokep.hu" + ], + "id": "1dc10ed2-800f-484c-9df1-6f99d8cd2584", + "last_modified": 1674560738204 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-choice-dialog" + }, + "schema": 1674521304583, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPgbQkAPgbQkAEsABCHUCjCoAP_AAH_AAA6gHQpB7D7FbSFCyP55aLsAMAhXRkCEAqQAAASFAmABQAKQIBQCkkAQFAygBCACAAAgIAJBAQIMCAgACUEBQABAAAEEAAAABAAIIAAAgAEAAAAIAAACAIAAEAAIQAAAEAAAmQhAAIIACAAABAAAAAAAAAAAAAAAAgdCgHsLsVtIUJI_GkoswAgCFdGQIQCoAAAAIUCQAAAApAgBAKQQBAADKAEIAAAACAgAgEBAAgACAABQQFAAEAAAAAAAAAAAAggAACAAQAAAAAAAAIAgAAQAAhAAAAAAACJCEAAggAIAAAAAAAAAAAAAAAAAAACAAA.cqAADVgAAAA%22%2C%221~2052.2072.70.89.93.108.122.149.2202.162.167.196.2253.241.2299.259.2331.2357.311.317.323.2373.338.358.2415.415.440.449.2506.2526.482.486.494.495.2568.2571.2575.540.574.2624.2677.2707.817.827.2898.864.981.1031.1048.1051.1067.1095.1097.1127.1171.1201.1205.1211.1276.1301.1365.1415.1449.1570.1577.1616.1651.1716.1753.1765.1870.1878.1889.1917.1958.2012%22%2C%228BB76B67-7925-4F57-BFC5-31C47ABF23A9%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "origo.hu" + ], + "id": "f236d4c4-1f2f-4b62-a4ec-5d9388d9150e", + "last_modified": 1674560738200 + }, + { + "click": { + "optIn": "button.rodo-popup-agree", + "presence": "div.rodo-popup-buttons" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "interia.pl" + ], + "id": "ef9bacb9-d248-4129-baed-354a45277750", + "last_modified": 1674560738197 + }, + { + "click": { + "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "optOut": "button#CybotCookiebotDialogBodyButtonDecline", + "presence": "div#CybotCookiebotDialogBodyButtons" + }, + "schema": 1674521304583, + "cookies": { + "optOut": [ + { + "name": "CookieConsent", + "value": "{stamp:%27QmpmezEASR/DS4/lr1mawVaNO945EW6sk0Hi0cjL1CFS1A36t8Os1Q==%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cver:3%2Cutc:1665042549753%2Ciab2:%27CPgbQkAPgbQkACGABBENCjCgAAAAAE_AAAAAAAASCAJMNW4gC7EscCbQMIoEQIwrCQqgUAEFAMLRAYAODgp2VgE-sIEACAUARgRAhxBRgQCAAACAJCIAJAiwQCIAiAQAAgARCIQAEDAIKACwMAgABANAxRCgAECQgyICIpTAgIgSCAlsqEEoK5DTCAOssAKDRGxUACJAABSAAJCwcAwRICViwQJMUb5ACMEKAUSoVoAA.YAAAAAAAAAAA%27%2Cgacm:%271~%27%2Cregion:%27ro%27}" + } + ] + }, + "domains": [ + "ekstrabladet.dk" + ], + "id": "7c2c6827-7d60-49a5-b4c1-877ba7fcd2e7", + "last_modified": 1674560738189 + }, + { + "click": { + "optIn": "button.iubenda-cs-accept-btn", + "presence": "div#iubenda-cs-banner" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "repubblica.it" + ], + "id": "5c862324-6a7c-43d8-ae34-47c5e7d355f6", + "last_modified": 1674560738185 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-choice-dialog" + }, + "schema": 1674521304583, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPgbQkAPgbQkAEsABCBGCjCgAAAAAAAAAAIwAAAPaQD2F2K2kKEkfjaUWYIQBCujKEIBUBAEAEKBIAAAAUgQAgFIIAgABlACEAAAABAQAQCAgAQABAAAoICgACAAAAAQAAAAAAQQAABAAIAAACAAAAEAQAAAAAQAAAAAAABEhCAAQSQEAAAAAAAAAAAAAAAAAAABAAAAAAAAAIAA.YAAAAAAAAAA%22%2C%221~%22%2C%226C13DA30-0357-4AD8-8594-DBD146A8DF46%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "blitz.bg" + ], + "id": "7850dc20-f121-417b-9eb0-d91a0a8d6a8c", + "last_modified": 1674560738181 + }, + { + "click": { + "optIn": "button.fc-primary-button", + "presence": "div.fc-footer-buttons" + }, + "schema": 1674521304583, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPgejgAPgejgAEsABCBGCkCgAAAAAAAAAAIwAAAOhQD2F2K2kKEkfjSUWYAQBCujKEIhUAAAAECBIAAAAUgQAgFIIAgAAlACAAAAABAQAQCAgAQABAAAoACgAAAAAAAAAAAAAAQQAABAAIAAAAAAAAEAQAAIAAQAAAAAAABEhCAAQQAEAAAAAAAAAAAAAAAAAAABAAA%22%2C%221~%22%2C%2232D9A01E-9678-40B0-BD1B-AD0CDC174758%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "fakti.bg" + ], + "id": "abc3a8c3-6a15-48a6-83eb-4f7762df9270", + "last_modified": 1674560738175 + }, + { + "click": { + "optIn": "button.fc-cta-consent", + "presence": "div.fc-footer-buttons-container" + }, + "schema": 1674521304583, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPgocUAPgocUAEsABCFICkCgAAAAAAAAAApAAAAQWQD2F2q2kKEkfjSUWYgURCurIEIhcAAAAEKBoAAAAUgQAgFIMIgABlACEAAAABAQAQCAgAQABAAAoICgAAAAAAAAAAAAAAQQACBAAIAAAAAAAAEAQAAoAAQAAAAAAABEhCAAQQAEIAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAIAAA.YAAAAAAAAAA%22%2C%221~%22%2C%22AC745442-DC8E-42A9-AAB5-970367496CBA%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "seiska.fi" + ], + "id": "57d6c05c-44b6-4e02-91b6-ef91cca6a311", + "last_modified": 1674560738152 + }, + { + "click": { + "optIn": "button.css-hxv78t", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "irishmirror.ie" + ], + "id": "3cab2d97-d9eb-4254-97aa-441ad90ada69", + "last_modified": 1674560738146 + }, + { + "click": { + "optIn": "a.cmptxt_btn_yes", + "presence": "div.cmpboxinner" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "novosti.rs" + ], + "id": "53f117b7-5d9d-4387-8507-95fb64a4da86", + "last_modified": 1674560738136 + }, + { + "click": { + "optIn": "button.fc-primary-button", + "presence": "div.fc-footer-buttons" + }, + "schema": 1674521305403, + "cookies": { + "optOut": [ + { + "name": "FCCDCF", + "value": "%5Bnull%2Cnull%2Cnull%2C%5B%22CPgocUAPgocUAEsABCBGCkCgAAAAAAAAAAIwAAAOhQD2F2K2kKEkfjSUWYAQBCujKEIhUAAAAECBIAAAAUgQAgFIIAgAAlACAAAAABAQAQCAgAQABAAAoACgAAAAAAAAAAAAAAQQAABAAIAAAAAAAAEAQAAIAAQAAAAAAABEhCAAQQAEAAAAAAAAAAAAAAAAAAABAAA%22%2C%221~%22%2C%222259601D-A3C8-4B02-80F2-AF6ADF73E293%22%5D%2Cnull%2Cnull%2C%5B%5D%5D" + } + ] + }, + "domains": [ + "bazar.bg" + ], + "id": "a7cbcc28-fa3f-46cc-9010-613e5031549b", + "last_modified": 1674560738130 + }, + { + "click": { + "optIn": "button#almacmp-modalConfirmBtn", + "presence": "div.almacmp-controls" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "etuovi.com" + ], + "id": "a2cc4223-39e8-4e02-804b-fa9eb4a4ce65", + "last_modified": 1674560738126 + }, + { + "click": { + "optIn": "button.css-1tydlop", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "noticiasaominuto.com" + ], + "id": "cbd5d112-8190-40d1-ad4a-6e8f6aa17c5a", + "last_modified": 1674560738119 + }, + { + "click": { + "optIn": "button.css-47sehv", + "presence": "div#qc-cmp2-container" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "telegraf.rs" + ], + "id": "6785afba-648d-46cf-8633-3d9c433c79ff", + "last_modified": 1674560738115 + }, + { + "click": { + "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "optOut": "button#CybotCookiebotDialogBodyButtonDecline", + "presence": "div#CybotCookiebotDialog" + }, + "schema": 1674521305403, + "cookies": { + "optOut": [ + { + "name": "CookieConsent", + "value": "{stamp:%27hd4jdgROnfr2iNroDX0BfGjqyNhsMI7fLXjrkfZEp9hRGFDoFPyv7w==%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cmethod:%27explicit%27%2Cver:1%2Cutc:1674227765106%2Cregion:%27ro%27}" + } + ] + }, + "domains": [ + "profesia.sk" + ], + "id": "346b2412-6aef-4eea-b2d5-8b74b4917f4a", + "last_modified": 1674560738112 + }, + { + "click": { + "optIn": "button.css-1ysvf99", + "presence": "div.qc-cmp2-summary-buttons" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "flashback.org" + ], + "id": "14d5418a-4df6-4dfe-9c81-fd07669e0f9c", + "last_modified": 1674560738108 + }, + { + "click": { + "optIn": "button#almacmp-modalConfirmBtn", + "presence": "div#almacmp-footer--layer1" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "ampparit.com" + ], + "id": "679d7000-cd04-4c35-bbf8-21263a2ff357", + "last_modified": 1674560738105 + }, + { + "click": { + "optIn": "button.css-k8o10q", + "presence": "div#qc-cmp2-ui" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "videa.hu" + ], + "id": "6fed31af-f6d2-4452-88ef-8f2ca61c6f19", + "last_modified": 1674560738097 + }, + { + "click": { + "optOut": "button#modalConfirmBtn ", + "presence": "div.gravitoCMP-background-overlay" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "hitta.se" + ], + "id": "1998c717-cd3a-48a0-a9f3-67f790f98879", + "last_modified": 1674560738094 + }, + { + "click": { + "optIn": "button.css-47sehv", + "presence": "div.qc-cmp2-summary-buttons" + }, + "schema": 1674521305403, + "cookies": {}, + "domains": [ + "espreso.co.rs" + ], + "id": "ed6379a2-717a-4571-9a81-abb0c5b29098", + "last_modified": 1674560738090 + }, + { + "click": { + "optIn": "#cmpbntyestxt", + "optOut": "#cmpbntnotxt", + "presence": "div.cmpboxbtns" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "sourceforge.net" + ], + "id": "786f7ae5-548a-43d4-97a2-7b8efb5ed7a7", + "last_modified": 1674560738080 + }, + { + "click": { + "optIn": "div#gdpr-single-choice-overlay", + "presence": "button.wt-btn--filled" + }, + "schema": 1674521304583, + "cookies": { + "optOut": [ + { + "name": "p", + "value": "eyJnZHByX3RwIjoxLCJnZHByX3AiOjF9" + } + ] + }, + "domains": [ + "etsy.com" + ], + "id": "e5706488-f2e9-47bd-a178-4d569ca26ce8", + "last_modified": 1674560738076 + }, + { + "click": {}, + "schema": 1674521303759, + "cookies": { + "optOut": [ + { + "name": "gdpr", + "value": "1" + } + ] + }, + "domains": [ + "yandex.com", + "yandex.ru", + "ya.ru", + "kinopoisk.ru" + ], + "id": "37319f5d-9484-4da8-aee1-570a78688da3", + "last_modified": 1674560738073 + }, + { + "click": { + "optIn": "button.cmp-intro_acceptAll", + "presence": "div.cmp-popup_popup" + }, + "schema": 1674521304583, + "cookies": {}, + "domains": [ + "onet.pl", + "fakt.pl", + "plejada.pl", + "medonet.pl", + "businessinsider.com.pl" + ], + "id": "be15b898-a203-45cb-8200-b9b36d2af17b", + "last_modified": 1674560738053 + }, + { + "click": {}, + "schema": 1671501963715, + "cookies": { + "optOut": [ + { + "name": "notice_gdpr_prefs", + "value": "0:" + } + ] + }, + "domains": [ + "flickr.com" + ], + "id": "22d33421-6872-41bc-9316-adba6a9af18e", + "last_modified": 1671635844937 + }, + { + "click": {}, + "schema": 1671450839828, + "cookies": { + "optOut": [ + { + "name": "OPTOUTMULTI_TYPE", + "value": "A" + } + ] + }, + "domains": [ + "autodesk.com" + ], + "id": "24fdb694-9d8f-4049-a2f1-583465e711a5", + "last_modified": 1671501963591 + }, + { + "click": { + "optIn": "button#consent-accept-button", + "presence": "div.sc-36lcln-0" + }, + "schema": 1671324527556, + "cookies": {}, + "domains": [ + "xing.com" + ], + "id": "f82cabad-efa5-4637-8906-9fb842cd6556", + "last_modified": 1671383798361 + }, + { + "click": { + "optIn": "span.flex-button", + "presence": "section.jsx-539809080" + }, + "schema": 1671324525745, + "cookies": {}, + "domains": [ + "notion.so" + ], + "id": "85205acd-fa99-438e-b353-2b40adfdf868", + "last_modified": 1671383798347 + }, + { + "click": { + "optIn": "button#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll", + "optOut": "button#CybotCookiebotDialogBodyButtonDecline", + "presence": "div#CybotCookiebotDialog" + }, + "schema": 1671324523911, + "cookies": {}, + "domains": [ + "aruba.it" + ], + "id": "469218de-6bea-475f-be47-734a412b4fa7", + "last_modified": 1671383798336 + }, + { + "click": { + "optIn": "button#AcceptButton-cookie-banner", + "presence": "div#cookie-banner" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "prisjakt.nu" + ], + "id": "6950df9f-2529-4cdc-ab8b-9ba788b6d2f7", + "last_modified": 1670498157871 + }, + { + "click": { + "optIn": "button.css-1k3zih6", + "presence": "div#qc-cmp2-container" + }, + "schema": 1670333809819, + "cookies": { + "optIn": [], + "optOut": [ + { + "name": "addtl_consent", + "value": "1~" + } + ] + }, + "domains": [ + "capital.gr" + ], + "id": "8e9824be-3535-4d17-8e64-ae9628a0611d", + "last_modified": 1670498157859 + }, + { + "click": { + "optIn": "button#btnInfo", + "presence": "div#info-modal" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "limundo.com" + ], + "id": "5df00bf4-ecbc-4521-862b-db1453be23c9", + "last_modified": 1670498157844 + }, + { + "click": { + "optIn": "span.sd-cmp-3cRQ2", + "presence": "div#sd-cmp" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "forocoches.com" + ], + "id": "a0698f6e-dd50-4773-9aad-eb23688383f3", + "last_modified": 1670498157840 + }, + { + "click": { + "optIn": "div.accept-btn", + "optOut": "div.refuse-btn", + "presence": "div.cookie-pop" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "tencent.com" + ], + "id": "e964784f-f4f4-4e71-b973-8c68ef6f49d7", + "last_modified": 1670498157836 + }, + { + "click": { + "optIn": "button#truste-consent-button", + "optOut": "button#truste-consent-required", + "presence": "div#truste-consent-content" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "ibm.com" + ], + "id": "97c1b39b-6573-4dbc-83a4-3af02a416f0a", + "last_modified": 1670498157828 + }, + { + "click": { + "optIn": "button.fZYtinTR", + "presence": "div.CoZ9Nu8Z" + }, + "schema": 1670333809819, + "cookies": { + "optIn": [ + { + "name": "FC2_GDPR", + "value": "true" + } + ] + }, + "domains": [ + "fc2.com" + ], + "id": "5669aa13-d825-45b6-9a65-14904ecd9ee9", + "last_modified": 1670498157820 + }, + { + "click": { + "optIn": "div.cmc-cookie-policy-banner__close", + "presence": "div#cmc-cookie-policy-banner" + }, + "schema": 1670333809819, + "cookies": { + "optIn": [ + { + "name": "cmc_gdpr_hide", + "value": "1" + } + ] + }, + "domains": [ + "coinmarketcap.com" + ], + "id": "faccc72f-fab0-4dd8-82a5-4b97774a0058", + "last_modified": 1670498157816 + }, + { + "click": { + "optIn": "div#accept-choices", + "presence": "div.sn-inner" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "w3schools.com" + ], + "id": "230b6c9b-c717-4a88-9d64-9a892091d53d", + "last_modified": 1670498157808 + }, + { + "click": { + "optIn": "button#truste-consent-button", + "optOut": "button#truste-consent-required", + "presence": "div#truste-consent-track" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "nginx.com" + ], + "id": "aaace84f-069e-4713-85f0-7a2f419c34eb", + "last_modified": 1670498157804 + }, + { + "click": { + "optOut": "a.n-messaging-banner__button", + "presence": "div.consent-container" + }, + "schema": 1670333809819, + "cookies": { + "optIn": [ + { + "name": "optout", + "value": "0" + } + ], + "optOut": [ + { + "name": "optout", + "value": "1" + } + ] + }, + "domains": [ + "indiatimes.com" + ], + "id": "e991cb20-0597-4f14-aabc-214a2e493f90", + "last_modified": 1670498157800 + }, + { + "click": { + "optIn": "button#_evidon-accept-button", + "optOut": "button#_evidon-decline-button", + "presence": "div#_evidon_banner" + }, + "schema": 1670333809819, + "cookies": { + "optIn": [ + { + "name": "_tt_enable_cookie", + "value": "1" + } + ], + "optOut": [] + }, + "domains": [ + "eventbrite.com" + ], + "id": "25d7b523-6e33-414a-a1e8-b9f27c7e8a37", + "last_modified": 1670498157797 + }, + { + "click": { + "optIn": "a#okck", + "presence": "div#toast-container" + }, + "schema": 1670333809819, + "cookies": { + "optIn": [ + { + "name": "ck", + "value": "4" + } + ], + "optOut": [ + { + "name": "ck", + "value": "0" + } + ] + }, + "domains": [ + "ilovepdf.com" + ], + "id": "137eec59-6b48-413c-bb45-33420aabdc87", + "last_modified": 1670498157785 + }, + { + "click": { + "optIn": "a#CybotCookiebotDialogBodyButtonAccept", + "presence": "div#CybotCookiebotDialog" + }, + "schema": 1670333809819, + "cookies": {}, + "domains": [ + "themeforest.net", + "cpanel.net", + "envato.com" + ], + "id": "f9b5b6d8-50be-486e-bc59-a45733be492c", + "last_modified": 1670498157781 + }, + { + "click": { + "optIn": "button.welcome__button--accept", + "optOut": "button.welcome__button--decline", + "presence": "div.welcome__cookie-notice" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "wetransfer.com" + ], + "id": "8b43c95f-2617-4541-a88d-ebae3b819bee", + "last_modified": 1670498157774 + }, + { + "click": { + "optIn": "button.css-quk35p", + "presence": "div.css-103nllw" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "healthline.com" + ], + "id": "b62e5259-f7f5-4319-8175-e39266b8a031", + "last_modified": 1670498157766 + }, + { + "click": { + "optIn": "a.close", + "presence": "div.ReadPolicy" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "huawei.com" + ], + "id": "645dca4f-49b6-456e-8aa1-40dfdb35acb7", + "last_modified": 1670498157750 + }, + { + "click": { + "optIn": "button#_evidon-accept-button", + "optOut": "button#_evidon-decline-button", + "presence": "div#_evidon_banner" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "pubmatic.com" + ], + "id": "35142692-9d67-4f16-9512-36673e35c43d", + "last_modified": 1670498157747 + }, + { + "click": { + "optIn": "button.CookieBanner_button__P6xCl", + "presence": "section.CookieBanner_notificationBar__nn49h" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "kaspersky.com" + ], + "id": "1e504f8f-4377-4de7-90e4-cbbfaa30f54d", + "last_modified": 1670498157743 + }, + { + "click": { + "optIn": "a#tbp-consent-confirm", + "presence": "div.tbp-container" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "taboola.com" + ], + "id": "cade9499-6c59-4939-90e7-e5e029ce0446", + "last_modified": 1670498157735 + }, + { + "click": { + "optIn": "a.cookiepolicycontinue", + "presence": "div#cookieMessage_content" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "oup.com" + ], + "id": "2e0b3da1-4072-4e1b-b561-939789c90f4d", + "last_modified": 1670498157727 + }, + { + "click": { + "optIn": "button#cx_button_close", + "presence": "div#cx_bottom_banner" + }, + "schema": 1670460523671, + "cookies": { + "optIn": [ + { + "name": "show_gdpr_consent_messaging", + "value": "1" + } + ] + }, + "domains": [ + "nbcnews.com" + ], + "id": "fecfb874-fff9-4d91-af27-885729a4556d", + "last_modified": 1670498157720 + }, + { + "click": { + "optIn": "button.cta--is-1", + "presence": "div.cookie-banner__cta" + }, + "schema": 1670460523671, + "cookies": { + "optIn": [ + { + "name": "and_cba_EN_US", + "value": "true" + } + ] + }, + "domains": [ + "android.com" + ], + "id": "35a8e0de-9308-4dfe-abc9-764a9e3833ec", + "last_modified": 1670498157716 + }, + { + "click": { + "optIn": "a.cc-dismiss", + "presence": "div.cc-window" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "dnsmadeeasy.com" + ], + "id": "581b7d96-d1a4-43a8-9498-5650f9234e10", + "last_modified": 1670498157708 + }, + { + "click": { + "optIn": "button#gdpr-modal-agree", + "presence": "div.modal-window" + }, + "schema": 1670460523671, + "cookies": { + "optIn": [ + { + "name": "gdpr_agreed", + "value": "4" + } + ] + }, + "domains": [ + "usnews.com" + ], + "id": "d9ebe530-4e45-4315-b628-c2d8c81c48a5", + "last_modified": 1670498157693 + }, + { + "click": { + "optIn": "span.js-dismiss-cookie-banner", + "presence": "div.DesignSystem" + }, + "schema": 1670460523671, + "cookies": {}, + "domains": [ + "academia.edu" + ], + "id": "41001e78-302d-44da-bd82-a690d1968a9f", + "last_modified": 1670498157689 + }, + { + "click": { + "optIn": "button.cu-privacy-notice-close", + "presence": "div#cu-privacy-notice" + }, + "schema": 1670460524526, + "cookies": { + "optIn": [ + { + "name": "cuPivacyNotice", + "value": "1" + } + ] + }, + "domains": [ + "columbia.edu" + ], + "id": "2952e826-d897-431b-866d-f8ed95f4db64", + "last_modified": 1670498157658 + }, + { + "click": { + "optIn": "button.consent-btn", + "presence": "div.cookie-consent" + }, + "schema": 1670460524526, + "cookies": {}, + "domains": [ + "geeksforgeeks.org" + ], + "id": "f3d0e044-1dc7-4d6e-9477-2724c5115ba4", + "last_modified": 1670498157647 + }, + { + "click": { + "optIn": "button.accept-consent", + "presence": "section#cookieconsentpopup" + }, + "schema": 1670460524526, + "cookies": { + "optIn": [ + { + "name": "consent_cookie", + "value": "1" + } + ] + }, + "domains": [ + "worldbank.org" + ], + "id": "efe049ac-d21c-4fa6-b559-5c47b5ce307b", + "last_modified": 1670498157643 + }, + { + "click": { + "optIn": "button#close_feedback_btn", + "presence": "div#feedback-bar" + }, + "schema": 1670460524526, + "cookies": {}, + "domains": [ + "cricbuzz.com" + ], + "id": "22187533-0a56-4679-9244-f11b2c58083a", + "last_modified": 1670498157631 + }, + { + "click": { + "optIn": "button.color-secondary", + "presence": "div.footer_footer__ZK5Q_" + }, + "schema": 1670460524526, + "cookies": { + "optIn": [ + { + "name": "__cookie__agree", + "value": "Y" + } + ] + }, + "domains": [ + "sberdevices.ru" + ], + "id": "c8c58b5e-45d1-4442-ad53-b89f3603586f", + "last_modified": 1670498157620 + }, + { + "click": { + "optIn": "button.cookie-accept-btn", + "presence": "div.cookie-consent-area" + }, + "schema": 1670460524526, + "cookies": {}, + "domains": [ + "typeform.com" + ], + "id": "c0bef9bb-67e2-4038-bfe9-795085f37719", + "last_modified": 1670498157616 + }, + { + "click": { + "optIn": "button.js-opt-in-consent-button", + "presence": "div.opt-in-modal__inner" + }, + "schema": 1670460524526, + "cookies": {}, + "domains": [ + "wpengine.com" + ], + "id": "f89481a5-e087-4210-aeee-36bba9764a87", + "last_modified": 1670498157612 + }, + { + "click": { + "optIn": "button#cookie-policy-button-accept", + "presence": "div#modal" + }, + "schema": 1670460524526, + "cookies": { + "optIn": [ + { + "name": "_cookies_accepted", + "value": "all" + } + ], + "optOut": [ + { + "name": "_cookies_accepted", + "value": "essential" + } + ] + }, + "domains": [ + "ubuntu.com" + ], + "id": "b11001c6-7a9c-40a7-8595-3c8a9c1e7416", + "last_modified": 1670498157608 + }, + { + "click": { + "optIn": "button#privacy-consent-button", + "presence": "div#privacy-consent" + }, + "schema": 1670460524526, + "cookies": {}, + "domains": [ + "vox.com" + ], + "id": "e552ade4-c254-4e8b-823f-8307e4b21fb3", + "last_modified": 1670498157597 + }, + { + "click": { + "optIn": "button#ccc-notify-accept", + "optOut": "button#ccc-notify-dismiss", + "presence": "div#ccc" + }, + "schema": 1670460524526, + "cookies": { + "optIn": [ + { + "name": "_hjFirstSeen", + "value": "1" + } + ] + }, + "domains": [ + "ox.ac.uk" + ], + "id": "982a2a33-f052-4acd-936e-d3b0f7bf7596", + "last_modified": 1670498157593 + }, + { + "click": { + "optIn": "a.c-button", + "presence": "div#cookiebanner" + }, + "schema": 1670460524526, + "cookies": {}, + "domains": [ + "psychologytoday.com" + ], + "id": "fcd83d7a-0cbc-4cc4-ac1f-609a303ea5a3", + "last_modified": 1670498157589 + }, + { + "click": { + "optIn": "button.consent--accept", + "presence": "div.consent-banner-buttons" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "ring.com" + ], + "id": "081717d3-32c8-40e4-8bc1-ddd5495dbddf", + "last_modified": 1670498157581 + }, + { + "click": { + "optIn": "a.accept-cookies", + "presence": "div.cookie-notice" + }, + "schema": 1670460525411, + "cookies": { + "optIn": [ + { + "name": "_hjFirstSeen", + "value": "1" + } + ] + }, + "domains": [ + "evernote.com" + ], + "id": "a78db283-f6a6-4e80-83d1-65becec1df21", + "last_modified": 1670498157574 + }, + { + "click": { + "optIn": "span.cookiePolicy-close", + "presence": "div.cookiePolicy" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "gearbest.com" + ], + "id": "0100a0ae-2e3a-4dcd-8761-596193f6be8d", + "last_modified": 1670498157570 + }, + { + "click": { + "optIn": "span.b-policy-info__cross", + "presence": "div.b-policy-info" + }, + "schema": 1670460525411, + "cookies": { + "optIn": [ + { + "name": "cookieAgree", + "value": "1" + } + ] + }, + "domains": [ + "reg.ru" + ], + "id": "783a1b6f-616d-4f9d-a84b-b0f7954e3e24", + "last_modified": 1670498157563 + }, + { + "click": { + "optIn": "button#cookie-policy-btn", + "presence": "div.cookie" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "ucla.edu" + ], + "id": "955c6f08-e07d-4b05-be04-5de69f49753b", + "last_modified": 1670498157547 + }, + { + "click": { + "optIn": "button#_evidon-banner-acceptbutton", + "presence": "div#_evidon-banner-content" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "lenovo.com" + ], + "id": "21e686fb-7250-4fbc-ab5c-6a6bf67775d3", + "last_modified": 1670498157540 + }, + { + "click": { + "optIn": "a.button--default", + "presence": "div#cookie-notification" + }, + "schema": 1670460525411, + "cookies": { + "optIn": [ + { + "name": "mdpi_cookies_accepted", + "value": "1" + } + ] + }, + "domains": [ + "mdpi.com" + ], + "id": "4f9c6403-4953-48aa-a8d7-d7acf177d437", + "last_modified": 1670498157536 + }, + { + "click": { + "optIn": "button#_evidon-decline-button", + "presence": "div#_evidon-message" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "crunchyroll.com" + ], + "id": "85aec7ab-1cc2-4651-b3fc-0cbae9a241f2", + "last_modified": 1670498157525 + }, + { + "click": { + "optIn": "button.css-ZtxlD", + "optOut": "a.css-jMqFMB", + "presence": "div.css-fPpfoE" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "uber.com" + ], + "id": "663e3527-0018-4851-912d-c75a9d610881", + "last_modified": 1670498157517 + }, + { + "click": { + "optIn": "button#ccc-notify-accept", + "optOut": "button#ccc-notify-reject", + "presence": "div#ccc" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "tapad.com" + ], + "id": "026010ef-311b-4c3e-aa1d-cb1470591c6a", + "last_modified": 1670498157514 + }, + { + "click": { + "optIn": "button#unic-agree", + "presence": "div.unic-bar" + }, + "schema": 1670460525411, + "cookies": {}, + "domains": [ + "sharethrough.com" + ], + "id": "d448486a-ad5c-45dd-b661-c22a82869036", + "last_modified": 1670498157502 + }, + { + "click": { + "optIn": "span#cookie-banner-close", + "presence": "div#cookie-banner" + }, + "schema": 1670460526327, + "cookies": { + "optIn": [ + { + "name": "hideCookieBanner", + "value": "true" + } + ] + }, + "domains": [ + "substack.com" + ], + "id": "09f72241-e3be-4156-a8ac-ecf47145854f", + "last_modified": 1670498157487 + }, + { + "click": { + "optIn": "a#et_cookie_consent_allow", + "presence": "div.et_cookie_consent" + }, + "schema": 1670460526327, + "cookies": { + "optIn": [ + { + "name": "et_cookies", + "value": "on" + } + ], + "optOut": [ + { + "name": "et_cookies", + "value": "off" + } + ] + }, + "domains": [ + "elegantthemes.com" + ], + "id": "ae4a00ba-e49c-4376-a5a7-67d9b647342d", + "last_modified": 1670498157484 + }, + { + "click": { + "optIn": "button.cookie-consent-banner-opt-out__action", + "presence": "div.cookie-consent-banner-opt-out" + }, + "schema": 1670460526327, + "cookies": {}, + "domains": [ + "mercadolibre.com.ar" + ], + "id": "b6fda273-20bf-4e49-8a2b-fe2afdcc12da", + "last_modified": 1670498157472 + }, + { + "click": { + "optIn": "button.closeButton", + "presence": "div.cookieBanner" + }, + "schema": 1670460526327, + "cookies": {}, + "domains": [ + "anchor.fm" + ], + "id": "475b40e3-18d5-457e-a084-76faa301c753", + "last_modified": 1670498157450 + }, + { + "click": { + "optIn": "button.button-accept-cookie", + "presence": "div.banner-overlay" + }, + "schema": 1670460526327, + "cookies": { + "optIn": [ + { + "name": "analytics_accepted", + "value": "true" + }, + { + "name": "cookie_accepted", + "value": "true" + } + ], + "optOut": [ + { + "name": "cookies_denied", + "value": "true" + } + ] + }, + "domains": [ + "aboutcookies.org" + ], + "id": "2de3ce99-ba76-417e-851c-87e0c4035180", + "last_modified": 1670498157446 + }, + { + "click": { + "optIn": "a.js-gdpr-cookie-acceptLink", + "presence": "div.widget-GdprCookieBanner" + }, + "schema": 1670460526327, + "cookies": { + "optIn": [ + { + "name": "CookieBanner_Closed", + "value": "true" + } + ] + }, + "domains": [ + "jamanetwork.com" + ], + "id": "6026221f-dd61-493e-aea8-cdf38198f401", + "last_modified": 1670498157442 + }, + { + "click": { + "optIn": "button#nhsuk-cookie-banner__link_accept_analytics", + "optOut": "button#nhsuk-cookie-banner__link_accept", + "presence": "div#nhsuk-cookie-banner" + }, + "schema": 1670460526327, + "cookies": {}, + "domains": [ + "www.nhs.uk" + ], + "id": "e6444889-fd2f-412a-b25d-41a6b6daaf3a", + "last_modified": 1670498157435 + }, + { + "click": { + "optIn": "button.button_b1lbbqqt", + "presence": "div.modalStyle_modrqv7" + }, + "schema": 1670460526327, + "cookies": {}, + "domains": [ + "nikkei.com" + ], + "id": "546792b9-f201-4e94-97f6-d5680abfe529", + "last_modified": 1670498157431 + }, + { + "click": { + "optIn": "button.fc-primary-button", + "presence": "div.fc-consent-root" + }, + "schema": 1670460526327, + "cookies": {}, + "domains": [ + "17track.net" + ], + "id": "a620f10c-033a-47c1-ad2b-a8fd1df9e504", + "last_modified": 1670498157427 + }, + { + "click": { + "optIn": "a.cc-dismiss", + "presence": "div.cc-window" + }, + "schema": 1670460526327, + "cookies": { + "optIn": [ + { + "name": "cookieconsent_status", + "value": "dismiss" + } + ] + }, + "domains": [ + "duke.edu" + ], + "id": "d614e3e1-1282-4c56-801d-525263028933", + "last_modified": 1670498157419 + }, + { + "click": { + "optIn": "a#cookie_action_close_header", + "presence": "div#cookie-law-info-bar" + }, + "schema": 1670460526327, + "cookies": { + "optIn": [ + { + "name": "viewed_cookie_policy", + "value": "yes" + } + ] + }, + "domains": [ + "usc.edu" + ], + "id": "5cedc12d-d79c-4be8-9e45-e98905147090", + "last_modified": 1670498157408 + }, + { + "click": { + "optIn": "button.btn-accept", + "presence": "div#gdpr-new-container" + }, + "schema": 1670460526327, + "cookies": {}, + "domains": [ + "aliexpress.com" + ], + "id": "80851a39-2183-49e4-99f4-16d6189bff1e", + "last_modified": 1670498157404 + }, + { + "click": { + "optOut": "button.cookieBarButtons", + "presence": "div#cookieBar" + }, + "schema": 1670460527205, + "cookies": {}, + "domains": [ + "google-analytics.com" + ], + "id": "286028cd-7cb7-4c71-91a7-ae1a23df8a31", + "last_modified": 1670498157378 + }, + { + "click": { + "optIn": "div.global-alert-banner button[action-type=ACCEPT]", + "optOut": "div.global-alert-banner button[action-type=DENY]", + "presence": "div.global-alert-banner" + }, + "schema": 1670460527205, + "cookies": {}, + "domains": [ + "linkedin.com" + ], + "id": "f1ddc6f6-8927-4205-8003-1f6e84ae22b0", + "last_modified": 1670498157329 + }, + { + "click": { + "optIn": "button#gdpr-banner-accept", + "optOut": "button#gdpr-banner-decline", + "presence": "div#gdpr-banner" + }, + "schema": 1670460527205, + "cookies": {}, + "domains": [ + "ebay.com", + "ebay.de", + "ebay.co.uk" + ], + "id": "1e6d35e7-b907-4f5c-a09a-9f3336ef6e61", + "last_modified": 1670498157322 + }, + { + "click": { + "optIn": "button[data-a-target=consent-banner-accept]", + "presence": "div.consent-banner" + }, + "schema": 1670460527205, + "cookies": {}, + "domains": [ + "twitch.tv" + ], + "id": "542aafba-9f44-4321-88e5-ece333073360", + "last_modified": 1670498157315 + }, + { + "click": { + "optIn": "div.gdpr div > button:not([aria-label]", + "optOut": "div.gdpr div > button + button:not([aria-label])", + "presence": "div.gdpr" + }, + "schema": 1670460527205, + "cookies": { + "optOut": [ + { + "name": "purr-pref-agent", + "value": "