diff options
Diffstat (limited to 'toolkit')
419 files changed, 1203 insertions, 74428 deletions
diff --git a/toolkit/components/alerts/nsXULAlerts.cpp b/toolkit/components/alerts/nsXULAlerts.cpp index 882617637..d353d8714 100644 --- a/toolkit/components/alerts/nsXULAlerts.cpp +++ b/toolkit/components/alerts/nsXULAlerts.cpp @@ -18,7 +18,6 @@ #include "nsIWindowWatcher.h" using namespace mozilla; -using mozilla::dom::NotificationTelemetryService; #define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xul" @@ -185,20 +184,6 @@ nsXULAlerts::ShowAlertWithIconURI(nsIAlertNotification* aAlert, NS_ENSURE_SUCCESS(rv, rv); if (mDoNotDisturb) { - if (!inPrivateBrowsing) { - RefPtr<NotificationTelemetryService> telemetry = - NotificationTelemetryService::GetInstance(); - if (telemetry) { - // Record the number of unique senders for XUL alerts. The OS X and - // libnotify backends will fire `alertshow` even if "do not disturb" - // is enabled. In that case, `NotificationObserver` will record the - // sender. - nsCOMPtr<nsIPrincipal> principal; - if (NS_SUCCEEDED(aAlert->GetPrincipal(getter_AddRefs(principal)))) { - Unused << NS_WARN_IF(NS_FAILED(telemetry->RecordSender(principal))); - } - } - } if (aAlertListener) aAlertListener->Observe(nullptr, "alertfinished", cookie.get()); return NS_OK; diff --git a/toolkit/components/blocklist/blocklist.manifest b/toolkit/components/blocklist/blocklist.manifest index c770b4e7d..6b63d6072 100644 --- a/toolkit/components/blocklist/blocklist.manifest +++ b/toolkit/components/blocklist/blocklist.manifest @@ -1,7 +1,5 @@ component {66354bc9-7ed1-4692-ae1d-8da97d6b205e} nsBlocklistService.js process=main contract @mozilla.org/extensions/blocklist;1 {66354bc9-7ed1-4692-ae1d-8da97d6b205e} process=main category profile-after-change nsBlocklistService @mozilla.org/extensions/blocklist;1 process=main -component {e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d} nsBlocklistServiceContent.js process=content -contract @mozilla.org/extensions/blocklist;1 {e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d} process=content category update-timer nsBlocklistService @mozilla.org/extensions/blocklist;1,getService,blocklist-background-update-timer,extensions.blocklist.interval,86400
\ No newline at end of file diff --git a/toolkit/components/blocklist/moz.build b/toolkit/components/blocklist/moz.build index 3dc3be5ba..0016e74c2 100644 --- a/toolkit/components/blocklist/moz.build +++ b/toolkit/components/blocklist/moz.build @@ -6,7 +6,6 @@ EXTRA_COMPONENTS += [ 'blocklist.manifest', - 'nsBlocklistServiceContent.js', ] EXTRA_PP_COMPONENTS += [ diff --git a/toolkit/components/blocklist/nsBlocklistService.js b/toolkit/components/blocklist/nsBlocklistService.js index 891346b72..1cd2ed806 100644 --- a/toolkit/components/blocklist/nsBlocklistService.js +++ b/toolkit/components/blocklist/nsBlocklistService.js @@ -24,13 +24,8 @@ try { XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); -#ifdef MOZ_WEBEXTENSIONS -XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", - "resource://gre/modules/UpdateUtils.jsm"); -#else XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm"); -#endif XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest", @@ -571,11 +566,7 @@ Blocklist.prototype = { dsURI = dsURI.replace(/%BUILD_TARGET%/g, gApp.OS + "_" + gABI); dsURI = dsURI.replace(/%OS_VERSION%/g, gOSVersion); dsURI = dsURI.replace(/%LOCALE%/g, getLocale()); -#ifdef MOZ_WEBEXTENSIONS - dsURI = dsURI.replace(/%CHANNEL%/g, UpdateUtils.UpdateChannel); -#else dsURI = dsURI.replace(/%CHANNEL%/g, UpdateChannel.get()); -#endif dsURI = dsURI.replace(/%PLATFORM_VERSION%/g, gApp.platformVersion); dsURI = dsURI.replace(/%DISTRIBUTION%/g, getDistributionPrefValue(PREF_APP_DISTRIBUTION)); diff --git a/toolkit/components/blocklist/nsBlocklistServiceContent.js b/toolkit/components/blocklist/nsBlocklistServiceContent.js deleted file mode 100644 index 1752924b5..000000000 --- a/toolkit/components/blocklist/nsBlocklistServiceContent.js +++ /dev/null @@ -1,113 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; - -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://gre/modules/Services.jsm"); - -const kMissingAPIMessage = "Unsupported blocklist call in the child process." - -/* - * A lightweight blocklist proxy for the content process that traps plugin - * related blocklist checks and forwards them to the parent. This interface is - * primarily designed to insure overlays work.. it does not control plugin - * or addon loading. - */ - -function Blocklist() { - this.init(); -} - -Blocklist.prototype = { - classID: Components.ID("{e0a106ed-6ad4-47a4-b6af-2f1c8aa4712d}"), - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsIBlocklistService]), - - init: function() { - Services.cpmm.addMessageListener("Blocklist:blocklistInvalidated", this); - Services.obs.addObserver(this, "xpcom-shutdown", false); - }, - - uninit: function() { - Services.cpmm.removeMessageListener("Blocklist:blocklistInvalidated", this); - Services.obs.removeObserver(this, "xpcom-shutdown", false); - }, - - observe: function(aSubject, aTopic, aData) { - switch (aTopic) { - case "xpcom-shutdown": - this.uninit(); - break; - } - }, - - // Message manager message handlers - receiveMessage: function(aMsg) { - switch (aMsg.name) { - case "Blocklist:blocklistInvalidated": - Services.obs.notifyObservers(null, "blocklist-updated", null); - Services.cpmm.sendAsyncMessage("Blocklist:content-blocklist-updated"); - break; - default: - throw new Error("Unknown blocklist message received from content: " + aMsg.name); - } - }, - - /* - * A helper that queries key data from a plugin or addon object - * and generates a simple data wrapper suitable for ipc. We hand - * these directly to the nsBlockListService in the parent which - * doesn't query for much.. allowing us to get away with this. - */ - flattenObject: function(aTag) { - // Based on debugging the nsBlocklistService, these are the props the - // parent side will check on our objects. - let props = ["name", "description", "filename", "version"]; - let dataWrapper = {}; - for (let prop of props) { - dataWrapper[prop] = aTag[prop]; - } - return dataWrapper; - }, - - // We support the addon methods here for completeness, but content currently - // only calls getPluginBlocklistState. - - isAddonBlocklisted: function(aAddon, aAppVersion, aToolkitVersion) { - return true; - }, - - getAddonBlocklistState: function(aAddon, aAppVersion, aToolkitVersion) { - return Components.interfaces.nsIBlocklistService.STATE_BLOCKED; - }, - - // There are a few callers in layout that rely on this. - getPluginBlocklistState: function(aPluginTag, aAppVersion, aToolkitVersion) { - return Services.cpmm.sendSyncMessage("Blocklist:getPluginBlocklistState", { - addonData: this.flattenObject(aPluginTag), - appVersion: aAppVersion, - toolkitVersion: aToolkitVersion - })[0]; - }, - - getAddonBlocklistURL: function(aAddon, aAppVersion, aToolkitVersion) { - throw new Error(kMissingAPIMessage); - }, - - getPluginBlocklistURL: function(aPluginTag) { - throw new Error(kMissingAPIMessage); - }, - - getPluginInfoURL: function(aPluginTag) { - throw new Error(kMissingAPIMessage); - } -}; - -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Blocklist]); diff --git a/toolkit/components/build/nsToolkitCompsModule.cpp b/toolkit/components/build/nsToolkitCompsModule.cpp index 33c604c4e..a4293c6f9 100644 --- a/toolkit/components/build/nsToolkitCompsModule.cpp +++ b/toolkit/components/build/nsToolkitCompsModule.cpp @@ -39,9 +39,6 @@ #include "nsBrowserStatusFilter.h" #include "mozilla/FinalizationWitnessService.h" #include "mozilla/NativeOSFileInternals.h" -#ifdef MOZ_WEBEXTENSIONS -#include "mozilla/AddonContentPolicy.h" -#endif #include "mozilla/AddonPathService.h" #if defined(XP_WIN) @@ -132,9 +129,6 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(FinalizationWitnessService, Init) NS_GENERIC_FACTORY_CONSTRUCTOR(NativeOSFileInternalsService) NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init) -#ifdef MOZ_WEBEXTENSIONS -NS_GENERIC_FACTORY_CONSTRUCTOR(AddonContentPolicy) -#endif NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance) NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID); @@ -171,9 +165,6 @@ NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID); #endif NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID); NS_DEFINE_NAMED_CID(NATIVE_OSFILE_INTERNALS_SERVICE_CID); -#ifdef MOZ_WEBEXTENSIONS -NS_DEFINE_NAMED_CID(NS_ADDONCONTENTPOLICY_CID); -#endif NS_DEFINE_NAMED_CID(NS_ADDON_PATH_SERVICE_CID); NS_DEFINE_NAMED_CID(NATIVE_FILEWATCHER_SERVICE_CID); @@ -211,9 +202,6 @@ static const Module::CIDEntry kToolkitCIDs[] = { #endif { &kFINALIZATIONWITNESSSERVICE_CID, false, nullptr, FinalizationWitnessServiceConstructor }, { &kNATIVE_OSFILE_INTERNALS_SERVICE_CID, false, nullptr, NativeOSFileInternalsServiceConstructor }, -#ifdef MOZ_WEBEXTENSIONS - { &kNS_ADDONCONTENTPOLICY_CID, false, nullptr, AddonContentPolicyConstructor }, -#endif { &kNS_ADDON_PATH_SERVICE_CID, false, nullptr, AddonPathServiceConstructor }, { &kNATIVE_FILEWATCHER_SERVICE_CID, false, nullptr, NativeFileWatcherServiceConstructor }, { nullptr } @@ -253,18 +241,12 @@ static const Module::ContractIDEntry kToolkitContracts[] = { #endif { FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID }, { NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID, &kNATIVE_OSFILE_INTERNALS_SERVICE_CID }, -#ifdef MOZ_WEBEXTENSIONS - { NS_ADDONCONTENTPOLICY_CONTRACTID, &kNS_ADDONCONTENTPOLICY_CID }, -#endif { NS_ADDONPATHSERVICE_CONTRACTID, &kNS_ADDON_PATH_SERVICE_CID }, { NATIVE_FILEWATCHER_SERVICE_CONTRACTID, &kNATIVE_FILEWATCHER_SERVICE_CID }, { nullptr } }; static const mozilla::Module::CategoryEntry kToolkitCategories[] = { -#ifdef MOZ_WEBEXTENSIONS - { "content-policy", NS_ADDONCONTENTPOLICY_CONTRACTID, NS_ADDONCONTENTPOLICY_CONTRACTID }, -#endif { nullptr } }; diff --git a/toolkit/components/downloads/ApplicationReputation.cpp b/toolkit/components/downloads/ApplicationReputation.cpp index a369ca884..9fdc8a103 100644 --- a/toolkit/components/downloads/ApplicationReputation.cpp +++ b/toolkit/components/downloads/ApplicationReputation.cpp @@ -117,14 +117,6 @@ private: friend class PendingDBLookup; - // Telemetry states. - // Status of the remote response (valid or not). - enum SERVER_RESPONSE_TYPES { - SERVER_RESPONSE_VALID = 0, - SERVER_RESPONSE_FAILED = 1, - SERVER_RESPONSE_INVALID = 2, - }; - // Number of blocklist and allowlist hits we have seen. uint32_t mBlocklistCount; uint32_t mAllowlistCount; diff --git a/toolkit/components/downloads/moz.build b/toolkit/components/downloads/moz.build index 20394a70d..e611f4010 100644 --- a/toolkit/components/downloads/moz.build +++ b/toolkit/components/downloads/moz.build @@ -53,9 +53,10 @@ if CONFIG['OS_ARCH'] == 'WINNT': 'nsDownloadScanner.cpp', ] -# XXX - Until Suite builds off XULRunner we can't guarantee our implementation -# of nsIDownloadManagerUI overrides toolkit's. -if not CONFIG['MOZ_SUITE']: +# The Communicator Downloads Manager uses its own DownloadManagerUI +# component and it can't be guaranteed that its implimentation will override +# toolkit's so don't include toolkit's +if not CONFIG['MOZ_SUITE'] and not CONFIG['BINOC_COMM_DLMGR']: EXTRA_COMPONENTS += [ 'nsDownloadManagerUI.js', 'nsDownloadManagerUI.manifest', diff --git a/toolkit/components/maintenanceservice/Makefile.in b/toolkit/components/maintenanceservice/Makefile.in deleted file mode 100644 index b07afbb0a..000000000 --- a/toolkit/components/maintenanceservice/Makefile.in +++ /dev/null @@ -1,13 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -ifndef MOZ_WINCONSOLE -ifdef MOZ_DEBUG -MOZ_WINCONSOLE = 1 -else -MOZ_WINCONSOLE = 0 -endif -endif - -include $(topsrcdir)/config/rules.mk diff --git a/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi deleted file mode 100644 index d4d21e377..000000000 --- a/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi +++ /dev/null @@ -1,275 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -; Set verbosity to 3 (e.g. no script) to lessen the noise in the build logs -!verbose 3 - -; 7-Zip provides better compression than the lzma from NSIS so we add the files -; uncompressed and use 7-Zip to create a SFX archive of it -SetDatablockOptimize on -SetCompress off -CRCCheck on - -RequestExecutionLevel admin - -; The commands inside this ifdef require NSIS 3.0a2 or greater so the ifdef can -; be removed after we require NSIS 3.0a2 or greater. -!ifdef NSIS_PACKEDVERSION - Unicode true - ManifestSupportedOS all - ManifestDPIAware true -!endif - -!addplugindir ./ - -; Variables -Var TempMaintServiceName -Var BrandFullNameDA -Var BrandFullName - -; Other included files may depend upon these includes! -; The following includes are provided by NSIS. -!include FileFunc.nsh -!include LogicLib.nsh -!include MUI.nsh -!include WinMessages.nsh -!include WinVer.nsh -!include WordFunc.nsh - -!insertmacro GetOptions -!insertmacro GetParameters -!insertmacro GetSize - -; The test slaves use this fallback key to run tests. -; And anyone that wants to run tests themselves should already have -; this installed. -!define FallbackKey \ - "SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4" - -!define CompanyName "Mozilla Corporation" -!define BrandFullNameInternal "" - -; The following includes are custom. -!include defines.nsi -; We keep defines.nsi defined so that we get other things like -; the version number, but we redefine BrandFullName -!define MaintFullName "Mozilla Maintenance Service" -!undef BrandFullName -!define BrandFullName "${MaintFullName}" - -!include common.nsh -!include locales.nsi - -VIAddVersionKey "FileDescription" "${MaintFullName} Installer" -VIAddVersionKey "OriginalFilename" "maintenanceservice_installer.exe" - -Name "${MaintFullName}" -OutFile "maintenanceservice_installer.exe" - -; Get installation folder from registry if available -InstallDirRegKey HKLM "Software\Mozilla\MaintenanceService" "" - -SetOverwrite on - -!define MaintUninstallKey \ - "Software\Microsoft\Windows\CurrentVersion\Uninstall\MozillaMaintenanceService" - -; Always install into the 32-bit location even if we have a 64-bit build. -; This is because we use only 1 service for all Firefox channels. -; Allow either x86 and x64 builds to exist at this location, depending on -; what is the latest build. -InstallDir "$PROGRAMFILES32\${MaintFullName}\" -ShowUnInstDetails nevershow - -################################################################################ -# Modern User Interface - MUI - -!define MUI_ICON setup.ico -!define MUI_UNICON setup.ico -!define MUI_WELCOMEPAGE_TITLE_3LINES -!define MUI_UNWELCOMEFINISHPAGE_BITMAP wizWatermark.bmp - -;Interface Settings -!define MUI_ABORTWARNING - -; Uninstaller Pages -!insertmacro MUI_UNPAGE_CONFIRM -!insertmacro MUI_UNPAGE_INSTFILES - -################################################################################ -# Language - -!insertmacro MOZ_MUI_LANGUAGE 'baseLocale' -!verbose push -!verbose 3 -!include "overrideLocale.nsh" -!include "customLocale.nsh" -!verbose pop - -; Set this after the locale files to override it if it is in the locale -; using " " for BrandingText will hide the "Nullsoft Install System..." branding -BrandingText " " - -Function .onInit - ; Remove the current exe directory from the search order. - ; This only effects LoadLibrary calls and not implicitly loaded DLLs. - System::Call 'kernel32::SetDllDirectoryW(w "")' - - SetSilent silent - ${Unless} ${AtLeastWin7} - Abort - ${EndUnless} -FunctionEnd - -Function un.onInit - ; Remove the current exe directory from the search order. - ; This only effects LoadLibrary calls and not implicitly loaded DLLs. - System::Call 'kernel32::SetDllDirectoryW(w "")' - -; The commands inside this ifndef are needed prior to NSIS 3.0a2 and can be -; removed after we require NSIS 3.0a2 or greater. -!ifndef NSIS_PACKEDVERSION - ${If} ${AtLeastWinVista} - System::Call 'user32::SetProcessDPIAware()' - ${EndIf} -!endif - - StrCpy $BrandFullNameDA "${MaintFullName}" - StrCpy $BrandFullName "${MaintFullName}" -FunctionEnd - -Section "MaintenanceService" - AllowSkipFiles off - - CreateDirectory $INSTDIR - SetOutPath $INSTDIR - - ; If the service already exists, then it will be stopped when upgrading it - ; via the maintenanceservice_tmp.exe command executed below. - ; The maintenanceservice_tmp.exe command will rename the file to - ; maintenanceservice.exe if maintenanceservice_tmp.exe is newer. - ; If the service does not exist yet, we install it and drop the file on - ; disk as maintenanceservice.exe directly. - StrCpy $TempMaintServiceName "maintenanceservice.exe" - IfFileExists "$INSTDIR\maintenanceservice.exe" 0 skipAlreadyExists - StrCpy $TempMaintServiceName "maintenanceservice_tmp.exe" - skipAlreadyExists: - - ; We always write out a copy and then decide whether to install it or - ; not via calling its 'install' cmdline which works by version comparison. - CopyFiles "$EXEDIR\maintenanceservice.exe" "$INSTDIR\$TempMaintServiceName" - - ; The updater.ini file is only used when performing an install or upgrade, - ; and only if that install or upgrade is successful. If an old updater.ini - ; happened to be copied into the maintenance service installation directory - ; but the service was not newer, the updater.ini file would be unused. - ; It is used to fill the description of the service on success. - CopyFiles "$EXEDIR\updater.ini" "$INSTDIR\updater.ini" - - ; Install the application maintenance service. - ; If a service already exists, the command line parameter will stop the - ; service and only install itself if it is newer than the already installed - ; service. If successful it will remove the old maintenanceservice.exe - ; and replace it with maintenanceservice_tmp.exe. - ClearErrors - ;${GetParameters} $0 - ;${GetOptions} "$0" "/Upgrade" $0 - ;${If} ${Errors} - ExecWait '"$INSTDIR\$TempMaintServiceName" forceinstall' - ;${Else} - ; The upgrade cmdline is the same as install except - ; It will fail if the service isn't already installed. - ; ExecWait '"$INSTDIR\$TempMaintServiceName" upgrade' - ;${EndIf} - - WriteUninstaller "$INSTDIR\Uninstall.exe" - WriteRegStr HKLM "${MaintUninstallKey}" "DisplayName" "${MaintFullName}" - WriteRegStr HKLM "${MaintUninstallKey}" "UninstallString" \ - '"$INSTDIR\uninstall.exe"' - WriteRegStr HKLM "${MaintUninstallKey}" "DisplayIcon" \ - "$INSTDIR\Uninstall.exe,0" - WriteRegStr HKLM "${MaintUninstallKey}" "DisplayVersion" "${AppVersion}" - WriteRegStr HKLM "${MaintUninstallKey}" "Publisher" "Mozilla" - WriteRegStr HKLM "${MaintUninstallKey}" "Comments" "${BrandFullName}" - WriteRegDWORD HKLM "${MaintUninstallKey}" "NoModify" 1 - ${GetSize} "$INSTDIR" "/S=0K" $R2 $R3 $R4 - WriteRegDWORD HKLM "${MaintUninstallKey}" "EstimatedSize" $R2 - - ; Write out that a maintenance service was attempted. - ; We do this because on upgrades we will check this value and we only - ; want to install once on the first upgrade to maintenance service. - ; Also write out that we are currently installed, preferences will check - ; this value to determine if we should show the service update pref. - ; Since the Maintenance service can be installed either x86 or x64, - ; always use the 64-bit registry for checking if an attempt was made. - ${If} ${RunningX64} - SetRegView 64 - ${EndIf} - WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Attempted" 1 - WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Installed" 1 - DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled" - - ; Included here for debug purposes only. - ; These keys are used to bypass the installation dir is a valid installation - ; check from the service so that tests can be run. - WriteRegStr HKLM "${FallbackKey}\0" "name" "Mozilla Corporation" - WriteRegStr HKLM "${FallbackKey}\0" "issuer" "DigiCert SHA2 Assured ID Code Signing CA" - WriteRegStr HKLM "${FallbackKey}\1" "name" "Mozilla Fake SPC" - WriteRegStr HKLM "${FallbackKey}\1" "issuer" "Mozilla Fake CA" - ${If} ${RunningX64} - SetRegView lastused - ${EndIf} -SectionEnd - -; By renaming before deleting we improve things slightly in case -; there is a file in use error. In this case a new install can happen. -Function un.RenameDelete - Pop $9 - ; If the .moz-delete file already exists previously, delete it - ; If it doesn't exist, the call is ignored. - ; We don't need to pass /REBOOTOK here since it was already marked that way - ; if it exists. - Delete "$9.moz-delete" - Rename "$9" "$9.moz-delete" - ${If} ${Errors} - Delete /REBOOTOK "$9" - ${Else} - Delete /REBOOTOK "$9.moz-delete" - ${EndIf} - ClearErrors -FunctionEnd - -Section "Uninstall" - ; Delete the service so that no updates will be attempted - ExecWait '"$INSTDIR\maintenanceservice.exe" uninstall' - - Push "$INSTDIR\updater.ini" - Call un.RenameDelete - Push "$INSTDIR\maintenanceservice.exe" - Call un.RenameDelete - Push "$INSTDIR\maintenanceservice_tmp.exe" - Call un.RenameDelete - Push "$INSTDIR\maintenanceservice.old" - Call un.RenameDelete - Push "$INSTDIR\Uninstall.exe" - Call un.RenameDelete - Push "$INSTDIR\update\updater.ini" - Call un.RenameDelete - Push "$INSTDIR\update\updater.exe" - Call un.RenameDelete - RMDir /REBOOTOK "$INSTDIR\update" - RMDir /REBOOTOK "$INSTDIR" - DeleteRegKey HKLM "${MaintUninstallKey}" - - ${If} ${RunningX64} - SetRegView 64 - ${EndIf} - DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "Installed" - DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled" - DeleteRegKey HKLM "${FallbackKey}\" - ${If} ${RunningX64} - SetRegView lastused - ${EndIf} -SectionEnd - diff --git a/toolkit/components/maintenanceservice/maintenanceservice.cpp b/toolkit/components/maintenanceservice/maintenanceservice.cpp deleted file mode 100644 index f1275b095..000000000 --- a/toolkit/components/maintenanceservice/maintenanceservice.cpp +++ /dev/null @@ -1,391 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include <windows.h> -#include <shlwapi.h> -#include <stdio.h> -#include <wchar.h> -#include <shlobj.h> - -#include "serviceinstall.h" -#include "maintenanceservice.h" -#include "servicebase.h" -#include "workmonitor.h" -#include "uachelper.h" -#include "updatehelper.h" -#include "registrycertificates.h" - -// Link w/ subsystem window so we don't get a console when executing -// this binary through the installer. -#pragma comment(linker, "/SUBSYSTEM:windows") - -SERVICE_STATUS gSvcStatus = { 0 }; -SERVICE_STATUS_HANDLE gSvcStatusHandle = nullptr; -HANDLE gWorkDoneEvent = nullptr; -HANDLE gThread = nullptr; -bool gServiceControlStopping = false; - -// logs are pretty small, about 20 lines, so 10 seems reasonable. -#define LOGS_TO_KEEP 10 - -BOOL GetLogDirectoryPath(WCHAR *path); - -int -wmain(int argc, WCHAR **argv) -{ - // If command-line parameter is "install", install the service - // or upgrade if already installed - // If command line parameter is "forceinstall", install the service - // even if it is older than what is already installed. - // If command-line parameter is "upgrade", upgrade the service - // but do not install it if it is not already installed. - // If command line parameter is "uninstall", uninstall the service. - // Otherwise, the service is probably being started by the SCM. - bool forceInstall = !lstrcmpi(argv[1], L"forceinstall"); - if (!lstrcmpi(argv[1], L"install") || forceInstall) { - WCHAR updatePath[MAX_PATH + 1]; - if (GetLogDirectoryPath(updatePath)) { - LogInit(updatePath, L"maintenanceservice-install.log"); - } - - SvcInstallAction action = InstallSvc; - if (forceInstall) { - action = ForceInstallSvc; - LOG(("Installing service with force specified...")); - } else { - LOG(("Installing service...")); - } - - bool ret = SvcInstall(action); - if (!ret) { - LOG_WARN(("Could not install service. (%d)", GetLastError())); - LogFinish(); - return 1; - } - - LOG(("The service was installed successfully")); - LogFinish(); - return 0; - } - - if (!lstrcmpi(argv[1], L"upgrade")) { - WCHAR updatePath[MAX_PATH + 1]; - if (GetLogDirectoryPath(updatePath)) { - LogInit(updatePath, L"maintenanceservice-install.log"); - } - - LOG(("Upgrading service if installed...")); - if (!SvcInstall(UpgradeSvc)) { - LOG_WARN(("Could not upgrade service. (%d)", GetLastError())); - LogFinish(); - return 1; - } - - LOG(("The service was upgraded successfully")); - LogFinish(); - return 0; - } - - if (!lstrcmpi(argv[1], L"uninstall")) { - WCHAR updatePath[MAX_PATH + 1]; - if (GetLogDirectoryPath(updatePath)) { - LogInit(updatePath, L"maintenanceservice-uninstall.log"); - } - LOG(("Uninstalling service...")); - if (!SvcUninstall()) { - LOG_WARN(("Could not uninstall service. (%d)", GetLastError())); - LogFinish(); - return 1; - } - LOG(("The service was uninstalled successfully")); - LogFinish(); - return 0; - } - - if (!lstrcmpi(argv[1], L"check-cert") && argc > 2) { - return DoesBinaryMatchAllowedCertificates(argv[2], argv[3], FALSE) ? 0 : 1; - } - - SERVICE_TABLE_ENTRYW DispatchTable[] = { - { SVC_NAME, (LPSERVICE_MAIN_FUNCTIONW) SvcMain }, - { nullptr, nullptr } - }; - - // This call returns when the service has stopped. - // The process should simply terminate when the call returns. - if (!StartServiceCtrlDispatcherW(DispatchTable)) { - LOG_WARN(("StartServiceCtrlDispatcher failed. (%d)", GetLastError())); - } - - return 0; -} - -/** - * Obtains the base path where logs should be stored - * - * @param path The out buffer for the backup log path of size MAX_PATH + 1 - * @return TRUE if successful. - */ -BOOL -GetLogDirectoryPath(WCHAR *path) -{ - if (!GetModuleFileNameW(nullptr, path, MAX_PATH)) { - return FALSE; - } - - if (!PathRemoveFileSpecW(path)) { - return FALSE; - } - - if (!PathAppendSafe(path, L"logs")) { - return FALSE; - } - CreateDirectoryW(path, nullptr); - return TRUE; -} - -/** - * Calculated a backup path based on the log number. - * - * @param path The out buffer to store the log path of size MAX_PATH + 1 - * @param basePath The base directory where the calculated path should go - * @param logNumber The log number, 0 == updater.log - * @return TRUE if successful. - */ -BOOL -GetBackupLogPath(LPWSTR path, LPCWSTR basePath, int logNumber) -{ - WCHAR logName[64] = { L'\0' }; - wcsncpy(path, basePath, sizeof(logName) / sizeof(logName[0]) - 1); - if (logNumber <= 0) { - swprintf(logName, sizeof(logName) / sizeof(logName[0]), - L"maintenanceservice.log"); - } else { - swprintf(logName, sizeof(logName) / sizeof(logName[0]), - L"maintenanceservice-%d.log", logNumber); - } - return PathAppendSafe(path, logName); -} - -/** - * Moves the old log files out of the way before a new one is written. - * If you for example keep 3 logs, then this function will do: - * updater2.log -> updater3.log - * updater1.log -> updater2.log - * updater.log -> updater1.log - * Which clears room for a new updater.log in the basePath directory - * - * @param basePath The base directory path where log files are stored - * @param numLogsToKeep The number of logs to keep - */ -void -BackupOldLogs(LPCWSTR basePath, int numLogsToKeep) -{ - WCHAR oldPath[MAX_PATH + 1]; - WCHAR newPath[MAX_PATH + 1]; - for (int i = numLogsToKeep; i >= 1; i--) { - if (!GetBackupLogPath(oldPath, basePath, i -1)) { - continue; - } - - if (!GetBackupLogPath(newPath, basePath, i)) { - continue; - } - - if (!MoveFileExW(oldPath, newPath, MOVEFILE_REPLACE_EXISTING)) { - continue; - } - } -} - -/** - * Ensures the service is shutdown once all work is complete. - * There is an issue on XP SP2 and below where the service can hang - * in a stop pending state even though the SCM is notified of a stopped - * state. Control *should* be returned to StartServiceCtrlDispatcher from the - * call to SetServiceStatus on a stopped state in the wmain thread. - * Sometimes this is not the case though. This thread will terminate the process - * if it has been 5 seconds after all work is done and the process is still not - * terminated. This thread is only started once a stopped state was sent to the - * SCM. The stop pending hang can be reproduced intermittently even if you set - * a stopped state dirctly and never set a stop pending state. It is safe to - * forcefully terminate the process ourselves since all work is done once we - * start this thread. -*/ -DWORD WINAPI -EnsureProcessTerminatedThread(LPVOID) -{ - Sleep(5000); - exit(0); - return 0; -} - -void -StartTerminationThread() -{ - // If the process does not self terminate like it should, this thread - // will terminate the process after 5 seconds. - HANDLE thread = CreateThread(nullptr, 0, EnsureProcessTerminatedThread, - nullptr, 0, nullptr); - if (thread) { - CloseHandle(thread); - } -} - -/** - * Main entry point when running as a service. - */ -void WINAPI -SvcMain(DWORD argc, LPWSTR *argv) -{ - // Setup logging, and backup the old logs - WCHAR updatePath[MAX_PATH + 1]; - if (GetLogDirectoryPath(updatePath)) { - BackupOldLogs(updatePath, LOGS_TO_KEEP); - LogInit(updatePath, L"maintenanceservice.log"); - } - - // Disable every privilege we don't need. Processes started using - // CreateProcess will use the same token as this process. - UACHelper::DisablePrivileges(nullptr); - - // Register the handler function for the service - gSvcStatusHandle = RegisterServiceCtrlHandlerW(SVC_NAME, SvcCtrlHandler); - if (!gSvcStatusHandle) { - LOG_WARN(("RegisterServiceCtrlHandler failed. (%d)", GetLastError())); - ExecuteServiceCommand(argc, argv); - LogFinish(); - exit(1); - } - - // These values will be re-used later in calls involving gSvcStatus - gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; - gSvcStatus.dwServiceSpecificExitCode = 0; - - // Report initial status to the SCM - ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000); - - // This event will be used to tell the SvcCtrlHandler when the work is - // done for when a stop comamnd is manually issued. - gWorkDoneEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); - if (!gWorkDoneEvent) { - ReportSvcStatus(SERVICE_STOPPED, 1, 0); - StartTerminationThread(); - return; - } - - // Initialization complete and we're about to start working on - // the actual command. Report the service state as running to the SCM. - ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0); - - // The service command was executed, stop logging and set an event - // to indicate the work is done in case someone is waiting on a - // service stop operation. - ExecuteServiceCommand(argc, argv); - LogFinish(); - - SetEvent(gWorkDoneEvent); - - // If we aren't already in a stopping state then tell the SCM we're stopped - // now. If we are already in a stopping state then the SERVICE_STOPPED state - // will be set by the SvcCtrlHandler. - if (!gServiceControlStopping) { - ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); - StartTerminationThread(); - } -} - -/** - * Sets the current service status and reports it to the SCM. - * - * @param currentState The current state (see SERVICE_STATUS) - * @param exitCode The system error code - * @param waitHint Estimated time for pending operation in milliseconds - */ -void -ReportSvcStatus(DWORD currentState, - DWORD exitCode, - DWORD waitHint) -{ - static DWORD dwCheckPoint = 1; - - gSvcStatus.dwCurrentState = currentState; - gSvcStatus.dwWin32ExitCode = exitCode; - gSvcStatus.dwWaitHint = waitHint; - - if (SERVICE_START_PENDING == currentState || - SERVICE_STOP_PENDING == currentState) { - gSvcStatus.dwControlsAccepted = 0; - } else { - gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | - SERVICE_ACCEPT_SHUTDOWN; - } - - if ((SERVICE_RUNNING == currentState) || - (SERVICE_STOPPED == currentState)) { - gSvcStatus.dwCheckPoint = 0; - } else { - gSvcStatus.dwCheckPoint = dwCheckPoint++; - } - - // Report the status of the service to the SCM. - SetServiceStatus(gSvcStatusHandle, &gSvcStatus); -} - -/** - * Since the SvcCtrlHandler should only spend at most 30 seconds before - * returning, this function does the service stop work for the SvcCtrlHandler. -*/ -DWORD WINAPI -StopServiceAndWaitForCommandThread(LPVOID) -{ - do { - ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000); - } while(WaitForSingleObject(gWorkDoneEvent, 100) == WAIT_TIMEOUT); - CloseHandle(gWorkDoneEvent); - gWorkDoneEvent = nullptr; - ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); - StartTerminationThread(); - return 0; -} - -/** - * Called by SCM whenever a control code is sent to the service - * using the ControlService function. - */ -void WINAPI -SvcCtrlHandler(DWORD dwCtrl) -{ - // After a SERVICE_CONTROL_STOP there should be no more commands sent to - // the SvcCtrlHandler. - if (gServiceControlStopping) { - return; - } - - // Handle the requested control code. - switch(dwCtrl) { - case SERVICE_CONTROL_SHUTDOWN: - case SERVICE_CONTROL_STOP: { - gServiceControlStopping = true; - ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000); - - // The SvcCtrlHandler thread should not spend more than 30 seconds in - // shutdown so we spawn a new thread for stopping the service - HANDLE thread = CreateThread(nullptr, 0, - StopServiceAndWaitForCommandThread, - nullptr, 0, nullptr); - if (thread) { - CloseHandle(thread); - } else { - // Couldn't start the thread so just call the stop ourselves. - // If it happens to take longer than 30 seconds the caller will - // get an error. - StopServiceAndWaitForCommandThread(nullptr); - } - } - break; - default: - break; - } -} diff --git a/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest deleted file mode 100644 index e6bfba8ca..000000000 --- a/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> -<assemblyIdentity - version="1.0.0.0" - processorArchitecture="*" - name="MaintenanceService" - type="win32" -/> -<description>MaintenanceService</description> -<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> - <ms_asmv3:security> - <ms_asmv3:requestedPrivileges> - <ms_asmv3:requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> - </ms_asmv3:requestedPrivileges> - </ms_asmv3:security> -</ms_asmv3:trustInfo> - <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> - <application> - <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> - <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> - <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> - <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> - </application> - </compatibility> -</assembly> diff --git a/toolkit/components/maintenanceservice/maintenanceservice.h b/toolkit/components/maintenanceservice/maintenanceservice.h deleted file mode 100644 index 9e02914a0..000000000 --- a/toolkit/components/maintenanceservice/maintenanceservice.h +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -void WINAPI SvcMain(DWORD dwArgc, LPWSTR *lpszArgv); -void SvcInit(DWORD dwArgc, LPWSTR *lpszArgv); -void WINAPI SvcCtrlHandler(DWORD dwCtrl); -void ReportSvcStatus(DWORD dwCurrentState, - DWORD dwWin32ExitCode, - DWORD dwWaitHint); diff --git a/toolkit/components/maintenanceservice/maintenanceservice.rc b/toolkit/components/maintenanceservice/maintenanceservice.rc deleted file mode 100644 index ddd3e942b..000000000 --- a/toolkit/components/maintenanceservice/maintenanceservice.rc +++ /dev/null @@ -1,86 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winresrc.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (U.S.) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) -#endif //_WIN32 - -///////////////////////////////////////////////////////////////////////////// -// -// RT_MANIFEST -// - -1 RT_MANIFEST "maintenanceservice.exe.manifest" - -///////////////////////////////////////////////////////////////////////////// -// -// DESIGNINFO -// - -#ifdef APSTUDIO_INVOKED -GUIDELINES DESIGNINFO -BEGIN -END -#endif // APSTUDIO_INVOKED - - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winresrc.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - -#endif // English (U.S.) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - diff --git a/toolkit/components/maintenanceservice/moz.build b/toolkit/components/maintenanceservice/moz.build deleted file mode 100644 index 2c54c8c76..000000000 --- a/toolkit/components/maintenanceservice/moz.build +++ /dev/null @@ -1,54 +0,0 @@ -# -*- 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/. - -Program('maintenanceservice') - -SOURCES += [ - 'maintenanceservice.cpp', - 'servicebase.cpp', - 'serviceinstall.cpp', - 'workmonitor.cpp', -] - -if CONFIG['OS_ARCH'] == 'WINNT': - USE_LIBS += [ - 'updatecommon-standalone', - ] -else: - USE_LIBS += [ - 'updatecommon', - ] - -# For debugging purposes only -#DEFINES['DISABLE_UPDATER_AUTHENTICODE_CHECK'] = True - -DEFINES['UNICODE'] = True -DEFINES['_UNICODE'] = True -DEFINES['NS_NO_XPCOM'] = True - -# Pick up nsWindowsRestart.cpp -LOCAL_INCLUDES += [ - '/toolkit/mozapps/update/common', - '/toolkit/xre', -] - -USE_STATIC_LIBS = True - -if CONFIG['_MSC_VER']: - WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup'] - -RCINCLUDE = 'maintenanceservice.rc' - -DISABLE_STL_WRAPPING = True - -OS_LIBS += [ - 'comctl32', - 'ws2_32', - 'shell32', -] - -with Files('**'): - BUG_COMPONENT = ('Toolkit', 'Application Update') diff --git a/toolkit/components/maintenanceservice/resource.h b/toolkit/components/maintenanceservice/resource.h deleted file mode 100644 index 45619457c..000000000 --- a/toolkit/components/maintenanceservice/resource.h +++ /dev/null @@ -1,20 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by updater.rc -// -#define IDI_DIALOG 1003 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1003 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/toolkit/components/maintenanceservice/servicebase.cpp b/toolkit/components/maintenanceservice/servicebase.cpp deleted file mode 100644 index a858c4537..000000000 --- a/toolkit/components/maintenanceservice/servicebase.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "servicebase.h" -#include "nsWindowsHelpers.h" - -// Shared code between applications and updater.exe -#include "nsWindowsRestart.cpp" - -/** - * Verifies if 2 files are byte for byte equivalent. - * - * @param file1Path The first file to verify. - * @param file2Path The second file to verify. - * @param sameContent Out parameter, TRUE if the files are equal - * @return TRUE If there was no error checking the files. - */ -BOOL -VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL &sameContent) -{ - sameContent = FALSE; - nsAutoHandle file1(CreateFileW(file1Path, GENERIC_READ, FILE_SHARE_READ, - nullptr, OPEN_EXISTING, 0, nullptr)); - if (INVALID_HANDLE_VALUE == file1) { - return FALSE; - } - nsAutoHandle file2(CreateFileW(file2Path, GENERIC_READ, FILE_SHARE_READ, - nullptr, OPEN_EXISTING, 0, nullptr)); - if (INVALID_HANDLE_VALUE == file2) { - return FALSE; - } - - DWORD fileSize1 = GetFileSize(file1, nullptr); - DWORD fileSize2 = GetFileSize(file2, nullptr); - if (INVALID_FILE_SIZE == fileSize1 || INVALID_FILE_SIZE == fileSize2) { - return FALSE; - } - - if (fileSize1 != fileSize2) { - // sameContent is already set to FALSE - return TRUE; - } - - char buf1[COMPARE_BLOCKSIZE]; - char buf2[COMPARE_BLOCKSIZE]; - DWORD numBlocks = fileSize1 / COMPARE_BLOCKSIZE; - DWORD leftOver = fileSize1 % COMPARE_BLOCKSIZE; - DWORD readAmount; - for (DWORD i = 0; i < numBlocks; i++) { - if (!ReadFile(file1, buf1, COMPARE_BLOCKSIZE, &readAmount, nullptr) || - readAmount != COMPARE_BLOCKSIZE) { - return FALSE; - } - - if (!ReadFile(file2, buf2, COMPARE_BLOCKSIZE, &readAmount, nullptr) || - readAmount != COMPARE_BLOCKSIZE) { - return FALSE; - } - - if (memcmp(buf1, buf2, COMPARE_BLOCKSIZE)) { - // sameContent is already set to FALSE - return TRUE; - } - } - - if (leftOver) { - if (!ReadFile(file1, buf1, leftOver, &readAmount, nullptr) || - readAmount != leftOver) { - return FALSE; - } - - if (!ReadFile(file2, buf2, leftOver, &readAmount, nullptr) || - readAmount != leftOver) { - return FALSE; - } - - if (memcmp(buf1, buf2, leftOver)) { - // sameContent is already set to FALSE - return TRUE; - } - } - - sameContent = TRUE; - return TRUE; -} diff --git a/toolkit/components/maintenanceservice/servicebase.h b/toolkit/components/maintenanceservice/servicebase.h deleted file mode 100644 index 447981489..000000000 --- a/toolkit/components/maintenanceservice/servicebase.h +++ /dev/null @@ -1,22 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include <windows.h> -#include "updatecommon.h" - -BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); -BOOL VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL &sameContent); - -// 32KiB for comparing files at a time seems reasonable. -// The bigger the better for speed, but this will be used -// on the stack so I don't want it to be too big. -#define COMPARE_BLOCKSIZE 32768 - -// The following string resource value is used to uniquely identify the signed -// Mozilla application as an updater. Before the maintenance service will -// execute the updater it must have this updater identity string in its string -// table. No other signed Mozilla product will have this string table value. -#define UPDATER_IDENTITY_STRING \ - "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49" -#define IDS_UPDATER_IDENTITY 1006 diff --git a/toolkit/components/maintenanceservice/serviceinstall.cpp b/toolkit/components/maintenanceservice/serviceinstall.cpp deleted file mode 100644 index 3b9522d47..000000000 --- a/toolkit/components/maintenanceservice/serviceinstall.cpp +++ /dev/null @@ -1,759 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include <windows.h> -#include <aclapi.h> -#include <stdlib.h> -#include <shlwapi.h> - -// Used for DNLEN and UNLEN -#include <lm.h> - -#include <nsWindowsHelpers.h> -#include "mozilla/UniquePtr.h" - -#include "serviceinstall.h" -#include "servicebase.h" -#include "updatehelper.h" -#include "shellapi.h" -#include "readstrings.h" -#include "errors.h" - -#pragma comment(lib, "version.lib") - -// This uninstall key is defined originally in maintenanceservice_installer.nsi -#define MAINT_UNINSTALL_KEY L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MozillaMaintenanceService" - -static BOOL -UpdateUninstallerVersionString(LPWSTR versionString) -{ - HKEY uninstallKey; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, - MAINT_UNINSTALL_KEY, 0, - KEY_WRITE | KEY_WOW64_32KEY, - &uninstallKey) != ERROR_SUCCESS) { - return FALSE; - } - - LONG rv = RegSetValueExW(uninstallKey, L"DisplayVersion", 0, REG_SZ, - reinterpret_cast<const BYTE *>(versionString), - (wcslen(versionString) + 1) * sizeof(WCHAR)); - RegCloseKey(uninstallKey); - return rv == ERROR_SUCCESS; -} - -/** - * A wrapper function to read strings for the maintenance service. - * - * @param path The path of the ini file to read from - * @param results The maintenance service strings that were read - * @return OK on success -*/ -static int -ReadMaintenanceServiceStrings(LPCWSTR path, - MaintenanceServiceStringTable *results) -{ - // Read in the maintenance service description string if specified. - const unsigned int kNumStrings = 1; - const char *kServiceKeys = "MozillaMaintenanceDescription\0"; - char serviceStrings[kNumStrings][MAX_TEXT_LEN]; - int result = ReadStrings(path, kServiceKeys, - kNumStrings, serviceStrings); - if (result != OK) { - serviceStrings[0][0] = '\0'; - } - strncpy(results->serviceDescription, - serviceStrings[0], MAX_TEXT_LEN - 1); - results->serviceDescription[MAX_TEXT_LEN - 1] = '\0'; - return result; -} - -/** - * Obtains the version number from the specified PE file's version information - * Version Format: A.B.C.D (Example 10.0.0.300) - * - * @param path The path of the file to check the version on - * @param A The first part of the version number - * @param B The second part of the version number - * @param C The third part of the version number - * @param D The fourth part of the version number - * @return TRUE if successful - */ -static BOOL -GetVersionNumberFromPath(LPWSTR path, DWORD &A, DWORD &B, - DWORD &C, DWORD &D) -{ - DWORD fileVersionInfoSize = GetFileVersionInfoSizeW(path, 0); - mozilla::UniquePtr<char[]> fileVersionInfo(new char[fileVersionInfoSize]); - if (!GetFileVersionInfoW(path, 0, fileVersionInfoSize, - fileVersionInfo.get())) { - LOG_WARN(("Could not obtain file info of old service. (%d)", - GetLastError())); - return FALSE; - } - - VS_FIXEDFILEINFO *fixedFileInfo = - reinterpret_cast<VS_FIXEDFILEINFO *>(fileVersionInfo.get()); - UINT size; - if (!VerQueryValueW(fileVersionInfo.get(), L"\\", - reinterpret_cast<LPVOID*>(&fixedFileInfo), &size)) { - LOG_WARN(("Could not query file version info of old service. (%d)", - GetLastError())); - return FALSE; - } - - A = HIWORD(fixedFileInfo->dwFileVersionMS); - B = LOWORD(fixedFileInfo->dwFileVersionMS); - C = HIWORD(fixedFileInfo->dwFileVersionLS); - D = LOWORD(fixedFileInfo->dwFileVersionLS); - return TRUE; -} - -/** - * Updates the service description with what is stored in updater.ini - * at the same path as the currently executing module binary. - * - * @param serviceHandle A handle to an opened service with - * SERVICE_CHANGE_CONFIG access right - * @param TRUE on succcess. -*/ -BOOL -UpdateServiceDescription(SC_HANDLE serviceHandle) -{ - WCHAR updaterINIPath[MAX_PATH + 1]; - if (!GetModuleFileNameW(nullptr, updaterINIPath, - sizeof(updaterINIPath) / - sizeof(updaterINIPath[0]))) { - LOG_WARN(("Could not obtain module filename when attempting to " - "modify service description. (%d)", GetLastError())); - return FALSE; - } - - if (!PathRemoveFileSpecW(updaterINIPath)) { - LOG_WARN(("Could not remove file spec when attempting to " - "modify service description. (%d)", GetLastError())); - return FALSE; - } - - if (!PathAppendSafe(updaterINIPath, L"updater.ini")) { - LOG_WARN(("Could not append updater.ini filename when attempting to " - "modify service description. (%d)", GetLastError())); - return FALSE; - } - - if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES) { - LOG_WARN(("updater.ini file does not exist, will not modify " - "service description. (%d)", GetLastError())); - return FALSE; - } - - MaintenanceServiceStringTable serviceStrings; - int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings); - if (rv != OK || !strlen(serviceStrings.serviceDescription)) { - LOG_WARN(("updater.ini file does not contain a maintenance " - "service description.")); - return FALSE; - } - - WCHAR serviceDescription[MAX_TEXT_LEN]; - if (!MultiByteToWideChar(CP_UTF8, 0, - serviceStrings.serviceDescription, -1, - serviceDescription, - sizeof(serviceDescription) / - sizeof(serviceDescription[0]))) { - LOG_WARN(("Could not convert description to wide string format. (%d)", - GetLastError())); - return FALSE; - } - - SERVICE_DESCRIPTIONW descriptionConfig; - descriptionConfig.lpDescription = serviceDescription; - if (!ChangeServiceConfig2W(serviceHandle, - SERVICE_CONFIG_DESCRIPTION, - &descriptionConfig)) { - LOG_WARN(("Could not change service config. (%d)", GetLastError())); - return FALSE; - } - - LOG(("The service description was updated successfully.")); - return TRUE; -} - -/** - * Determines if the MozillaMaintenance service path needs to be updated - * and fixes it if it is wrong. - * - * @param service A handle to the service to fix. - * @param currentServicePath The current (possibly wrong) path that is used. - * @param servicePathWasWrong Out parameter set to TRUE if a fix was needed. - * @return TRUE if the service path is now correct. -*/ -BOOL -FixServicePath(SC_HANDLE service, - LPCWSTR currentServicePath, - BOOL &servicePathWasWrong) -{ - // When we originally upgraded the MozillaMaintenance service we - // would uninstall the service on each upgrade. This had an - // intermittent error which could cause the service to use the file - // maintenanceservice_tmp.exe as the install path. Only a small number - // of Nightly users would be affected by this, but we check for this - // state here and fix the user if they are affected. - // - // We also fix the path in the case of the path not being quoted. - size_t currentServicePathLen = wcslen(currentServicePath); - bool doesServiceHaveCorrectPath = - currentServicePathLen > 2 && - !wcsstr(currentServicePath, L"maintenanceservice_tmp.exe") && - currentServicePath[0] == L'\"' && - currentServicePath[currentServicePathLen - 1] == L'\"'; - - if (doesServiceHaveCorrectPath) { - LOG(("The MozillaMaintenance service path is correct.")); - servicePathWasWrong = FALSE; - return TRUE; - } - // This is a recoverable situation so not logging as a warning - LOG(("The MozillaMaintenance path is NOT correct. It was: %ls", - currentServicePath)); - - servicePathWasWrong = TRUE; - WCHAR fixedPath[MAX_PATH + 1] = { L'\0' }; - wcsncpy(fixedPath, currentServicePath, MAX_PATH); - PathUnquoteSpacesW(fixedPath); - if (!PathRemoveFileSpecW(fixedPath)) { - LOG_WARN(("Couldn't remove file spec. (%d)", GetLastError())); - return FALSE; - } - if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe")) { - LOG_WARN(("Couldn't append file spec. (%d)", GetLastError())); - return FALSE; - } - PathQuoteSpacesW(fixedPath); - - - if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, - SERVICE_NO_CHANGE, fixedPath, nullptr, nullptr, - nullptr, nullptr, nullptr, nullptr)) { - LOG_WARN(("Could not fix service path. (%d)", GetLastError())); - return FALSE; - } - - LOG(("Fixed service path to: %ls.", fixedPath)); - return TRUE; -} - -/** - * Installs or upgrades the SVC_NAME service. - * If an existing service is already installed, we replace it with the - * currently running process. - * - * @param action The action to perform. - * @return TRUE if the service was installed/upgraded - */ -BOOL -SvcInstall(SvcInstallAction action) -{ - // Get a handle to the local computer SCM database with full access rights. - nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr, - SC_MANAGER_ALL_ACCESS)); - if (!schSCManager) { - LOG_WARN(("Could not open service manager. (%d)", GetLastError())); - return FALSE; - } - - WCHAR newServiceBinaryPath[MAX_PATH + 1]; - if (!GetModuleFileNameW(nullptr, newServiceBinaryPath, - sizeof(newServiceBinaryPath) / - sizeof(newServiceBinaryPath[0]))) { - LOG_WARN(("Could not obtain module filename when attempting to " - "install service. (%d)", - GetLastError())); - return FALSE; - } - - // Check if we already have the service installed. - nsAutoServiceHandle schService(OpenServiceW(schSCManager, - SVC_NAME, - SERVICE_ALL_ACCESS)); - DWORD lastError = GetLastError(); - if (!schService && ERROR_SERVICE_DOES_NOT_EXIST != lastError) { - // The service exists but we couldn't open it - LOG_WARN(("Could not open service. (%d)", GetLastError())); - return FALSE; - } - - if (schService) { - // The service exists but it may not have the correct permissions. - // This could happen if the permissions were not set correctly originally - // or have been changed after the installation. This will reset the - // permissions back to allow limited user accounts. - if (!SetUserAccessServiceDACL(schService)) { - LOG_WARN(("Could not reset security ACE on service handle. It might not be " - "possible to start the service. This error should never " - "happen. (%d)", GetLastError())); - } - - // The service exists and we opened it - DWORD bytesNeeded; - if (!QueryServiceConfigW(schService, nullptr, 0, &bytesNeeded) && - GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - LOG_WARN(("Could not determine buffer size for query service config. (%d)", - GetLastError())); - return FALSE; - } - - // Get the service config information, in particular we want the binary - // path of the service. - mozilla::UniquePtr<char[]> serviceConfigBuffer(new char[bytesNeeded]); - if (!QueryServiceConfigW(schService, - reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()), - bytesNeeded, &bytesNeeded)) { - LOG_WARN(("Could open service but could not query service config. (%d)", - GetLastError())); - return FALSE; - } - QUERY_SERVICE_CONFIGW &serviceConfig = - *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()); - - // Check if we need to fix the service path - BOOL servicePathWasWrong; - static BOOL alreadyCheckedFixServicePath = FALSE; - if (!alreadyCheckedFixServicePath) { - if (!FixServicePath(schService, serviceConfig.lpBinaryPathName, - servicePathWasWrong)) { - LOG_WARN(("Could not fix service path. This should never happen. (%d)", - GetLastError())); - // True is returned because the service is pointing to - // maintenanceservice_tmp.exe so it actually was upgraded to the - // newest installed service. - return TRUE; - } else if (servicePathWasWrong) { - // Now that the path is fixed we should re-attempt the install. - // This current process' image path is maintenanceservice_tmp.exe. - // The service used to point to maintenanceservice_tmp.exe. - // The service was just fixed to point to maintenanceservice.exe. - // Re-attempting an install from scratch will work as normal. - alreadyCheckedFixServicePath = TRUE; - LOG(("Restarting install action: %d", action)); - return SvcInstall(action); - } - } - - // Ensure the service path is not quoted. We own this memory and know it to - // be large enough for the quoted path, so it is large enough for the - // unquoted path. This function cannot fail. - PathUnquoteSpacesW(serviceConfig.lpBinaryPathName); - - // Obtain the existing maintenanceservice file's version number and - // the new file's version number. Versions are in the format of - // A.B.C.D. - DWORD existingA, existingB, existingC, existingD; - DWORD newA, newB, newC, newD; - BOOL obtainedExistingVersionInfo = - GetVersionNumberFromPath(serviceConfig.lpBinaryPathName, - existingA, existingB, - existingC, existingD); - if (!GetVersionNumberFromPath(newServiceBinaryPath, newA, - newB, newC, newD)) { - LOG_WARN(("Could not obtain version number from new path")); - return FALSE; - } - - // Check if we need to replace the old binary with the new one - // If we couldn't get the old version info then we assume we should - // replace it. - if (ForceInstallSvc == action || - !obtainedExistingVersionInfo || - (existingA < newA) || - (existingA == newA && existingB < newB) || - (existingA == newA && existingB == newB && - existingC < newC) || - (existingA == newA && existingB == newB && - existingC == newC && existingD < newD)) { - - // We have a newer updater, so update the description from the INI file. - UpdateServiceDescription(schService); - - schService.reset(); - if (!StopService()) { - return FALSE; - } - - if (!wcscmp(newServiceBinaryPath, serviceConfig.lpBinaryPathName)) { - LOG(("File is already in the correct location, no action needed for " - "upgrade. The path is: \"%ls\"", newServiceBinaryPath)); - return TRUE; - } - - BOOL result = TRUE; - - // Attempt to copy the new binary over top the existing binary. - // If there is an error we try to move it out of the way and then - // copy it in. First try the safest / easiest way to overwrite the file. - if (!CopyFileW(newServiceBinaryPath, - serviceConfig.lpBinaryPathName, FALSE)) { - LOG_WARN(("Could not overwrite old service binary file. " - "This should never happen, but if it does the next " - "upgrade will fix it, the service is not a critical " - "component that needs to be installed for upgrades " - "to work. (%d)", GetLastError())); - - // We rename the last 3 filename chars in an unsafe way. Manually - // verify there are more than 3 chars for safe failure in MoveFileExW. - const size_t len = wcslen(serviceConfig.lpBinaryPathName); - if (len > 3) { - // Calculate the temp file path that we're moving the file to. This - // is the same as the proper service path but with a .old extension. - LPWSTR oldServiceBinaryTempPath = - new WCHAR[len + 1]; - memset(oldServiceBinaryTempPath, 0, (len + 1) * sizeof (WCHAR)); - wcsncpy(oldServiceBinaryTempPath, serviceConfig.lpBinaryPathName, len); - // Rename the last 3 chars to 'old' - wcsncpy(oldServiceBinaryTempPath + len - 3, L"old", 3); - - // Move the current (old) service file to the temp path. - if (MoveFileExW(serviceConfig.lpBinaryPathName, - oldServiceBinaryTempPath, - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { - // The old binary is moved out of the way, copy in the new one. - if (!CopyFileW(newServiceBinaryPath, - serviceConfig.lpBinaryPathName, FALSE)) { - // It is best to leave the old service binary in this condition. - LOG_WARN(("The new service binary could not be copied in." - " The service will not be upgraded.")); - result = FALSE; - } else { - LOG(("The new service binary was copied in by first moving the" - " old one out of the way.")); - } - - // Attempt to get rid of the old service temp path. - if (DeleteFileW(oldServiceBinaryTempPath)) { - LOG(("The old temp service path was deleted: %ls.", - oldServiceBinaryTempPath)); - } else { - // The old temp path could not be removed. It will be removed - // the next time the user can't copy the binary in or on uninstall. - LOG_WARN(("The old temp service path was not deleted.")); - } - } else { - // It is best to leave the old service binary in this condition. - LOG_WARN(("Could not move old service file out of the way from:" - " \"%ls\" to \"%ls\". Service will not be upgraded. (%d)", - serviceConfig.lpBinaryPathName, - oldServiceBinaryTempPath, GetLastError())); - result = FALSE; - } - delete[] oldServiceBinaryTempPath; - } else { - // It is best to leave the old service binary in this condition. - LOG_WARN(("Service binary path was less than 3, service will" - " not be updated. This should never happen.")); - result = FALSE; - } - } else { - WCHAR versionStr[128] = { L'\0' }; - swprintf(versionStr, 128, L"%d.%d.%d.%d", newA, newB, newC, newD); - if (!UpdateUninstallerVersionString(versionStr)) { - LOG(("The uninstaller version string could not be updated.")); - } - LOG(("The new service binary was copied in.")); - } - - // We made a copy of ourselves to the existing location. - // The tmp file (the process of which we are executing right now) will be - // left over. Attempt to delete the file on the next reboot. - if (MoveFileExW(newServiceBinaryPath, nullptr, - MOVEFILE_DELAY_UNTIL_REBOOT)) { - LOG(("Deleting the old file path on the next reboot: %ls.", - newServiceBinaryPath)); - } else { - LOG_WARN(("Call to delete the old file path failed: %ls.", - newServiceBinaryPath)); - } - - return result; - } - - // We don't need to copy ourselves to the existing location. - // The tmp file (the process of which we are executing right now) will be - // left over. Attempt to delete the file on the next reboot. - MoveFileExW(newServiceBinaryPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT); - - // nothing to do, we already have a newer service installed - return TRUE; - } - - // If the service does not exist and we are upgrading, don't install it. - if (UpgradeSvc == action) { - // The service does not exist and we are upgrading, so don't install it - return TRUE; - } - - // Quote the path only if it contains spaces. - PathQuoteSpacesW(newServiceBinaryPath); - // The service does not already exist so create the service as on demand - schService.own(CreateServiceW(schSCManager, SVC_NAME, SVC_DISPLAY_NAME, - SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, - SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, - newServiceBinaryPath, nullptr, nullptr, - nullptr, nullptr, nullptr)); - if (!schService) { - LOG_WARN(("Could not create Windows service. " - "This error should never happen since a service install " - "should only be called when elevated. (%d)", GetLastError())); - return FALSE; - } - - if (!SetUserAccessServiceDACL(schService)) { - LOG_WARN(("Could not set security ACE on service handle, the service will not " - "be able to be started from unelevated processes. " - "This error should never happen. (%d)", - GetLastError())); - } - - UpdateServiceDescription(schService); - - return TRUE; -} - -/** - * Stops the Maintenance service. - * - * @return TRUE if successful. - */ -BOOL -StopService() -{ - // Get a handle to the local computer SCM database with full access rights. - nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr, - SC_MANAGER_ALL_ACCESS)); - if (!schSCManager) { - LOG_WARN(("Could not open service manager. (%d)", GetLastError())); - return FALSE; - } - - // Open the service - nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME, - SERVICE_ALL_ACCESS)); - if (!schService) { - LOG_WARN(("Could not open service. (%d)", GetLastError())); - return FALSE; - } - - LOG(("Sending stop request...")); - SERVICE_STATUS status; - SetLastError(ERROR_SUCCESS); - if (!ControlService(schService, SERVICE_CONTROL_STOP, &status) && - GetLastError() != ERROR_SERVICE_NOT_ACTIVE) { - LOG_WARN(("Error sending stop request. (%d)", GetLastError())); - } - - schSCManager.reset(); - schService.reset(); - - LOG(("Waiting for service stop...")); - DWORD lastState = WaitForServiceStop(SVC_NAME, 30); - - // The service can be in a stopped state but the exe still in use - // so make sure the process is really gone before proceeding - WaitForProcessExit(L"maintenanceservice.exe", 30); - LOG(("Done waiting for service stop, last service state: %d", lastState)); - - return lastState == SERVICE_STOPPED; -} - -/** - * Uninstalls the Maintenance service. - * - * @return TRUE if successful. - */ -BOOL -SvcUninstall() -{ - // Get a handle to the local computer SCM database with full access rights. - nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr, - SC_MANAGER_ALL_ACCESS)); - if (!schSCManager) { - LOG_WARN(("Could not open service manager. (%d)", GetLastError())); - return FALSE; - } - - // Open the service - nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME, - SERVICE_ALL_ACCESS)); - if (!schService) { - LOG_WARN(("Could not open service. (%d)", GetLastError())); - return FALSE; - } - - //Stop the service so it deletes faster and so the uninstaller - // can actually delete its EXE. - DWORD totalWaitTime = 0; - SERVICE_STATUS status; - static const int maxWaitTime = 1000 * 60; // Never wait more than a minute - if (ControlService(schService, SERVICE_CONTROL_STOP, &status)) { - do { - Sleep(status.dwWaitHint); - totalWaitTime += (status.dwWaitHint + 10); - if (status.dwCurrentState == SERVICE_STOPPED) { - break; - } else if (totalWaitTime > maxWaitTime) { - break; - } - } while (QueryServiceStatus(schService, &status)); - } - - // Delete the service or mark it for deletion - BOOL deleted = DeleteService(schService); - if (!deleted) { - deleted = (GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE); - } - - return deleted; -} - -/** - * Sets the access control list for user access for the specified service. - * - * @param hService The service to set the access control list on - * @return TRUE if successful - */ -BOOL -SetUserAccessServiceDACL(SC_HANDLE hService) -{ - PACL pNewAcl = nullptr; - PSECURITY_DESCRIPTOR psd = nullptr; - DWORD lastError = SetUserAccessServiceDACL(hService, pNewAcl, psd); - if (pNewAcl) { - LocalFree((HLOCAL)pNewAcl); - } - if (psd) { - LocalFree((LPVOID)psd); - } - return ERROR_SUCCESS == lastError; -} - -/** - * Sets the access control list for user access for the specified service. - * - * @param hService The service to set the access control list on - * @param pNewAcl The out param ACL which should be freed by caller - * @param psd out param security descriptor, should be freed by caller - * @return ERROR_SUCCESS if successful - */ -DWORD -SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl, - PSECURITY_DESCRIPTOR psd) -{ - // Get the current security descriptor needed size - DWORD needed = 0; - if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, - &psd, 0, &needed)) { - if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - LOG_WARN(("Could not query service object security size. (%d)", - GetLastError())); - return GetLastError(); - } - - DWORD size = needed; - psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size); - if (!psd) { - LOG_WARN(("Could not allocate security descriptor. (%d)", - GetLastError())); - return ERROR_INSUFFICIENT_BUFFER; - } - - // Get the actual security descriptor now - if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, - psd, size, &needed)) { - LOG_WARN(("Could not allocate security descriptor. (%d)", - GetLastError())); - return GetLastError(); - } - } - - // Get the current DACL from the security descriptor. - PACL pacl = nullptr; - BOOL bDaclPresent = FALSE; - BOOL bDaclDefaulted = FALSE; - if ( !GetSecurityDescriptorDacl(psd, &bDaclPresent, &pacl, - &bDaclDefaulted)) { - LOG_WARN(("Could not obtain DACL. (%d)", GetLastError())); - return GetLastError(); - } - - PSID sid; - DWORD SIDSize = SECURITY_MAX_SID_SIZE; - sid = LocalAlloc(LMEM_FIXED, SIDSize); - if (!sid) { - LOG_WARN(("Could not allocate SID memory. (%d)", GetLastError())); - return GetLastError(); - } - - if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sid, &SIDSize)) { - DWORD lastError = GetLastError(); - LOG_WARN(("Could not create well known SID. (%d)", lastError)); - LocalFree(sid); - return lastError; - } - - // Lookup the account name, the function fails if you don't pass in - // a buffer for the domain name but it's not used since we're using - // the built in account Sid. - SID_NAME_USE accountType; - WCHAR accountName[UNLEN + 1] = { L'\0' }; - WCHAR domainName[DNLEN + 1] = { L'\0' }; - DWORD accountNameSize = UNLEN + 1; - DWORD domainNameSize = DNLEN + 1; - if (!LookupAccountSidW(nullptr, sid, accountName, - &accountNameSize, - domainName, &domainNameSize, &accountType)) { - LOG_WARN(("Could not lookup account Sid, will try Users. (%d)", - GetLastError())); - wcsncpy(accountName, L"Users", UNLEN); - } - - // We already have the group name so we can get rid of the SID - FreeSid(sid); - sid = nullptr; - - // Build the ACE, BuildExplicitAccessWithName cannot fail so it is not logged. - EXPLICIT_ACCESS ea; - BuildExplicitAccessWithNameW(&ea, accountName, - SERVICE_START | SERVICE_STOP | GENERIC_READ, - SET_ACCESS, NO_INHERITANCE); - DWORD lastError = SetEntriesInAclW(1, (PEXPLICIT_ACCESS)&ea, pacl, &pNewAcl); - if (ERROR_SUCCESS != lastError) { - LOG_WARN(("Could not set entries in ACL. (%d)", lastError)); - return lastError; - } - - // Initialize a new security descriptor. - SECURITY_DESCRIPTOR sd; - if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) { - LOG_WARN(("Could not initialize security descriptor. (%d)", - GetLastError())); - return GetLastError(); - } - - // Set the new DACL in the security descriptor. - if (!SetSecurityDescriptorDacl(&sd, TRUE, pNewAcl, FALSE)) { - LOG_WARN(("Could not set security descriptor DACL. (%d)", - GetLastError())); - return GetLastError(); - } - - // Set the new security descriptor for the service object. - if (!SetServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &sd)) { - LOG_WARN(("Could not set object security. (%d)", - GetLastError())); - return GetLastError(); - } - - // Woohoo, raise the roof - LOG(("User access was set successfully on the service.")); - return ERROR_SUCCESS; -} diff --git a/toolkit/components/maintenanceservice/serviceinstall.h b/toolkit/components/maintenanceservice/serviceinstall.h deleted file mode 100644 index d8532a968..000000000 --- a/toolkit/components/maintenanceservice/serviceinstall.h +++ /dev/null @@ -1,21 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "readstrings.h" - -#define SVC_DISPLAY_NAME L"Mozilla Maintenance Service" - -enum SvcInstallAction { UpgradeSvc, InstallSvc, ForceInstallSvc }; -BOOL SvcInstall(SvcInstallAction action); -BOOL SvcUninstall(); -BOOL StopService(); -BOOL SetUserAccessServiceDACL(SC_HANDLE hService); -DWORD SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl, - PSECURITY_DESCRIPTOR psd); - -struct MaintenanceServiceStringTable -{ - char serviceDescription[MAX_TEXT_LEN]; -}; - diff --git a/toolkit/components/maintenanceservice/workmonitor.cpp b/toolkit/components/maintenanceservice/workmonitor.cpp deleted file mode 100644 index d06db3ca2..000000000 --- a/toolkit/components/maintenanceservice/workmonitor.cpp +++ /dev/null @@ -1,758 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include <shlobj.h> -#include <shlwapi.h> -#include <wtsapi32.h> -#include <userenv.h> -#include <shellapi.h> - -#pragma comment(lib, "wtsapi32.lib") -#pragma comment(lib, "userenv.lib") -#pragma comment(lib, "shlwapi.lib") -#pragma comment(lib, "ole32.lib") -#pragma comment(lib, "rpcrt4.lib") - -#include "nsWindowsHelpers.h" - -#include "workmonitor.h" -#include "serviceinstall.h" -#include "servicebase.h" -#include "registrycertificates.h" -#include "uachelper.h" -#include "updatehelper.h" -#include "pathhash.h" -#include "errors.h" - -// Wait 15 minutes for an update operation to run at most. -// Updates usually take less than a minute so this seems like a -// significantly large and safe amount of time to wait. -static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000; -wchar_t* MakeCommandLine(int argc, wchar_t** argv); -BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode); -BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, - LPCWSTR newFileName); -BOOL DoesFallbackKeyExist(); - -/* - * Read the update.status file and sets isApplying to true if - * the status is set to applying. - * - * @param updateDirPath The directory where update.status is stored - * @param isApplying Out parameter for specifying if the status - * is set to applying or not. - * @return TRUE if the information was filled. -*/ -static BOOL -IsStatusApplying(LPCWSTR updateDirPath, BOOL &isApplying) -{ - isApplying = FALSE; - WCHAR updateStatusFilePath[MAX_PATH + 1] = {L'\0'}; - wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH); - if (!PathAppendSafe(updateStatusFilePath, L"update.status")) { - LOG_WARN(("Could not append path for update.status file")); - return FALSE; - } - - nsAutoHandle statusFile(CreateFileW(updateStatusFilePath, GENERIC_READ, - FILE_SHARE_READ | - FILE_SHARE_WRITE | - FILE_SHARE_DELETE, - nullptr, OPEN_EXISTING, 0, nullptr)); - - if (INVALID_HANDLE_VALUE == statusFile) { - LOG_WARN(("Could not open update.status file")); - return FALSE; - } - - char buf[32] = { 0 }; - DWORD read; - if (!ReadFile(statusFile, buf, sizeof(buf), &read, nullptr)) { - LOG_WARN(("Could not read from update.status file")); - return FALSE; - } - - const char kApplying[] = "applying"; - isApplying = strncmp(buf, kApplying, - sizeof(kApplying) - 1) == 0; - return TRUE; -} - -/** - * Determines whether we're staging an update. - * - * @param argc The argc value normally sent to updater.exe - * @param argv The argv value normally sent to updater.exe - * @return boolean True if we're staging an update - */ -static bool -IsUpdateBeingStaged(int argc, LPWSTR *argv) -{ - // PID will be set to -1 if we're supposed to stage an update. - return argc == 4 && !wcscmp(argv[3], L"-1") || - argc == 5 && !wcscmp(argv[4], L"-1"); -} - -/** - * Determines whether the param only contains digits. - * - * @param str The string to check - * @param boolean True if the param only contains digits - */ -static bool -IsDigits(WCHAR *str) -{ - while (*str) { - if (!iswdigit(*str++)) { - return FALSE; - } - } - return TRUE; -} - -/** - * Determines whether the command line contains just the directory to apply the - * update to (old command line) or if it contains the installation directory and - * the directory to apply the update to. - * - * @param argc The argc value normally sent to updater.exe - * @param argv The argv value normally sent to updater.exe - * @param boolean True if the command line contains just the directory to apply - * the update to - */ -static bool -IsOldCommandline(int argc, LPWSTR *argv) -{ - return argc == 4 && !wcscmp(argv[3], L"-1") || - argc >= 4 && (wcsstr(argv[3], L"/replace") || IsDigits(argv[3])); -} - -/** - * Gets the installation directory from the arguments passed to updater.exe. - * - * @param argcTmp The argc value normally sent to updater.exe - * @param argvTmp The argv value normally sent to updater.exe - * @param aResultDir Buffer to hold the installation directory. - */ -static BOOL -GetInstallationDir(int argcTmp, LPWSTR *argvTmp, WCHAR aResultDir[MAX_PATH + 1]) -{ - int index = 3; - if (IsOldCommandline(argcTmp, argvTmp)) { - index = 2; - } - - if (argcTmp < index) { - return FALSE; - } - - wcsncpy(aResultDir, argvTmp[2], MAX_PATH); - WCHAR* backSlash = wcsrchr(aResultDir, L'\\'); - // Make sure that the path does not include trailing backslashes - if (backSlash && (backSlash[1] == L'\0')) { - *backSlash = L'\0'; - } - - // The new command line's argv[2] is always the installation directory. - if (index == 2) { - bool backgroundUpdate = IsUpdateBeingStaged(argcTmp, argvTmp); - bool replaceRequest = (argcTmp >= 4 && wcsstr(argvTmp[3], L"/replace")); - if (backgroundUpdate || replaceRequest) { - return PathRemoveFileSpecW(aResultDir); - } - } - return TRUE; -} - -/** - * Runs an update process as the service using the SYSTEM account. - * - * @param argc The number of arguments in argv - * @param argv The arguments normally passed to updater.exe - * argv[0] must be the path to updater.exe - * @param processStarted Set to TRUE if the process was started. - * @return TRUE if the update process was run had a return code of 0. - */ -BOOL -StartUpdateProcess(int argc, - LPWSTR *argv, - LPCWSTR installDir, - BOOL &processStarted) -{ - LOG(("Starting update process as the service in session 0.")); - STARTUPINFO si = {0}; - si.cb = sizeof(STARTUPINFO); - si.lpDesktop = L"winsta0\\Default"; - PROCESS_INFORMATION pi = {0}; - - // The updater command line is of the form: - // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]] - LPWSTR cmdLine = MakeCommandLine(argc, argv); - - int index = 3; - if (IsOldCommandline(argc, argv)) { - index = 2; - } - - // If we're about to start the update process from session 0, - // then we should not show a GUI. This only really needs to be done - // on Vista and higher, but it's better to keep everything consistent - // across all OS if it's of no harm. - if (argc >= index) { - // Setting the desktop to blank will ensure no GUI is displayed - si.lpDesktop = L""; - si.dwFlags |= STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; - } - - // Add an env var for MOZ_USING_SERVICE so the updater.exe can - // do anything special that it needs to do for service updates. - // Search in updater.cpp for more info on MOZ_USING_SERVICE. - putenv(const_cast<char*>("MOZ_USING_SERVICE=1")); - LOG(("Starting service with cmdline: %ls", cmdLine)); - processStarted = CreateProcessW(argv[0], cmdLine, - nullptr, nullptr, FALSE, - CREATE_DEFAULT_ERROR_MODE, - nullptr, - nullptr, &si, &pi); - - BOOL updateWasSuccessful = FALSE; - if (processStarted) { - BOOL processTerminated = FALSE; - BOOL noProcessExitCode = FALSE; - // Wait for the updater process to finish - LOG(("Process was started... waiting on result.")); - DWORD waitRes = WaitForSingleObject(pi.hProcess, TIME_TO_WAIT_ON_UPDATER); - if (WAIT_TIMEOUT == waitRes) { - // We waited a long period of time for updater.exe and it never finished - // so kill it. - TerminateProcess(pi.hProcess, 1); - processTerminated = TRUE; - } else { - // Check the return code of updater.exe to make sure we get 0 - DWORD returnCode; - if (GetExitCodeProcess(pi.hProcess, &returnCode)) { - LOG(("Process finished with return code %d.", returnCode)); - // updater returns 0 if successful. - updateWasSuccessful = (returnCode == 0); - } else { - LOG_WARN(("Process finished but could not obtain return code.")); - noProcessExitCode = TRUE; - } - } - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - - // Check just in case updater.exe didn't change the status from - // applying. If this is the case we report an error. - BOOL isApplying = FALSE; - if (IsStatusApplying(argv[1], isApplying) && isApplying) { - if (updateWasSuccessful) { - LOG(("update.status is still applying even though update was " - "successful.")); - if (!WriteStatusFailure(argv[1], - SERVICE_STILL_APPLYING_ON_SUCCESS)) { - LOG_WARN(("Could not write update.status still applying on " - "success error.")); - } - // Since we still had applying we know updater.exe didn't do its - // job correctly. - updateWasSuccessful = FALSE; - } else { - LOG_WARN(("update.status is still applying and update was not successful.")); - int failcode = SERVICE_STILL_APPLYING_ON_FAILURE; - if (noProcessExitCode) { - failcode = SERVICE_STILL_APPLYING_NO_EXIT_CODE; - } else if (processTerminated) { - failcode = SERVICE_STILL_APPLYING_TERMINATED; - } - if (!WriteStatusFailure(argv[1], failcode)) { - LOG_WARN(("Could not write update.status still applying on " - "failure error.")); - } - } - } - } else { - DWORD lastError = GetLastError(); - LOG_WARN(("Could not create process as current user, " - "updaterPath: %ls; cmdLine: %ls. (%d)", - argv[0], cmdLine, lastError)); - } - - // Empty value on putenv is how you remove an env variable in Windows - putenv(const_cast<char*>("MOZ_USING_SERVICE=")); - - free(cmdLine); - return updateWasSuccessful; -} - -/** - * Validates a file as an official updater. - * - * @param updater Path to the updater to validate - * @param installDir Path to the application installation - * being updated - * @param updateDir Update applyTo direcotry, - * where logs will be written - * - * @return true if updater is the path to a valid updater - */ -static bool -UpdaterIsValid(LPWSTR updater, LPWSTR installDir, LPWSTR updateDir) -{ - // Make sure the path to the updater to use for the update is local. - // We do this check to make sure that file locking is available for - // race condition security checks. - BOOL isLocal = FALSE; - if (!IsLocalFile(updater, isLocal) || !isLocal) { - LOG_WARN(("Filesystem in path %ls is not supported (%d)", - updater, GetLastError())); - if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_NOT_FIXED_DRIVE)) { - LOG_WARN(("Could not write update.status service update failure. (%d)", - GetLastError())); - } - return false; - } - - nsAutoHandle noWriteLock(CreateFileW(updater, GENERIC_READ, FILE_SHARE_READ, - nullptr, OPEN_EXISTING, 0, nullptr)); - if (INVALID_HANDLE_VALUE == noWriteLock) { - LOG_WARN(("Could not set no write sharing access on file. (%d)", - GetLastError())); - if (!WriteStatusFailure(updateDir, SERVICE_COULD_NOT_LOCK_UPDATER)) { - LOG_WARN(("Could not write update.status service update failure. (%d)", - GetLastError())); - } - return false; - } - - // Verify that the updater.exe that we are executing is the same - // as the one in the installation directory which we are updating. - // The installation dir that we are installing to is installDir. - WCHAR installDirUpdater[MAX_PATH + 1] = { L'\0' }; - wcsncpy(installDirUpdater, installDir, MAX_PATH); - if (!PathAppendSafe(installDirUpdater, L"updater.exe")) { - LOG_WARN(("Install directory updater could not be determined.")); - return false; - } - - BOOL updaterIsCorrect; - if (!VerifySameFiles(updater, installDirUpdater, updaterIsCorrect)) { - LOG_WARN(("Error checking if the updaters are the same.\n" - "Path 1: %ls\nPath 2: %ls", updater, installDirUpdater)); - return false; - } - - if (!updaterIsCorrect) { - LOG_WARN(("The updaters do not match, updater will not run.\n" - "Path 1: %ls\nPath 2: %ls", updater, installDirUpdater)); - if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_COMPARE_ERROR)) { - LOG_WARN(("Could not write update.status updater compare failure.")); - } - return false; - } - - LOG(("updater.exe was compared successfully to the installation directory" - " updater.exe.")); - - // Check to make sure the updater.exe module has the unique updater identity. - // This is a security measure to make sure that the signed executable that - // we will run is actually an updater. - bool result = true; - HMODULE updaterModule = LoadLibraryEx(updater, nullptr, - LOAD_LIBRARY_AS_DATAFILE); - if (!updaterModule) { - LOG_WARN(("updater.exe module could not be loaded. (%d)", GetLastError())); - result = false; - } else { - char updaterIdentity[64]; - if (!LoadStringA(updaterModule, IDS_UPDATER_IDENTITY, - updaterIdentity, sizeof(updaterIdentity))) { - LOG_WARN(("The updater.exe application does not contain the Mozilla" - " updater identity.")); - result = false; - } - - if (strcmp(updaterIdentity, UPDATER_IDENTITY_STRING)) { - LOG_WARN(("The updater.exe identity string is not valid.")); - result = false; - } - FreeLibrary(updaterModule); - } - - if (result) { - LOG(("The updater.exe application contains the Mozilla" - " updater identity.")); - } else { - if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_IDENTITY_ERROR)) { - LOG_WARN(("Could not write update.status no updater identity.")); - } - return false; - } - -#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK - return DoesBinaryMatchAllowedCertificates(installDir, updater); -#else - return true; -#endif -} - -/** - * Processes a software update command - * - * @param argc The number of arguments in argv - * @param argv The arguments normally passed to updater.exe - * argv[0] must be the path to updater.exe - * @return TRUE if the update was successful. - */ -BOOL -ProcessSoftwareUpdateCommand(DWORD argc, LPWSTR *argv) -{ - BOOL result = TRUE; - if (argc < 3) { - LOG_WARN(("Not enough command line parameters specified. " - "Updating update.status.")); - - // We can only update update.status if argv[1] exists. argv[1] is - // the directory where the update.status file exists. - if (argc < 2 || - !WriteStatusFailure(argv[1], SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS)) { - LOG_WARN(("Could not write update.status service update failure. (%d)", - GetLastError())); - } - return FALSE; - } - - WCHAR installDir[MAX_PATH + 1] = {L'\0'}; - if (!GetInstallationDir(argc, argv, installDir)) { - LOG_WARN(("Could not get the installation directory")); - if (!WriteStatusFailure(argv[1], SERVICE_INSTALLDIR_ERROR)) { - LOG_WARN(("Could not write update.status for GetInstallationDir failure.")); - } - return FALSE; - } - - if (UpdaterIsValid(argv[0], installDir, argv[1])) { - BOOL updateProcessWasStarted = FALSE; - if (StartUpdateProcess(argc, argv, installDir, - updateProcessWasStarted)) { - LOG(("updater.exe was launched and run successfully!")); - LogFlush(); - - // Don't attempt to update the service when the update is being staged. - if (!IsUpdateBeingStaged(argc, argv)) { - // We might not execute code after StartServiceUpdate because - // the service installer will stop the service if it is running. - StartServiceUpdate(installDir); - } - } else { - result = FALSE; - LOG_WARN(("Error running update process. Updating update.status (%d)", - GetLastError())); - LogFlush(); - - // If the update process was started, then updater.exe is responsible for - // setting the failure code. If it could not be started then we do the - // work. We set an error instead of directly setting status pending - // so that the app.update.service.errors pref can be updated when - // the callback app restarts. - if (!updateProcessWasStarted) { - if (!WriteStatusFailure(argv[1], - SERVICE_UPDATER_COULD_NOT_BE_STARTED)) { - LOG_WARN(("Could not write update.status service update failure. (%d)", - GetLastError())); - } - } - } - } else { - result = FALSE; - LOG_WARN(("Could not start process due to certificate check error on " - "updater.exe. Updating update.status. (%d)", GetLastError())); - - // When there is a certificate check error on the updater.exe application, - // we want to write out the error. - if (!WriteStatusFailure(argv[1], - SERVICE_UPDATER_SIGN_ERROR)) { - LOG_WARN(("Could not write pending state to update.status. (%d)", - GetLastError())); - } - } - - return result; -} - -/** - * Obtains the updater path alongside a subdir of the service binary. - * The purpose of this function is to return a path that is likely high - * integrity and therefore more safe to execute code from. - * - * @param serviceUpdaterPath Out parameter for the path where the updater - * should be copied to. - * @return TRUE if a file path was obtained. - */ -BOOL -GetSecureUpdaterPath(WCHAR serviceUpdaterPath[MAX_PATH + 1]) -{ - if (!GetModuleFileNameW(nullptr, serviceUpdaterPath, MAX_PATH)) { - LOG_WARN(("Could not obtain module filename when attempting to " - "use a secure updater path. (%d)", GetLastError())); - return FALSE; - } - - if (!PathRemoveFileSpecW(serviceUpdaterPath)) { - LOG_WARN(("Couldn't remove file spec when attempting to use a secure " - "updater path. (%d)", GetLastError())); - return FALSE; - } - - if (!PathAppendSafe(serviceUpdaterPath, L"update")) { - LOG_WARN(("Couldn't append file spec when attempting to use a secure " - "updater path. (%d)", GetLastError())); - return FALSE; - } - - CreateDirectoryW(serviceUpdaterPath, nullptr); - - if (!PathAppendSafe(serviceUpdaterPath, L"updater.exe")) { - LOG_WARN(("Couldn't append file spec when attempting to use a secure " - "updater path. (%d)", GetLastError())); - return FALSE; - } - - return TRUE; -} - -/** - * Deletes the passed in updater path and the associated updater.ini file. - * - * @param serviceUpdaterPath The path to delete. - * @return TRUE if a file was deleted. - */ -BOOL -DeleteSecureUpdater(WCHAR serviceUpdaterPath[MAX_PATH + 1]) -{ - BOOL result = FALSE; - if (serviceUpdaterPath[0]) { - result = DeleteFileW(serviceUpdaterPath); - if (!result && GetLastError() != ERROR_PATH_NOT_FOUND && - GetLastError() != ERROR_FILE_NOT_FOUND) { - LOG_WARN(("Could not delete service updater path: '%ls'.", - serviceUpdaterPath)); - } - - WCHAR updaterINIPath[MAX_PATH + 1] = { L'\0' }; - if (PathGetSiblingFilePath(updaterINIPath, serviceUpdaterPath, - L"updater.ini")) { - result = DeleteFileW(updaterINIPath); - if (!result && GetLastError() != ERROR_PATH_NOT_FOUND && - GetLastError() != ERROR_FILE_NOT_FOUND) { - LOG_WARN(("Could not delete service updater INI path: '%ls'.", - updaterINIPath)); - } - } - } - return result; -} - -/** - * Executes a service command. - * - * @param argc The number of arguments in argv - * @param argv The service command line arguments, argv[0] and argv[1] - * and automatically included by Windows. argv[2] is the - * service command. - * - * @return FALSE if there was an error executing the service command. - */ -BOOL -ExecuteServiceCommand(int argc, LPWSTR *argv) -{ - if (argc < 3) { - LOG_WARN(("Not enough command line arguments to execute a service command")); - return FALSE; - } - - // The tests work by making sure the log has changed, so we put a - // unique ID in the log. - RPC_WSTR guidString = RPC_WSTR(L""); - GUID guid; - HRESULT hr = CoCreateGuid(&guid); - if (SUCCEEDED(hr)) { - UuidToString(&guid, &guidString); - } - LOG(("Executing service command %ls, ID: %ls", - argv[2], reinterpret_cast<LPCWSTR>(guidString))); - RpcStringFree(&guidString); - - BOOL result = FALSE; - if (!lstrcmpi(argv[2], L"software-update")) { - // This check is also performed in updater.cpp and is performed here - // as well since the maintenance service can be called directly. - if (argc < 4 || !IsValidFullPath(argv[4])) { - // Since the status file is written to the patch directory and the patch - // directory is invalid don't write the status file. - LOG_WARN(("The patch directory path is not valid for this application.")); - return FALSE; - } - - // This check is also performed in updater.cpp and is performed here - // as well since the maintenance service can be called directly. - if (argc < 5 || !IsValidFullPath(argv[5])) { - LOG_WARN(("The install directory path is not valid for this application.")); - if (!WriteStatusFailure(argv[4], SERVICE_INVALID_INSTALL_DIR_PATH_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - - if (!IsOldCommandline(argc - 3, argv + 3)) { - // This check is also performed in updater.cpp and is performed here - // as well since the maintenance service can be called directly. - if (argc < 6 || !IsValidFullPath(argv[6])) { - LOG_WARN(("The working directory path is not valid for this application.")); - if (!WriteStatusFailure(argv[4], SERVICE_INVALID_WORKING_DIR_PATH_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - - // These checks are also performed in updater.cpp and is performed here - // as well since the maintenance service can be called directly. - if (_wcsnicmp(argv[6], argv[5], MAX_PATH) != 0) { - if (wcscmp(argv[7], L"-1") != 0 && !wcsstr(argv[7], L"/replace")) { - LOG_WARN(("Installation directory and working directory must be the " - "same for non-staged updates. Exiting.")); - if (!WriteStatusFailure(argv[4], SERVICE_INVALID_APPLYTO_DIR_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - - NS_tchar workingDirParent[MAX_PATH]; - NS_tsnprintf(workingDirParent, - sizeof(workingDirParent) / sizeof(workingDirParent[0]), - NS_T("%s"), argv[6]); - if (!PathRemoveFileSpecW(workingDirParent)) { - LOG_WARN(("Couldn't remove file spec when attempting to verify the " - "working directory path. (%d)", GetLastError())); - if (!WriteStatusFailure(argv[4], REMOVE_FILE_SPEC_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - - if (_wcsnicmp(workingDirParent, argv[5], MAX_PATH) != 0) { - LOG_WARN(("The apply-to directory must be the same as or " - "a child of the installation directory! Exiting.")); - if (!WriteStatusFailure(argv[4], SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - } - - } - - // Use the passed in command line arguments for the update, except for the - // path to updater.exe. We always look for updater.exe in the installation - // directory, then we copy updater.exe to a the directory of the - // MozillaMaintenance service so that a low integrity process cannot - // replace the updater.exe at any point and use that for the update. - // It also makes DLL injection attacks harder. - WCHAR installDir[MAX_PATH + 1] = { L'\0' }; - if (!GetInstallationDir(argc - 3, argv + 3, installDir)) { - LOG_WARN(("Could not get the installation directory")); - if (!WriteStatusFailure(argv[4], SERVICE_INSTALLDIR_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - - if (!DoesFallbackKeyExist()) { - WCHAR maintenanceServiceKey[MAX_PATH + 1]; - if (CalculateRegistryPathFromFilePath(installDir, maintenanceServiceKey)) { - LOG(("Checking for Maintenance Service registry. key: '%ls'", - maintenanceServiceKey)); - HKEY baseKey = nullptr; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, - maintenanceServiceKey, 0, - KEY_READ | KEY_WOW64_64KEY, - &baseKey) != ERROR_SUCCESS) { - LOG_WARN(("The maintenance service registry key does not exist.")); - if (!WriteStatusFailure(argv[4], SERVICE_INSTALL_DIR_REG_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - RegCloseKey(baseKey); - } else { - if (!WriteStatusFailure(argv[4], SERVICE_CALC_REG_PATH_ERROR)) { - LOG_WARN(("Could not write update.status for previous failure.")); - } - return FALSE; - } - } - - WCHAR installDirUpdater[MAX_PATH + 1] = { L'\0' }; - wcsncpy(installDirUpdater, installDir, MAX_PATH); - if (!PathAppendSafe(installDirUpdater, L"updater.exe")) { - LOG_WARN(("Install directory updater could not be determined.")); - result = FALSE; - } - - result = UpdaterIsValid(installDirUpdater, installDir, argv[4]); - - WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' }; - if (result) { - result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging - } - if (result) { - LOG(("Passed in path: '%ls'; Using this path for updating: '%ls'.", - installDirUpdater, secureUpdaterPath)); - DeleteSecureUpdater(secureUpdaterPath); - result = CopyFileW(installDirUpdater, secureUpdaterPath, FALSE); - } - - if (!result) { - LOG_WARN(("Could not copy path to secure location. (%d)", - GetLastError())); - if (!WriteStatusFailure(argv[4], SERVICE_COULD_NOT_COPY_UPDATER)) { - LOG_WARN(("Could not write update.status could not copy updater error")); - } - } else { - - // We obtained the path and copied it successfully, update the path to - // use for the service update. - argv[3] = secureUpdaterPath; - - WCHAR installDirUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; - WCHAR secureUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; - if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath, - L"updater.ini") && - PathGetSiblingFilePath(installDirUpdaterINIPath, installDirUpdater, - L"updater.ini")) { - // This is non fatal if it fails there is no real harm - if (!CopyFileW(installDirUpdaterINIPath, secureUpdaterINIPath, FALSE)) { - LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'. (%d)", - installDirUpdaterINIPath, secureUpdaterINIPath, GetLastError())); - } - } - - result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3); - DeleteSecureUpdater(secureUpdaterPath); - } - - // We might not reach here if the service install succeeded - // because the service self updates itself and the service - // installer will stop the service. - LOG(("Service command %ls complete.", argv[2])); - } else { - LOG_WARN(("Service command not recognized: %ls.", argv[2])); - // result is already set to FALSE - } - - LOG(("service command %ls complete with result: %ls.", - argv[1], (result ? L"Success" : L"Failure"))); - return TRUE; -} diff --git a/toolkit/components/maintenanceservice/workmonitor.h b/toolkit/components/maintenanceservice/workmonitor.h deleted file mode 100644 index ac4cd679b..000000000 --- a/toolkit/components/maintenanceservice/workmonitor.h +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -BOOL ExecuteServiceCommand(int argc, LPWSTR *argv); diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index 953e6c6e3..7173ca4e7 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -66,9 +66,6 @@ DIRS += [ 'xulstore' ] -if CONFIG['MOZ_WEBEXTENSIONS']: - DIRS += ['webextensions'] - DIRS += ['mozintl'] if not CONFIG['MOZ_FENNEC']: diff --git a/toolkit/components/osfile/NativeOSFileInternals.cpp b/toolkit/components/osfile/NativeOSFileInternals.cpp index e4725d390..36517d9ec 100644 --- a/toolkit/components/osfile/NativeOSFileInternals.cpp +++ b/toolkit/components/osfile/NativeOSFileInternals.cpp @@ -232,7 +232,6 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AbstractResult) NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbstractResult) - NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AbstractResult) diff --git a/toolkit/components/passwordmgr/LoginManagerParent.jsm b/toolkit/components/passwordmgr/LoginManagerParent.jsm index e472fb61c..2d75dac7a 100644 --- a/toolkit/components/passwordmgr/LoginManagerParent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm @@ -102,8 +102,13 @@ var LoginManagerParent = { } case "RemoteLogins:findRecipes": { - let formHost = (new URL(data.formOrigin)).host; - return this._recipeManager.getRecipesForHost(formHost); + try { + let formHost = (new URL(data.formOrigin)).host; + return this._recipeManager.getRecipesForHost(formHost); + } catch(e) { + // Just return an empty set in case of error. + return new Set(); + } } case "RemoteLogins:onFormSubmit": { diff --git a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js index 720e80446..c4be39e31 100644 --- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js +++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js @@ -97,17 +97,25 @@ LoginManagerPromptFactory.prototype = { return; } - // Allow only a limited number of authentication dialogs when they are all - // canceled by the user. - var cancelationCounter = (prompter._browser && prompter._browser.canceledAuthenticationPromptCounter) || { count: 0, id: 0 }; - if (prompt.channel) { - var httpChannel = prompt.channel.QueryInterface(Ci.nsIHttpChannel); - if (httpChannel) { - var windowId = httpChannel.topLevelContentWindowId; - if (windowId != cancelationCounter.id) { - // window has been reloaded or navigated, reset the counter - cancelationCounter = { count: 0, id: windowId }; - } + // Set up a counter for ensuring that the basic auth prompt can not + // be abused for DOS-style attacks. With this counter, each eTLD+1 + // per browser will get a limited number of times a user can + // cancel the prompt until we stop showing it. + let browser = prompter._browser; + let baseDomain = null; + if (browser && browser.isAuthDOSProtected) { + try { + baseDomain = Services.eTLD.getBaseDomainFromHost(hostname); + } catch (e) { + baseDomain = hostname; + } + + if (!browser.authPromptCounter) { + browser.authPromptCounter = {}; + } + + if (!browser.authPromptCounter[baseDomain]) { + browser.authPromptCounter[baseDomain] = 0; } } @@ -137,13 +145,14 @@ LoginManagerPromptFactory.prototype = { prompt.inProgress = false; self._asyncPromptInProgress = false; - if (ok) { - cancelationCounter.count = 0; - } else { - cancelationCounter.count++; - } - if (prompter._browser) { - prompter._browser.canceledAuthenticationPromptCounter = cancelationCounter; + if (browser && browser.isAuthDOSProtected) { + // Reset the counter state if the user replied to a prompt and actually + // tried to login (vs. simply clicking any button to get out). + if (ok && (prompt.authInfo.username || prompt.authInfo.password)) { + browser.authPromptCounter[baseDomain] = 0; + } else { + browser.authPromptCounter[baseDomain] += 1; + } } } @@ -168,14 +177,27 @@ LoginManagerPromptFactory.prototype = { var cancelDialogLimit = Services.prefs.getIntPref("prompts.authentication_dialog_abuse_limit"); - this.log("cancelationCounter =", cancelationCounter); - if (cancelDialogLimit && cancelationCounter.count >= cancelDialogLimit) { - this.log("Blocking auth dialog, due to exceeding dialog bloat limit"); - delete this._asyncPrompts[hashKey]; - - // just make the runnable cancel all consumers - runnable.cancel = true; + // Block the auth prompt if: + // - There is an attached browser element + // - The browser element has opted-in to DOS protection + // - The dialog cancellation limit is not 0 (= feature disabled) + // - The amount of cancellations >= the set abuse limit + if (browser && browser.isAuthDOSProtected) { + let cancelationCounter = browser.authPromptCounter[baseDomain]; + this.log("cancelationCounter =", cancelationCounter); + + if (cancelDialogLimit && cancelationCounter >= cancelDialogLimit) { + this.log("Blocking auth dialog, due to exceeding dialog bloat limit"); + delete this._asyncPrompts[hashKey]; + + // just make the runnable cancel all consumers + runnable.cancel = true; + } else { + this._asyncPromptInProgress = true; + prompt.inProgress = true; + } } else { + // No DOS protection: prompt this._asyncPromptInProgress = true; prompt.inProgress = true; } diff --git a/toolkit/components/perfmonitoring/nsPerformanceStats.cpp b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp index 33aeaf7c2..59d84ced1 100644 --- a/toolkit/components/perfmonitoring/nsPerformanceStats.cpp +++ b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp @@ -662,9 +662,6 @@ nsPerformanceStatsService::nsPerformanceStatsService() nsPerformanceGroup::GroupScope::RUNTIME // scope )) , mIsHandlingUserInput(false) - , mProcessStayed(0) - , mProcessMoved(0) - , mProcessUpdateCounter(0) , mIsMonitoringPerCompartment(false) , mJankAlertThreshold(mozilla::MaxValue<uint64_t>::value) // By default, no alerts , mJankAlertBufferingDelay(1000 /* ms */) @@ -953,13 +950,6 @@ nsPerformanceStatsService::SetJankAlertBufferingDelay(uint32_t value) { return NS_OK; } -nsresult -nsPerformanceStatsService::UpdateTelemetry() -{ - /* STUB */ - return NS_OK; -} - /* static */ nsIPerformanceStats* nsPerformanceStatsService::GetStatsForGroup(const js::PerformanceGroup* group) @@ -992,12 +982,6 @@ nsPerformanceStatsService::GetSnapshot(JSContext* cx, nsIPerformanceSnapshot * * } } - js::GetPerfMonitoringTestCpuRescheduling(cx, &mProcessStayed, &mProcessMoved); - - if (++mProcessUpdateCounter % 10 == 0) { - mozilla::Unused << UpdateTelemetry(); - } - snapshot.forget(aSnapshot); return NS_OK; @@ -1098,6 +1082,9 @@ nsPerformanceStatsService::GetPerformanceGroups(JSContext* cx, return false; } + // Returning a vector that is too large would cause allocations all over the + // place in the JS engine. We want to be sure that all data is stored inline. + MOZ_ASSERT(out.length() <= out.sMaxInlineStorage); return true; } @@ -1326,8 +1313,12 @@ nsPerformanceStatsService::GetResources(uint64_t* userTime, void nsPerformanceStatsService::NotifyJankObservers(const mozilla::Vector<uint64_t>& aPreviousJankLevels) { - GroupVector alerts; - mPendingAlerts.swap(alerts); + + // The move operation is generally constant time, unless + // `mPendingAlerts.length()` is very small, in which case it's fast anyway. + GroupVector alerts(Move(mPendingAlerts)); + mPendingAlerts = GroupVector(); // Reconstruct after `Move`. + if (!mPendingAlertsCollector) { // We are shutting down. return; diff --git a/toolkit/components/perfmonitoring/nsPerformanceStats.h b/toolkit/components/perfmonitoring/nsPerformanceStats.h index c82a3e92c..661a78a1a 100644 --- a/toolkit/components/perfmonitoring/nsPerformanceStats.h +++ b/toolkit/components/perfmonitoring/nsPerformanceStats.h @@ -19,7 +19,7 @@ class nsPerformanceGroup; class nsPerformanceGroupDetails; -typedef mozilla::Vector<RefPtr<nsPerformanceGroup>> GroupVector; +typedef mozilla::Vector<RefPtr<nsPerformanceGroup>, 8> GroupVector; /** * A data structure for registering observers interested in @@ -363,21 +363,6 @@ protected: nsPerformanceGroup* group); - - - /********************************************************** - * - * To check whether our algorithm makes sense, we keep count of the - * number of times the process has been rescheduled to another CPU - * while we were monitoring the performance of a group and we upload - * this data through Telemetry. - */ - nsresult UpdateTelemetry(); - - uint64_t mProcessStayed; - uint64_t mProcessMoved; - uint32_t mProcessUpdateCounter; - /********************************************************** * * Options controlling measurements. diff --git a/toolkit/components/places/Database.cpp b/toolkit/components/places/Database.cpp index a87c14b37..08c382377 100644 --- a/toolkit/components/places/Database.cpp +++ b/toolkit/components/places/Database.cpp @@ -597,7 +597,7 @@ Database::BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage) // If anything fails from this point on, we have a stale connection or // database file, and there's not much more we can do. // The only thing we can try to do is to replace the database on the next - // startup, and report the problem through telemetry. + // startup. { enum eCorruptDBReplaceStage : int8_t { stage_closing = 0, diff --git a/toolkit/components/places/Helpers.cpp b/toolkit/components/places/Helpers.cpp index dda162197..13e040bfd 100644 --- a/toolkit/components/places/Helpers.cpp +++ b/toolkit/components/places/Helpers.cpp @@ -382,12 +382,5 @@ AsyncStatementCallbackNotifier::HandleCompletion(uint16_t aReason) //////////////////////////////////////////////////////////////////////////////// //// AsyncStatementCallbackNotifier -NS_IMETHODIMP -AsyncStatementTelemetryTimer::HandleCompletion(uint16_t aReason) -{ - /* STUB */ - return NS_OK; -} - } // namespace places } // namespace mozilla diff --git a/toolkit/components/places/Helpers.h b/toolkit/components/places/Helpers.h index 654e42539..4e79abc75 100644 --- a/toolkit/components/places/Helpers.h +++ b/toolkit/components/places/Helpers.h @@ -15,7 +15,6 @@ #include "nsThreadUtils.h" #include "nsProxyRelease.h" #include "prtime.h" -#include "mozilla/Telemetry.h" namespace mozilla { namespace places { @@ -270,26 +269,6 @@ private: const char* mTopic; }; -/** - * Used to notify a topic to system observers on async execute completion. - */ -class AsyncStatementTelemetryTimer : public AsyncStatementCallback -{ -public: - explicit AsyncStatementTelemetryTimer(Telemetry::ID aHistogramId, - TimeStamp aStart = TimeStamp::Now()) - : mHistogramId(aHistogramId) - , mStart(aStart) - { - } - - NS_IMETHOD HandleCompletion(uint16_t aReason); - -private: - const Telemetry::ID mHistogramId; - const TimeStamp mStart; -}; - } // namespace places } // namespace mozilla diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp index 49d911d65..e72526022 100644 --- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -3063,17 +3063,15 @@ nsNavHistory::Observe(nsISupports *aSubject, const char *aTopic, namespace { -class DecayFrecencyCallback : public AsyncStatementTelemetryTimer +class DecayFrecencyCallback : public AsyncStatementCallback { public: DecayFrecencyCallback() - : AsyncStatementTelemetryTimer(Telemetry::PLACES_IDLE_FRECENCY_DECAY_TIME_MS) { } NS_IMETHOD HandleCompletion(uint16_t aReason) { - (void)AsyncStatementTelemetryTimer::HandleCompletion(aReason); if (aReason == REASON_FINISHED) { nsNavHistory *navHistory = nsNavHistory::GetHistoryService(); NS_ENSURE_STATE(navHistory); diff --git a/toolkit/components/search/current/nsSearchService.js b/toolkit/components/search/current/nsSearchService.js index 99e73b50b..6e8f6da43 100644 --- a/toolkit/components/search/current/nsSearchService.js +++ b/toolkit/components/search/current/nsSearchService.js @@ -21,6 +21,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", + "resource://gre/modules/Deprecated.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SearchStaticData", "resource://gre/modules/SearchStaticData.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", @@ -2691,6 +2693,13 @@ SearchService.prototype = { return; } + let performanceWarning = + "Search service falling back to synchronous initialization. " + + "This is generally the consequence of an add-on using a deprecated " + + "search service API."; + Deprecated.perfWarning(performanceWarning, "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIBrowserSearchService#async_warning"); + LOG(performanceWarning); + this._syncInit(); if (!Components.isSuccessCode(this._initRV)) { throw this._initRV; diff --git a/toolkit/components/search/orginal/nsSearchService.js b/toolkit/components/search/orginal/nsSearchService.js index 8d81e1a27..6b23724a7 100644 --- a/toolkit/components/search/orginal/nsSearchService.js +++ b/toolkit/components/search/orginal/nsSearchService.js @@ -2917,12 +2917,12 @@ SearchService.prototype = { return; } - let warning = + let performanceWarning = "Search service falling back to synchronous initialization. " + "This is generally the consequence of an add-on using a deprecated " + "search service API."; - Deprecated.warning(warning, "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIBrowserSearchService#async_warning"); - LOG(warning); + Deprecated.perfWarning(performanceWarning, "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIBrowserSearchService#async_warning"); + LOG(performanceWarning); engineMetadataService.syncInit(); this._syncInit(); diff --git a/toolkit/components/telemetry/TelemetryHistogram.cpp b/toolkit/components/telemetry/TelemetryHistogram.cpp index ba0288979..30fcc05ee 100644 --- a/toolkit/components/telemetry/TelemetryHistogram.cpp +++ b/toolkit/components/telemetry/TelemetryHistogram.cpp @@ -2090,11 +2090,6 @@ void TelemetryHistogram::InitializeGlobalState(bool canRecordBase, // don't go unnoticed. // TODO: Compare explicitly with gHistograms[<histogram id>].bucketCount here // once we can make gHistograms constexpr (requires VS2015). - static_assert((JS::gcreason::NUM_TELEMETRY_REASONS == 100), - "NUM_TELEMETRY_REASONS is assumed to be a fixed value in Histograms.json." - " If this was an intentional change, update this assert with its value " - "and update the n_values for the following in Histograms.json: " - "GC_MINOR_REASON, GC_MINOR_REASON_LONG, GC_REASON_2"); static_assert((mozilla::StartupTimeline::MAX_EVENT_ID == 16), "MAX_EVENT_ID is assumed to be a fixed value in Histograms.json. If this" " was an intentional change, update this assert with its value and update" @@ -2689,37 +2684,5 @@ TelemetryHistogram::IPCTimerFired(nsITimer* aTimer, void* aClosure) } } - switch (XRE_GetProcessType()) { - case GeckoProcessType_Content: { - mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton(); - mozilla::Unused << NS_WARN_IF(!contentChild); - if (contentChild) { - if (accumulationsToSend.Length()) { - mozilla::Unused << - NS_WARN_IF(!contentChild->SendAccumulateChildHistogram(accumulationsToSend)); - } - if (keyedAccumulationsToSend.Length()) { - mozilla::Unused << - NS_WARN_IF(!contentChild->SendAccumulateChildKeyedHistogram(keyedAccumulationsToSend)); - } - } - break; - } - case GeckoProcessType_GPU: { - if (mozilla::gfx::GPUParent* gpu = mozilla::gfx::GPUParent::GetSingleton()) { - if (accumulationsToSend.Length()) { - mozilla::Unused << gpu->SendAccumulateChildHistogram(accumulationsToSend); - } - if (keyedAccumulationsToSend.Length()) { - mozilla::Unused << gpu->SendAccumulateChildKeyedHistogram(keyedAccumulationsToSend); - } - } - break; - } - default: - MOZ_ASSERT_UNREACHABLE("Unsupported process type"); - break; - } - gIPCTimerArmed = false; } diff --git a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm index 3eec9827d..3ba0c346c 100644 --- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm +++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm @@ -41,6 +41,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", "resource://gre/modules/ContextualIdentityService.jsm"); const global = this; +// contains base64 version of a placeholder thumbnail +#include blankthumb.inc + const BackgroundPageThumbs = { /** diff --git a/toolkit/components/thumbnails/PageThumbs.jsm b/toolkit/components/thumbnails/PageThumbs.jsm index 5c7754b31..b0affee92 100644 --- a/toolkit/components/thumbnails/PageThumbs.jsm +++ b/toolkit/components/thumbnails/PageThumbs.jsm @@ -28,9 +28,6 @@ const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs. */ const THUMBNAIL_DIRECTORY = "thumbnails"; -// contains base64 version of a placeholder thumbnail -#include blankthumb.inc - Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); Cu.import("resource://gre/modules/PromiseWorker.jsm", this); Cu.import("resource://gre/modules/Promise.jsm", this); diff --git a/toolkit/components/thumbnails/moz.build b/toolkit/components/thumbnails/moz.build index d4003c635..e4a178998 100644 --- a/toolkit/components/thumbnails/moz.build +++ b/toolkit/components/thumbnails/moz.build @@ -13,13 +13,13 @@ EXTRA_COMPONENTS += [ ] EXTRA_JS_MODULES += [ - 'BackgroundPageThumbs.jsm', + 'PageThumbs.jsm', 'PageThumbsWorker.js', 'PageThumbUtils.jsm', ] EXTRA_PP_JS_MODULES += [ - 'PageThumbs.jsm', + 'BackgroundPageThumbs.jsm', ] diff --git a/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp b/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp index e230f6951..9319822d5 100644 --- a/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp +++ b/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp @@ -20,7 +20,6 @@ #include "mozilla/Logging.h" #include "nsIInterfaceRequestor.h" #include "mozilla/LoadContext.h" -#include "mozilla/Telemetry.h" #include "nsContentUtils.h" #include "nsIURLFormatter.h" @@ -503,115 +502,6 @@ nsUrlClassifierStreamUpdater::AddRequestBody(const nsACString &aRequestBody) return NS_OK; } -// Map the HTTP response code to a Telemetry bucket -static uint32_t HTTPStatusToBucket(uint32_t status) -{ - uint32_t statusBucket; - switch (status) { - case 100: - case 101: - // Unexpected 1xx return code - statusBucket = 0; - break; - case 200: - // OK - Data is available in the HTTP response body. - statusBucket = 1; - break; - case 201: - case 202: - case 203: - case 205: - case 206: - // Unexpected 2xx return code - statusBucket = 2; - break; - case 204: - // No Content - statusBucket = 3; - break; - case 300: - case 301: - case 302: - case 303: - case 304: - case 305: - case 307: - case 308: - // Unexpected 3xx return code - statusBucket = 4; - break; - case 400: - // Bad Request - The HTTP request was not correctly formed. - // The client did not provide all required CGI parameters. - statusBucket = 5; - break; - case 401: - case 402: - case 405: - case 406: - case 407: - case 409: - case 410: - case 411: - case 412: - case 414: - case 415: - case 416: - case 417: - case 421: - case 426: - case 428: - case 429: - case 431: - case 451: - // Unexpected 4xx return code - statusBucket = 6; - break; - case 403: - // Forbidden - The client id is invalid. - statusBucket = 7; - break; - case 404: - // Not Found - statusBucket = 8; - break; - case 408: - // Request Timeout - statusBucket = 9; - break; - case 413: - // Request Entity Too Large - Bug 1150334 - statusBucket = 10; - break; - case 500: - case 501: - case 510: - // Unexpected 5xx return code - statusBucket = 11; - break; - case 502: - case 504: - case 511: - // Local network errors, we'll ignore these. - statusBucket = 12; - break; - case 503: - // Service Unavailable - The server cannot handle the request. - // Clients MUST follow the backoff behavior specified in the - // Request Frequency section. - statusBucket = 13; - break; - case 505: - // HTTP Version Not Supported - The server CANNOT handle the requested - // protocol major version. - statusBucket = 14; - break; - default: - statusBucket = 15; - }; - return statusBucket; -} - /////////////////////////////////////////////////////////////////////////////// // nsIStreamListenerObserver implementation diff --git a/toolkit/components/webextensions/.eslintrc.js b/toolkit/components/webextensions/.eslintrc.js deleted file mode 100644 index 70f91ab6d..000000000 --- a/toolkit/components/webextensions/.eslintrc.js +++ /dev/null @@ -1,494 +0,0 @@ -"use strict"; - -module.exports = { // eslint-disable-line no-undef - "extends": "../../.eslintrc.js", - - "parserOptions": { - "ecmaVersion": 8, - }, - - "globals": { - "Cc": true, - "Ci": true, - "Components": true, - "Cr": true, - "Cu": true, - "dump": true, - "TextDecoder": false, - "TextEncoder": false, - // Specific to WebExtensions: - "Extension": true, - "ExtensionManagement": true, - "extensions": true, - "global": true, - "NetUtil": true, - "openOptionsPage": true, - "require": false, - "runSafe": true, - "runSafeSync": true, - "runSafeSyncWithoutClone": true, - "Services": true, - "TabManager": true, - "WindowListManager": true, - "XPCOMUtils": true, - }, - - "rules": { - // Rules from the mozilla plugin - "mozilla/balanced-listeners": "error", - "mozilla/no-aArgs": "error", - "mozilla/no-cpows-in-tests": "warn", - "mozilla/var-only-at-top-level": "warn", - - "valid-jsdoc": ["error", { - "prefer": { - "return": "returns", - }, - "preferType": { - "Boolean": "boolean", - "Number": "number", - "String": "string", - "bool": "boolean", - }, - "requireParamDescription": false, - "requireReturn": false, - "requireReturnDescription": false, - }], - - // Braces only needed for multi-line arrow function blocks - // "arrow-body-style": ["error", "as-needed"], - - // Require spacing around => - "arrow-spacing": "error", - - // Always require spacing around a single line block - "block-spacing": "warn", - - // Forbid spaces inside the square brackets of array literals. - "array-bracket-spacing": ["error", "never"], - - // Forbid spaces inside the curly brackets of object literals. - "object-curly-spacing": ["error", "never"], - - // No space padding in parentheses - "space-in-parens": ["error", "never"], - - // Enforce one true brace style (opening brace on the same line) and avoid - // start and end braces on the same line. - "brace-style": ["error", "1tbs", {"allowSingleLine": true}], - - // No space before always a space after a comma - "comma-spacing": ["error", {"before": false, "after": true}], - - // Commas at the end of the line not the start - "comma-style": "error", - - // Don't require spaces around computed properties - "computed-property-spacing": ["error", "never"], - - // Functions are not required to consistently return something or nothing - "consistent-return": "off", - - // Require braces around blocks that start a new line - "curly": ["error", "all"], - - // Always require a trailing EOL - "eol-last": "error", - - // Require function* name() - "generator-star-spacing": ["error", {"before": false, "after": true}], - - // Two space indent - "indent": ["error", 2, {"SwitchCase": 1}], - - // Space after colon not before in property declarations - "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "minimum"}], - - // Require spaces before and after finally, catch, etc. - "keyword-spacing": "error", - - // Unix linebreaks - "linebreak-style": ["error", "unix"], - - // Always require parenthesis for new calls - "new-parens": "error", - - // Use [] instead of Array() - "no-array-constructor": "error", - - // No duplicate arguments in function declarations - "no-dupe-args": "error", - - // No duplicate keys in object declarations - "no-dupe-keys": "error", - - // No duplicate cases in switch statements - "no-duplicate-case": "error", - - // If an if block ends with a return no need for an else block - // "no-else-return": "error", - - // Disallow empty statements. This will report an error for: - // try { something(); } catch (e) {} - // but will not report it for: - // try { something(); } catch (e) { /* Silencing the error because ...*/ } - // which is a valid use case. - "no-empty": "error", - - // No empty character classes in regex - "no-empty-character-class": "error", - - // Disallow empty destructuring - "no-empty-pattern": "error", - - // No assiging to exception variable - "no-ex-assign": "error", - - // No using !! where casting to boolean is already happening - "no-extra-boolean-cast": "warn", - - // No double semicolon - "no-extra-semi": "error", - - // No overwriting defined functions - "no-func-assign": "error", - - // No invalid regular expresions - "no-invalid-regexp": "error", - - // No odd whitespace characters - "no-irregular-whitespace": "error", - - // No single if block inside an else block - "no-lonely-if": "warn", - - // No mixing spaces and tabs in indent - "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], - - // Disallow use of multiple spaces (sometimes used to align const values, - // array or object items, etc.). It's hard to maintain and doesn't add that - // much benefit. - "no-multi-spaces": "warn", - - // No reassigning native JS objects - "no-native-reassign": "error", - - // Nested ternary statements are confusing - "no-nested-ternary": "error", - - // Use {} instead of new Object() - "no-new-object": "error", - - // No Math() or JSON() - "no-obj-calls": "error", - - // No octal literals - "no-octal": "error", - - // No redeclaring variables - "no-redeclare": "error", - - // No unnecessary comparisons - "no-self-compare": "error", - - // No spaces between function name and parentheses - "no-spaced-func": "warn", - - // No trailing whitespace - "no-trailing-spaces": "error", - - // Error on newline where a semicolon is needed - "no-unexpected-multiline": "error", - - // No unreachable statements - "no-unreachable": "error", - - // No expressions where a statement is expected - "no-unused-expressions": "error", - - // No declaring variables that are never used - "no-unused-vars": ["error", {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}], - - // No using variables before defined - "no-use-before-define": "error", - - // No using with - "no-with": "error", - - // Always require semicolon at end of statement - "semi": ["error", "always"], - - // Require space before blocks - "space-before-blocks": "error", - - // Never use spaces before function parentheses - "space-before-function-paren": ["error", {"anonymous": "never", "named": "never"}], - - // Require spaces around operators, except for a|0. - "space-infix-ops": ["error", {"int32Hint": true}], - - // ++ and -- should not need spacing - "space-unary-ops": ["warn", {"nonwords": false, "words": true, "overrides": {"typeof": false}}], - - // No comparisons to NaN - "use-isnan": "error", - - // Only check typeof against valid results - "valid-typeof": "error", - - // Disallow using variables outside the blocks they are defined (especially - // since only let and const are used, see "no-var"). - "block-scoped-var": "error", - - // Allow trailing commas for easy list extension. Having them does not - // impair readability, but also not required either. - "comma-dangle": ["error", "always-multiline"], - - // Warn about cyclomatic complexity in functions. - "complexity": "warn", - - // Don't warn for inconsistent naming when capturing this (not so important - // with auto-binding fat arrow functions). - // "consistent-this": ["error", "self"], - - // Don't require a default case in switch statements. Avoid being forced to - // add a bogus default when you know all possible cases are handled. - "default-case": "off", - - // Enforce dots on the next line with property name. - "dot-location": ["error", "property"], - - // Encourage the use of dot notation whenever possible. - "dot-notation": "error", - - // Allow using == instead of ===, in the interest of landing something since - // the devtools codebase is split on convention here. - "eqeqeq": "off", - - // Don't require function expressions to have a name. - // This makes the code more verbose and hard to read. Our engine already - // does a fantastic job assigning a name to the function, which includes - // the enclosing function name, and worst case you have a line number that - // you can just look up. - "func-names": "off", - - // Allow use of function declarations and expressions. - "func-style": "off", - - // Don't enforce the maximum depth that blocks can be nested. The complexity - // rule is a better rule to check this. - "max-depth": "off", - - // Maximum length of a line. - // Disabled because we exceed this in too many places. - "max-len": [0, 80], - - // Maximum depth callbacks can be nested. - "max-nested-callbacks": ["error", 4], - - // Don't limit the number of parameters that can be used in a function. - "max-params": "off", - - // Don't limit the maximum number of statement allowed in a function. We - // already have the complexity rule that's a better measurement. - "max-statements": "off", - - // Don't require a capital letter for constructors, only check if all new - // operators are followed by a capital letter. Don't warn when capitalized - // functions are used without the new operator. - "new-cap": ["off", {"capIsNew": false}], - - // Allow use of bitwise operators. - "no-bitwise": "off", - - // Disallow use of arguments.caller or arguments.callee. - "no-caller": "error", - - // Disallow the catch clause parameter name being the same as a variable in - // the outer scope, to avoid confusion. - "no-catch-shadow": "off", - - // Disallow assignment in conditional expressions. - "no-cond-assign": "error", - - // Disallow using the console API. - "no-console": "error", - - // Allow using constant expressions in conditions like while (true) - "no-constant-condition": "off", - - // Allow use of the continue statement. - "no-continue": "off", - - // Disallow control characters in regular expressions. - "no-control-regex": "error", - - // Disallow use of debugger. - "no-debugger": "error", - - // Disallow deletion of variables (deleting properties is fine). - "no-delete-var": "error", - - // Allow division operators explicitly at beginning of regular expression. - "no-div-regex": "off", - - // Disallow use of eval(). We have other APIs to evaluate code in content. - "no-eval": "error", - - // Disallow adding to native types - "no-extend-native": "error", - - // Disallow unnecessary function binding. - "no-extra-bind": "error", - - // Allow unnecessary parentheses, as they may make the code more readable. - "no-extra-parens": "off", - - // Disallow fallthrough of case statements, except if there is a comment. - "no-fallthrough": "error", - - // Allow the use of leading or trailing decimal points in numeric literals. - "no-floating-decimal": "off", - - // Allow comments inline after code. - "no-inline-comments": "off", - - // Disallow use of labels for anything other then loops and switches. - "no-labels": ["error", {"allowLoop": true}], - - // Disallow use of multiline strings (use template strings instead). - "no-multi-str": "warn", - - // Disallow multiple empty lines. - "no-multiple-empty-lines": [1, {"max": 2}], - - // Allow reassignment of function parameters. - "no-param-reassign": "off", - - // Allow string concatenation with __dirname and __filename (not a node env). - "no-path-concat": "off", - - // Allow use of unary operators, ++ and --. - "no-plusplus": "off", - - // Allow using process.env (not a node environment). - "no-process-env": "off", - - // Allow using process.exit (not a node environment). - "no-process-exit": "off", - - // Disallow usage of __proto__ property. - "no-proto": "error", - - // Disallow multiple spaces in a regular expression literal. - "no-regex-spaces": "error", - - // Allow reserved words being used as object literal keys. - "no-reserved-keys": "off", - - // Don't restrict usage of specified node modules (not a node environment). - "no-restricted-modules": "off", - - // Disallow use of assignment in return statement. It is preferable for a - // single line of code to have only one easily predictable effect. - "no-return-assign": "error", - - // Don't warn about declaration of variables already declared in the outer scope. - "no-shadow": "off", - - // Disallow shadowing of names such as arguments. - "no-shadow-restricted-names": "error", - - // Allow use of synchronous methods (not a node environment). - "no-sync": "off", - - // Allow the use of ternary operators. - "no-ternary": "off", - - // Disallow throwing literals (eg. throw "error" instead of - // throw new Error("error")). - "no-throw-literal": "error", - - // Disallow use of undeclared variables unless mentioned in a /* global */ - // block. Note that globals from head.js are automatically imported in tests - // by the import-headjs-globals rule form the mozilla eslint plugin. - "no-undef": "error", - - // Allow dangling underscores in identifiers (for privates). - "no-underscore-dangle": "off", - - // Allow use of undefined variable. - "no-undefined": "off", - - // Disallow the use of Boolean literals in conditional expressions. - "no-unneeded-ternary": "error", - - // We use var-only-at-top-level instead of no-var as we allow top level - // vars. - "no-var": "off", - - // Allow using TODO/FIXME comments. - "no-warning-comments": "off", - - // Don't require method and property shorthand syntax for object literals. - // We use this in the code a lot, but not consistently, and this seems more - // like something to check at code review time. - "object-shorthand": "off", - - // Allow more than one variable declaration per function. - "one-var": "off", - - // Disallow padding within blocks. - "padded-blocks": ["warn", "never"], - - // Don't require quotes around object literal property names. - "quote-props": "off", - - // Double quotes should be used. - "quotes": ["warn", "double", {"avoidEscape": true, "allowTemplateLiterals": true}], - - // Require use of the second argument for parseInt(). - "radix": "error", - - // Enforce spacing after semicolons. - "semi-spacing": ["error", {"before": false, "after": true}], - - // Don't require to sort variables within the same declaration block. - // Anyway, one-var is disabled. - "sort-vars": "off", - - // Require a space immediately following the // in a line comment. - "spaced-comment": ["error", "always"], - - // Require "use strict" to be defined globally in the script. - "strict": ["error", "global"], - - // Allow vars to be declared anywhere in the scope. - "vars-on-top": "off", - - // Don't require immediate function invocation to be wrapped in parentheses. - "wrap-iife": "off", - - // Don't require regex literals to be wrapped in parentheses (which - // supposedly prevent them from being mistaken for division operators). - "wrap-regex": "off", - - // Disallow Yoda conditions (where literal value comes first). - "yoda": "error", - - // disallow use of eval()-like methods - "no-implied-eval": "error", - - // Disallow function or variable declarations in nested blocks - "no-inner-declarations": "error", - - // Disallow usage of __iterator__ property - "no-iterator": "error", - - // Disallow labels that share a name with a variable - "no-label-var": "error", - - // Disallow creating new instances of String, Number, and Boolean - "no-new-wrappers": "error", - }, -}; diff --git a/toolkit/components/webextensions/Extension.jsm b/toolkit/components/webextensions/Extension.jsm deleted file mode 100644 index 3468f2594..000000000 --- a/toolkit/components/webextensions/Extension.jsm +++ /dev/null @@ -1,902 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"]; - -/* globals Extension ExtensionData */ - -/* - * This file is the main entry point for extensions. When an extension - * loads, its bootstrap.js file creates a Extension instance - * and calls .startup() on it. It calls .shutdown() when the extension - * unloads. Extension manages any extension-specific state in - * the chrome process. - */ - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.importGlobalProperties(["TextEncoder"]); - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs", - "resource://gre/modules/ExtensionAPI.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage", - "resource://gre/modules/ExtensionStorage.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon", - "resource://testing-common/ExtensionTestCommon.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Locale", - "resource://gre/modules/Locale.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Log", - "resource://gre/modules/Log.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", - "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Preferences", - "resource://gre/modules/Preferences.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "require", - "resource://devtools/shared/Loader.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); - -Cu.import("resource://gre/modules/ExtensionContent.jsm"); -Cu.import("resource://gre/modules/ExtensionManagement.jsm"); -Cu.import("resource://gre/modules/ExtensionParent.jsm"); -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "uuidGen", - "@mozilla.org/uuid-generator;1", - "nsIUUIDGenerator"); - -var { - GlobalManager, - ParentAPIManager, - apiManager: Management, -} = ExtensionParent; - -const { - EventEmitter, - LocaleData, - getUniqueId, -} = ExtensionUtils; - -XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole); - -const LOGGER_ID_BASE = "addons.webextension."; -const UUID_MAP_PREF = "extensions.webextensions.uuids"; -const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; -const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; - -const COMMENT_REGEXP = new RegExp(String.raw` - ^ - ( - (?: - [^"\n] | - " (?:[^"\\\n] | \\.)* " - )*? - ) - - //.* - `.replace(/\s+/g, ""), "gm"); - -// All moz-extension URIs use a machine-specific UUID rather than the -// extension's own ID in the host component. This makes it more -// difficult for web pages to detect whether a user has a given add-on -// installed (by trying to load a moz-extension URI referring to a -// web_accessible_resource from the extension). UUIDMap.get() -// returns the UUID for a given add-on ID. -var UUIDMap = { - _read() { - let pref = Preferences.get(UUID_MAP_PREF, "{}"); - try { - return JSON.parse(pref); - } catch (e) { - Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`); - return {}; - } - }, - - _write(map) { - Preferences.set(UUID_MAP_PREF, JSON.stringify(map)); - }, - - get(id, create = true) { - let map = this._read(); - - if (id in map) { - return map[id]; - } - - let uuid = null; - if (create) { - uuid = uuidGen.generateUUID().number; - uuid = uuid.slice(1, -1); // Strip { and } off the UUID. - - map[id] = uuid; - this._write(map); - } - return uuid; - }, - - remove(id) { - let map = this._read(); - delete map[id]; - this._write(map); - }, -}; - -// This is the old interface that UUIDMap replaced, to be removed when -// the references listed in bug 1291399 are updated. -/* exported getExtensionUUID */ -function getExtensionUUID(id) { - return UUIDMap.get(id, true); -} - -// For extensions that have called setUninstallURL(), send an event -// so the browser can display the URL. -var UninstallObserver = { - initialized: false, - - init() { - if (!this.initialized) { - AddonManager.addAddonListener(this); - XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false); - XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false); - this.initialized = true; - } - }, - - onUninstalling(addon) { - let extension = GlobalManager.extensionMap.get(addon.id); - if (extension) { - // Let any other interested listeners respond - // (e.g., display the uninstall URL) - Management.emit("uninstall", extension); - } - }, - - onUninstalled(addon) { - let uuid = UUIDMap.get(addon.id, false); - if (!uuid) { - return; - } - - if (!this.leaveStorage) { - // Clear browser.local.storage - ExtensionStorage.clear(addon.id); - - // Clear any IndexedDB storage created by the extension - let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`); - let principal = Services.scriptSecurityManager.createCodebasePrincipal( - baseURI, {addonId: addon.id} - ); - Services.qms.clearStoragesForPrincipal(principal); - - // Clear localStorage created by the extension - let attrs = JSON.stringify({addonId: addon.id}); - Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs); - } - - if (!this.leaveUuid) { - // Clear the entry in the UUID map - UUIDMap.remove(addon.id); - } - }, -}; - -UninstallObserver.init(); - -// Represents the data contained in an extension, contained either -// in a directory or a zip file, which may or may not be installed. -// This class implements the functionality of the Extension class, -// primarily related to manifest parsing and localization, which is -// useful prior to extension installation or initialization. -// -// No functionality of this class is guaranteed to work before -// |readManifest| has been called, and completed. -this.ExtensionData = class { - constructor(rootURI) { - this.rootURI = rootURI; - - this.manifest = null; - this.id = null; - this.uuid = null; - this.localeData = null; - this._promiseLocales = null; - - this.apiNames = new Set(); - this.dependencies = new Set(); - this.permissions = new Set(); - - this.errors = []; - } - - get builtinMessages() { - return null; - } - - get logger() { - let id = this.id || "<unknown>"; - return Log.repository.getLogger(LOGGER_ID_BASE + id); - } - - // Report an error about the extension's manifest file. - manifestError(message) { - this.packagingError(`Reading manifest: ${message}`); - } - - // Report an error about the extension's general packaging. - packagingError(message) { - this.errors.push(message); - this.logger.error(`Loading extension '${this.id}': ${message}`); - } - - /** - * Returns the moz-extension: URL for the given path within this - * extension. - * - * Must not be called unless either the `id` or `uuid` property has - * already been set. - * - * @param {string} path The path portion of the URL. - * @returns {string} - */ - getURL(path = "") { - if (!(this.id || this.uuid)) { - throw new Error("getURL may not be called before an `id` or `uuid` has been set"); - } - if (!this.uuid) { - this.uuid = UUIDMap.get(this.id); - } - return `moz-extension://${this.uuid}/${path}`; - } - - readDirectory(path) { - return Task.spawn(function* () { - if (this.rootURI instanceof Ci.nsIFileURL) { - let uri = NetUtil.newURI(this.rootURI.resolve("./" + path)); - let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path; - - let iter = new OS.File.DirectoryIterator(fullPath); - let results = []; - - try { - yield iter.forEach(entry => { - results.push(entry); - }); - } catch (e) { - // Always return a list, even if the directory does not exist (or is - // not a directory) for symmetry with the ZipReader behavior. - } - iter.close(); - - return results; - } - - // FIXME: We need a way to do this without main thread IO. - - let uri = this.rootURI.QueryInterface(Ci.nsIJARURI); - - let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file; - let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader); - zipReader.open(file); - try { - let results = []; - - // Normalize the directory path. - path = `${uri.JAREntry}/${path}`; - path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/"; - - // Escape pattern metacharacters. - let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&"); - - let enumerator = zipReader.findEntries(pattern + "*"); - while (enumerator.hasMore()) { - let name = enumerator.getNext(); - if (!name.startsWith(path)) { - throw new Error("Unexpected ZipReader entry"); - } - - // The enumerator returns the full path of all entries. - // Trim off the leading path, and filter out entries from - // subdirectories. - name = name.slice(path.length); - if (name && !/\/./.test(name)) { - results.push({ - name: name.replace("/", ""), - isDir: name.endsWith("/"), - }); - } - } - - return results; - } finally { - zipReader.close(); - } - }.bind(this)); - } - - readJSON(path) { - return new Promise((resolve, reject) => { - let uri = this.rootURI.resolve(`./${path}`); - - NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => { - if (!Components.isSuccessCode(status)) { - // Convert status code to a string - let e = Components.Exception("", status); - reject(new Error(`Error while loading '${uri}' (${e.name})`)); - return; - } - try { - let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(), - {charset: "utf-8"}); - - text = text.replace(COMMENT_REGEXP, "$1"); - - resolve(JSON.parse(text)); - } catch (e) { - reject(e); - } - }); - }); - } - - // Reads the extension's |manifest.json| file, and stores its - // parsed contents in |this.manifest|. - readManifest() { - return Promise.all([ - this.readJSON("manifest.json"), - Management.lazyInit(), - ]).then(([manifest]) => { - this.manifest = manifest; - this.rawManifest = manifest; - - if (manifest && manifest.default_locale) { - return this.initLocale(); - } - }).then(() => { - let context = { - url: this.baseURI && this.baseURI.spec, - - principal: this.principal, - - logError: error => { - this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`); - }, - - preprocessors: {}, - }; - - if (this.localeData) { - context.preprocessors.localize = (value, context) => this.localize(value); - } - - let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context); - if (normalized.error) { - this.manifestError(normalized.error); - } else { - this.manifest = normalized.value; - } - - try { - // Do not override the add-on id that has been already assigned. - if (!this.id && this.manifest.applications.gecko.id) { - this.id = this.manifest.applications.gecko.id; - } - } catch (e) { - // Errors are handled by the type checks above. - } - - let permissions = this.manifest.permissions || []; - - let whitelist = []; - for (let perm of permissions) { - this.permissions.add(perm); - - let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm); - if (!match) { - whitelist.push(perm); - } else if (match[1] == "experiments" && match[2]) { - this.apiNames.add(match[2]); - } - } - this.whiteListedHosts = new MatchPattern(whitelist); - - for (let api of this.apiNames) { - this.dependencies.add(`${api}@experiments.addons.mozilla.org`); - } - - return this.manifest; - }); - } - - localizeMessage(...args) { - return this.localeData.localizeMessage(...args); - } - - localize(...args) { - return this.localeData.localize(...args); - } - - // If a "default_locale" is specified in that manifest, returns it - // as a Gecko-compatible locale string. Otherwise, returns null. - get defaultLocale() { - if (this.manifest.default_locale != null) { - return this.normalizeLocaleCode(this.manifest.default_locale); - } - - return null; - } - - // Normalizes a Chrome-compatible locale code to the appropriate - // Gecko-compatible variant. Currently, this means simply - // replacing underscores with hyphens. - normalizeLocaleCode(locale) { - return String.replace(locale, /_/g, "-"); - } - - // Reads the locale file for the given Gecko-compatible locale code, and - // stores its parsed contents in |this.localeMessages.get(locale)|. - readLocaleFile(locale) { - return Task.spawn(function* () { - let locales = yield this.promiseLocales(); - let dir = locales.get(locale) || locale; - let file = `_locales/${dir}/messages.json`; - - try { - let messages = yield this.readJSON(file); - return this.localeData.addLocale(locale, messages, this); - } catch (e) { - this.packagingError(`Loading locale file ${file}: ${e}`); - return new Map(); - } - }.bind(this)); - } - - // Reads the list of locales available in the extension, and returns a - // Promise which resolves to a Map upon completion. - // Each map key is a Gecko-compatible locale code, and each value is the - // "_locales" subdirectory containing that locale: - // - // Map(gecko-locale-code -> locale-directory-name) - promiseLocales() { - if (!this._promiseLocales) { - this._promiseLocales = Task.spawn(function* () { - let locales = new Map(); - - let entries = yield this.readDirectory("_locales"); - for (let file of entries) { - if (file.isDir) { - let locale = this.normalizeLocaleCode(file.name); - locales.set(locale, file.name); - } - } - - this.localeData = new LocaleData({ - defaultLocale: this.defaultLocale, - locales, - builtinMessages: this.builtinMessages, - }); - - return locales; - }.bind(this)); - } - - return this._promiseLocales; - } - - // Reads the locale messages for all locales, and returns a promise which - // resolves to a Map of locale messages upon completion. Each key in the map - // is a Gecko-compatible locale code, and each value is a locale data object - // as returned by |readLocaleFile|. - initAllLocales() { - return Task.spawn(function* () { - let locales = yield this.promiseLocales(); - - yield Promise.all(Array.from(locales.keys(), - locale => this.readLocaleFile(locale))); - - let defaultLocale = this.defaultLocale; - if (defaultLocale) { - if (!locales.has(defaultLocale)) { - this.manifestError('Value for "default_locale" property must correspond to ' + - 'a directory in "_locales/". Not found: ' + - JSON.stringify(`_locales/${this.manifest.default_locale}/`)); - } - } else if (locales.size) { - this.manifestError('The "default_locale" property is required when a ' + - '"_locales/" directory is present.'); - } - - return this.localeData.messages; - }.bind(this)); - } - - // Reads the locale file for the given Gecko-compatible locale code, or the - // default locale if no locale code is given, and sets it as the currently - // selected locale on success. - // - // Pre-loads the default locale for fallback message processing, regardless - // of the locale specified. - // - // If no locales are unavailable, resolves to |null|. - initLocale(locale = this.defaultLocale) { - return Task.spawn(function* () { - if (locale == null) { - return null; - } - - let promises = [this.readLocaleFile(locale)]; - - let {defaultLocale} = this; - if (locale != defaultLocale && !this.localeData.has(defaultLocale)) { - promises.push(this.readLocaleFile(defaultLocale)); - } - - let results = yield Promise.all(promises); - - this.localeData.selectedLocale = locale; - return results[0]; - }.bind(this)); - } -}; - -let _browserUpdated = false; - -const PROXIED_EVENTS = new Set(["test-harness-message"]); - -// We create one instance of this class per extension. |addonData| -// comes directly from bootstrap.js when initializing. -this.Extension = class extends ExtensionData { - constructor(addonData, startupReason) { - super(addonData.resourceURI); - - this.uuid = UUIDMap.get(addonData.id); - this.instanceId = getUniqueId(); - - this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`; - Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this); - - if (addonData.cleanupFile) { - Services.obs.addObserver(this, "xpcom-shutdown", false); - this.cleanupFile = addonData.cleanupFile || null; - delete addonData.cleanupFile; - } - - this.addonData = addonData; - this.startupReason = startupReason; - - this.id = addonData.id; - this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL); - this.principal = this.createPrincipal(); - - this.onStartup = null; - - this.hasShutdown = false; - this.onShutdown = new Set(); - - this.uninstallURL = null; - - this.apis = []; - this.whiteListedHosts = null; - this.webAccessibleResources = null; - - this.emitter = new EventEmitter(); - } - - static set browserUpdated(updated) { - _browserUpdated = updated; - } - - static get browserUpdated() { - return _browserUpdated; - } - - static generateXPI(data) { - return ExtensionTestCommon.generateXPI(data); - } - - static generateZipFile(files, baseName = "generated-extension.xpi") { - return ExtensionTestCommon.generateZipFile(files, baseName); - } - - static generate(data) { - return ExtensionTestCommon.generate(data); - } - - on(hook, f) { - return this.emitter.on(hook, f); - } - - off(hook, f) { - return this.emitter.off(hook, f); - } - - emit(event, ...args) { - if (PROXIED_EVENTS.has(event)) { - Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args}); - } - - return this.emitter.emit(event, ...args); - } - - receiveMessage({name, data}) { - if (name === this.MESSAGE_EMIT_EVENT) { - this.emitter.emit(data.event, ...data.args); - } - } - - testMessage(...args) { - this.emit("test-harness-message", ...args); - } - - createPrincipal(uri = this.baseURI) { - return Services.scriptSecurityManager.createCodebasePrincipal( - uri, {addonId: this.id}); - } - - // Checks that the given URL is a child of our baseURI. - isExtensionURL(url) { - let uri = Services.io.newURI(url, null, null); - - let common = this.baseURI.getCommonBaseSpec(uri); - return common == this.baseURI.spec; - } - - readManifest() { - return super.readManifest().then(manifest => { - if (AppConstants.RELEASE_OR_BETA) { - return manifest; - } - - // Load Experiments APIs that this extension depends on. - return Promise.all( - Array.from(this.apiNames, api => ExtensionAPIs.load(api)) - ).then(apis => { - for (let API of apis) { - this.apis.push(new API(this)); - } - - return manifest; - }); - }); - } - - // Representation of the extension to send to content - // processes. This should include anything the content process might - // need. - serialize() { - return { - id: this.id, - uuid: this.uuid, - instanceId: this.instanceId, - manifest: this.manifest, - resourceURL: this.addonData.resourceURI.spec, - baseURL: this.baseURI.spec, - content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase - webAccessibleResources: this.webAccessibleResources.serialize(), - whiteListedHosts: this.whiteListedHosts.serialize(), - localeData: this.localeData.serialize(), - permissions: this.permissions, - principal: this.principal, - }; - } - - broadcast(msg, data) { - return new Promise(resolve => { - let count = Services.ppmm.childCount; - Services.ppmm.addMessageListener(msg + "Complete", function listener() { - count--; - if (count == 0) { - Services.ppmm.removeMessageListener(msg + "Complete", listener); - resolve(); - } - }); - Services.ppmm.broadcastAsyncMessage(msg, data); - }); - } - - runManifest(manifest) { - // Strip leading slashes from web_accessible_resources. - let strippedWebAccessibleResources = []; - if (manifest.web_accessible_resources) { - strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, "")); - } - - this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources); - - let promises = []; - for (let directive in manifest) { - if (manifest[directive] !== null) { - promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest)); - } - } - - let data = Services.ppmm.initialProcessData; - if (!data["Extension:Extensions"]) { - data["Extension:Extensions"] = []; - } - let serial = this.serialize(); - data["Extension:Extensions"].push(serial); - - return this.broadcast("Extension:Startup", serial).then(() => { - return Promise.all(promises); - }); - } - - callOnClose(obj) { - this.onShutdown.add(obj); - } - - forgetOnClose(obj) { - this.onShutdown.delete(obj); - } - - get builtinMessages() { - return new Map([ - ["@@extension_id", this.uuid], - ]); - } - - // Reads the locale file for the given Gecko-compatible locale code, or if - // no locale is given, the available locale closest to the UI locale. - // Sets the currently selected locale on success. - initLocale(locale = undefined) { - // Ugh. - let super_ = super.initLocale.bind(this); - - return Task.spawn(function* () { - if (locale === undefined) { - let locales = yield this.promiseLocales(); - - let localeList = Array.from(locales.keys(), locale => { - return {name: locale, locales: [locale]}; - }); - - let match = Locale.findClosestLocale(localeList); - locale = match ? match.name : this.defaultLocale; - } - - return super_(locale); - }.bind(this)); - } - - startup() { - let started = false; - return this.readManifest().then(() => { - ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this); - started = true; - - if (!this.hasShutdown) { - return this.initLocale(); - } - }).then(() => { - if (this.errors.length) { - return Promise.reject({errors: this.errors}); - } - - if (this.hasShutdown) { - return; - } - - GlobalManager.init(this); - - // The "startup" Management event sent on the extension instance itself - // is emitted just before the Management "startup" event, - // and it is used to run code that needs to be executed before - // any of the "startup" listeners. - this.emit("startup", this); - Management.emit("startup", this); - - return this.runManifest(this.manifest); - }).then(() => { - Management.emit("ready", this); - }).catch(e => { - dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`); - Cu.reportError(e); - - if (started) { - ExtensionManagement.shutdownExtension(this.uuid); - } - - this.cleanupGeneratedFile(); - - throw e; - }); - } - - cleanupGeneratedFile() { - if (!this.cleanupFile) { - return; - } - - let file = this.cleanupFile; - this.cleanupFile = null; - - Services.obs.removeObserver(this, "xpcom-shutdown"); - - this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => { - // We can't delete this file until everyone using it has - // closed it (because Windows is dumb). So we wait for all the - // child processes (including the parent) to flush their JAR - // caches. These caches may keep the file open. - file.remove(false); - }); - } - - shutdown() { - this.hasShutdown = true; - - Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this); - - if (!this.manifest) { - ExtensionManagement.shutdownExtension(this.uuid); - - this.cleanupGeneratedFile(); - return; - } - - GlobalManager.uninit(this); - - for (let obj of this.onShutdown) { - obj.close(); - } - - for (let api of this.apis) { - api.destroy(); - } - - ParentAPIManager.shutdownExtension(this.id); - - Management.emit("shutdown", this); - - Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id}); - - MessageChannel.abortResponses({extensionId: this.id}); - - ExtensionManagement.shutdownExtension(this.uuid); - - this.cleanupGeneratedFile(); - } - - observe(subject, topic, data) { - if (topic == "xpcom-shutdown") { - this.cleanupGeneratedFile(); - } - } - - hasPermission(perm) { - let match = /^manifest:(.*)/.exec(perm); - if (match) { - return this.manifest[match[1]] != null; - } - - return this.permissions.has(perm); - } - - get name() { - return this.manifest.name; - } -}; diff --git a/toolkit/components/webextensions/ExtensionAPI.jsm b/toolkit/components/webextensions/ExtensionAPI.jsm deleted file mode 100644 index 54dab8e3b..000000000 --- a/toolkit/components/webextensions/ExtensionAPI.jsm +++ /dev/null @@ -1,81 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionAPI", "ExtensionAPIs"]; - -/* exported ExtensionAPIs */ - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/ExtensionManagement.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", - "resource://devtools/shared/event-emitter.js"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); - -const global = this; - -class ExtensionAPI { - constructor(extension) { - this.extension = extension; - } - - destroy() { - } - - getAPI(context) { - throw new Error("Not Implemented"); - } -} - -var ExtensionAPIs = { - apis: ExtensionManagement.APIs.apis, - - load(apiName) { - let api = this.apis.get(apiName); - - if (api.loadPromise) { - return api.loadPromise; - } - - let {script, schema} = api; - - let addonId = `${apiName}@experiments.addons.mozilla.org`; - api.sandbox = Cu.Sandbox(global, { - wantXrays: false, - sandboxName: script, - addonId, - metadata: {addonID: addonId}, - }); - - api.sandbox.ExtensionAPI = ExtensionAPI; - - Services.scriptloader.loadSubScript(script, api.sandbox, "UTF-8"); - - api.loadPromise = Schemas.load(schema).then(() => { - return Cu.evalInSandbox("API", api.sandbox); - }); - - return api.loadPromise; - }, - - unload(apiName) { - let api = this.apis.get(apiName); - - let {schema} = api; - - Schemas.unload(schema); - Cu.nukeSandbox(api.sandbox); - - api.sandbox = null; - api.loadPromise = null; - }, -}; diff --git a/toolkit/components/webextensions/ExtensionChild.jsm b/toolkit/components/webextensions/ExtensionChild.jsm deleted file mode 100644 index 5dc4e2277..000000000 --- a/toolkit/components/webextensions/ExtensionChild.jsm +++ /dev/null @@ -1,1058 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionChild"]; - -/* - * This file handles addon logic that is independent of the chrome process. - * When addons run out-of-process, this is the main entry point. - * Its primary function is managing addon globals. - * - * Don't put contentscript logic here, use ExtensionContent.jsm instead. - */ - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", - "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NativeApp", - "resource://gre/modules/NativeMessaging.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", - "resource://gre/modules/PromiseUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); - -const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon"; - -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -const { - DefaultMap, - EventManager, - SingletonEventManager, - SpreadArgs, - defineLazyGetter, - getInnerWindowID, - getMessageManager, - getUniqueId, - injectAPI, -} = ExtensionUtils; - -const { - BaseContext, - LocalAPIImplementation, - SchemaAPIInterface, - SchemaAPIManager, -} = ExtensionCommon; - -var ExtensionChild; - -/** - * Abstraction for a Port object in the extension API. - * - * @param {BaseContext} context The context that owns this port. - * @param {nsIMessageSender} senderMM The message manager to send messages to. - * @param {Array<nsIMessageListenerManager>} receiverMMs Message managers to - * listen on. - * @param {string} name Arbitrary port name as defined by the addon. - * @param {string} id An ID that uniquely identifies this port's channel. - * @param {object} sender The `port.sender` property. - * @param {object} recipient The recipient of messages sent from this port. - */ -class Port { - constructor(context, senderMM, receiverMMs, name, id, sender, recipient) { - this.context = context; - this.senderMM = senderMM; - this.receiverMMs = receiverMMs; - this.name = name; - this.id = id; - this.sender = sender; - this.recipient = recipient; - this.disconnected = false; - this.disconnectListeners = new Set(); - this.unregisterMessageFuncs = new Set(); - - // Common options for onMessage and onDisconnect. - this.handlerBase = { - messageFilterStrict: {portId: id}, - - filterMessage: (sender, recipient) => { - return sender.contextId !== this.context.contextId; - }, - }; - - this.disconnectHandler = Object.assign({ - receiveMessage: ({data}) => this.disconnectByOtherEnd(data), - }, this.handlerBase); - - MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler); - - this.context.callOnClose(this); - } - - api() { - let portObj = Cu.createObjectIn(this.context.cloneScope); - - let portError = null; - let publicAPI = { - name: this.name, - - disconnect: () => { - this.disconnect(); - }, - - postMessage: json => { - this.postMessage(json); - }, - - onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => { - return this.registerOnDisconnect(error => { - portError = error && this.context.normalizeError(error); - fire.withoutClone(portObj); - }); - }).api(), - - onMessage: new EventManager(this.context, "Port.onMessage", fire => { - return this.registerOnMessage(msg => { - msg = Cu.cloneInto(msg, this.context.cloneScope); - fire.withoutClone(msg, portObj); - }); - }).api(), - - get error() { - return portError; - }, - }; - - if (this.sender) { - publicAPI.sender = this.sender; - } - - injectAPI(publicAPI, portObj); - return portObj; - } - - postMessage(json) { - if (this.disconnected) { - throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port"); - } - - this._sendMessage("Extension:Port:PostMessage", json); - } - - /** - * Register a callback that is called when the port is disconnected by the - * *other* end. The callback is automatically unregistered when the port or - * context is closed. - * - * @param {function} callback Called when the other end disconnects the port. - * If the disconnect is caused by an error, the first parameter is an - * object with a "message" string property that describes the cause. - * @returns {function} Function to unregister the listener. - */ - registerOnDisconnect(callback) { - let listener = error => { - if (this.context.active && !this.disconnected) { - callback(error); - } - }; - this.disconnectListeners.add(listener); - return () => { - this.disconnectListeners.delete(listener); - }; - } - - /** - * Register a callback that is called when a message is received. The callback - * is automatically unregistered when the port or context is closed. - * - * @param {function} callback Called when a message is received. - * @returns {function} Function to unregister the listener. - */ - registerOnMessage(callback) { - let handler = Object.assign({ - receiveMessage: ({data}) => { - if (this.context.active && !this.disconnected) { - callback(data); - } - }, - }, this.handlerBase); - - let unregister = () => { - this.unregisterMessageFuncs.delete(unregister); - MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler); - }; - MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler); - this.unregisterMessageFuncs.add(unregister); - return unregister; - } - - _sendMessage(message, data) { - let options = { - recipient: Object.assign({}, this.recipient, {portId: this.id}), - responseType: MessageChannel.RESPONSE_NONE, - }; - - return this.context.sendMessage(this.senderMM, message, data, options); - } - - handleDisconnection() { - MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler); - for (let unregister of this.unregisterMessageFuncs) { - unregister(); - } - this.context.forgetOnClose(this); - this.disconnected = true; - } - - /** - * Disconnect the port from the other end (which may not even exist). - * - * @param {Error|{message: string}} [error] The reason for disconnecting, - * if it is an abnormal disconnect. - */ - disconnectByOtherEnd(error = null) { - if (this.disconnected) { - return; - } - - for (let listener of this.disconnectListeners) { - listener(error); - } - - this.handleDisconnection(); - } - - /** - * Disconnect the port from this end. - * - * @param {Error|{message: string}} [error] The reason for disconnecting, - * if it is an abnormal disconnect. - */ - disconnect(error = null) { - if (this.disconnected) { - // disconnect() may be called without side effects even after the port is - // closed - https://developer.chrome.com/extensions/runtime#type-Port - return; - } - this.handleDisconnection(); - if (error) { - error = {message: this.context.normalizeError(error).message}; - } - this._sendMessage("Extension:Port:Disconnect", error); - } - - close() { - this.disconnect(); - } -} - -class NativePort extends Port { - postMessage(data) { - data = NativeApp.encodeMessage(this.context, data); - - return super.postMessage(data); - } -} - -/** - * Each extension context gets its own Messenger object. It handles the - * basics of sendMessage, onMessage, connect and onConnect. - * - * @param {BaseContext} context The context to which this Messenger is tied. - * @param {Array<nsIMessageListenerManager>} messageManagers - * The message managers used to receive messages (e.g. onMessage/onConnect - * requests). - * @param {object} sender Describes this sender to the recipient. This object - * is extended further by BaseContext's sendMessage method and appears as - * the `sender` object to `onConnect` and `onMessage`. - * Do not set the `extensionId`, `contextId` or `tab` properties. The former - * two are added by BaseContext's sendMessage, while `sender.tab` is set by - * the ProxyMessenger in the main process. - * @param {object} filter A recipient filter to apply to incoming messages from - * the broker. Messages are only handled by this Messenger if all key-value - * pairs match the `recipient` as specified by the sender of the message. - * In other words, this filter defines the required fields of `recipient`. - * @param {object} [optionalFilter] An additional filter to apply to incoming - * messages. Unlike `filter`, the keys from `optionalFilter` are allowed to - * be omitted from `recipient`. Only keys that are present in both - * `optionalFilter` and `recipient` are applied to filter incoming messages. - */ -class Messenger { - constructor(context, messageManagers, sender, filter, optionalFilter) { - this.context = context; - this.messageManagers = messageManagers; - this.sender = sender; - this.filter = filter; - this.optionalFilter = optionalFilter; - } - - _sendMessage(messageManager, message, data, recipient) { - let options = { - recipient, - sender: this.sender, - responseType: MessageChannel.RESPONSE_FIRST, - }; - - return this.context.sendMessage(messageManager, message, data, options); - } - - sendMessage(messageManager, msg, recipient, responseCallback) { - let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient) - .catch(error => { - if (error.result == MessageChannel.RESULT_NO_HANDLER) { - return Promise.reject({message: "Could not establish connection. Receiving end does not exist."}); - } else if (error.result != MessageChannel.RESULT_NO_RESPONSE) { - return Promise.reject({message: error.message}); - } - }); - - return this.context.wrapPromise(promise, responseCallback); - } - - sendNativeMessage(messageManager, msg, recipient, responseCallback) { - msg = NativeApp.encodeMessage(this.context, msg); - return this.sendMessage(messageManager, msg, recipient, responseCallback); - } - - _onMessage(name, filter) { - return new SingletonEventManager(this.context, name, callback => { - let listener = { - messageFilterPermissive: this.optionalFilter, - messageFilterStrict: this.filter, - - filterMessage: (sender, recipient) => { - // Ignore the message if it was sent by this Messenger. - return (sender.contextId !== this.context.contextId && - filter(sender, recipient)); - }, - - receiveMessage: ({target, data: message, sender, recipient}) => { - if (!this.context.active) { - return; - } - - let sendResponse; - let response = undefined; - let promise = new Promise(resolve => { - sendResponse = value => { - resolve(value); - response = promise; - }; - }); - - message = Cu.cloneInto(message, this.context.cloneScope); - sender = Cu.cloneInto(sender, this.context.cloneScope); - sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope); - - // Note: We intentionally do not use runSafe here so that any - // errors are propagated to the message sender. - let result = callback(message, sender, sendResponse); - if (result instanceof this.context.cloneScope.Promise) { - return result; - } else if (result === true) { - return promise; - } - return response; - }, - }; - - MessageChannel.addListener(this.messageManagers, "Extension:Message", listener); - return () => { - MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener); - }; - }).api(); - } - - onMessage(name) { - return this._onMessage(name, sender => sender.id === this.sender.id); - } - - onMessageExternal(name) { - return this._onMessage(name, sender => sender.id !== this.sender.id); - } - - _connect(messageManager, port, recipient) { - let msg = { - name: port.name, - portId: port.id, - }; - - this._sendMessage(messageManager, "Extension:Connect", msg, recipient).catch(error => { - if (error.result === MessageChannel.RESULT_NO_HANDLER) { - error = {message: "Could not establish connection. Receiving end does not exist."}; - } else if (error.result === MessageChannel.RESULT_DISCONNECTED) { - error = null; - } - port.disconnectByOtherEnd(error); - }); - - return port.api(); - } - - connect(messageManager, name, recipient) { - let portId = getUniqueId(); - - let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient); - - return this._connect(messageManager, port, recipient); - } - - connectNative(messageManager, name, recipient) { - let portId = getUniqueId(); - - let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient); - - return this._connect(messageManager, port, recipient); - } - - _onConnect(name, filter) { - return new SingletonEventManager(this.context, name, callback => { - let listener = { - messageFilterPermissive: this.optionalFilter, - messageFilterStrict: this.filter, - - filterMessage: (sender, recipient) => { - // Ignore the port if it was created by this Messenger. - return (sender.contextId !== this.context.contextId && - filter(sender, recipient)); - }, - - receiveMessage: ({target, data: message, sender}) => { - let {name, portId} = message; - let mm = getMessageManager(target); - let recipient = Object.assign({}, sender); - if (recipient.tab) { - recipient.tabId = recipient.tab.id; - delete recipient.tab; - } - let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient); - this.context.runSafeWithoutClone(callback, port.api()); - return true; - }, - }; - - MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener); - return () => { - MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener); - }; - }).api(); - } - - onConnect(name) { - return this._onConnect(name, sender => sender.id === this.sender.id); - } - - onConnectExternal(name) { - return this._onConnect(name, sender => sender.id !== this.sender.id); - } -} - -var apiManager = new class extends SchemaAPIManager { - constructor() { - super("addon"); - this.initialized = false; - } - - generateAPIs(...args) { - if (!this.initialized) { - this.initialized = true; - for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_ADDON)) { - this.loadScript(value); - } - } - return super.generateAPIs(...args); - } - - registerSchemaAPI(namespace, envType, getAPI) { - if (envType == "addon_child") { - super.registerSchemaAPI(namespace, envType, getAPI); - } - } -}(); - -/** - * An object that runs an remote implementation of an API. - */ -class ProxyAPIImplementation extends SchemaAPIInterface { - /** - * @param {string} namespace The full path to the namespace that contains the - * `name` member. This may contain dots, e.g. "storage.local". - * @param {string} name The name of the method or property. - * @param {ChildAPIManager} childApiManager The owner of this implementation. - */ - constructor(namespace, name, childApiManager) { - super(); - this.path = `${namespace}.${name}`; - this.childApiManager = childApiManager; - } - - callFunctionNoReturn(args) { - this.childApiManager.callParentFunctionNoReturn(this.path, args); - } - - callAsyncFunction(args, callback) { - return this.childApiManager.callParentAsyncFunction(this.path, args, callback); - } - - addListener(listener, args) { - let map = this.childApiManager.listeners.get(this.path); - - if (map.listeners.has(listener)) { - // TODO: Called with different args? - return; - } - - let id = getUniqueId(); - - map.ids.set(id, listener); - map.listeners.set(listener, id); - - this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", { - childId: this.childApiManager.id, - listenerId: id, - path: this.path, - args, - }); - } - - removeListener(listener) { - let map = this.childApiManager.listeners.get(this.path); - - if (!map.listeners.has(listener)) { - return; - } - - let id = map.listeners.get(listener); - map.listeners.delete(listener); - map.ids.delete(id); - - this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", { - childId: this.childApiManager.id, - listenerId: id, - path: this.path, - }); - } - - hasListener(listener) { - let map = this.childApiManager.listeners.get(this.path); - return map.listeners.has(listener); - } -} - -// We create one instance of this class for every extension context that -// needs to use remote APIs. It uses the message manager to communicate -// with the ParentAPIManager singleton in ExtensionParent.jsm. It -// handles asynchronous function calls as well as event listeners. -class ChildAPIManager { - constructor(context, messageManager, localApis, contextData) { - this.context = context; - this.messageManager = messageManager; - this.url = contextData.url; - - // The root namespace of all locally implemented APIs. If an extension calls - // an API that does not exist in this object, then the implementation is - // delegated to the ParentAPIManager. - this.localApis = localApis; - - this.id = `${context.extension.id}.${context.contextId}`; - - MessageChannel.addListener(messageManager, "API:RunListener", this); - messageManager.addMessageListener("API:CallResult", this); - - this.messageFilterStrict = {childId: this.id}; - - this.listeners = new DefaultMap(() => ({ - ids: new Map(), - listeners: new Map(), - })); - - // Map[callId -> Deferred] - this.callPromises = new Map(); - - let params = { - childId: this.id, - extensionId: context.extension.id, - principal: context.principal, - }; - Object.assign(params, contextData); - - this.messageManager.sendAsyncMessage("API:CreateProxyContext", params); - } - - receiveMessage({name, messageName, data}) { - if (data.childId != this.id) { - return; - } - - switch (name || messageName) { - case "API:RunListener": - let map = this.listeners.get(data.path); - let listener = map.ids.get(data.listenerId); - - if (listener) { - return this.context.runSafe(listener, ...data.args); - } - - Cu.reportError(`Unknown listener at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n`); - break; - - case "API:CallResult": - let deferred = this.callPromises.get(data.callId); - if ("error" in data) { - deferred.reject(data.error); - } else { - deferred.resolve(new SpreadArgs(data.result)); - } - this.callPromises.delete(data.callId); - break; - } - } - - /** - * Call a function in the parent process and ignores its return value. - * - * @param {string} path The full name of the method, e.g. "tabs.create". - * @param {Array} args The parameters for the function. - */ - callParentFunctionNoReturn(path, args) { - this.messageManager.sendAsyncMessage("API:Call", { - childId: this.id, - path, - args, - }); - } - - /** - * Calls a function in the parent process and returns its result - * asynchronously. - * - * @param {string} path The full name of the method, e.g. "tabs.create". - * @param {Array} args The parameters for the function. - * @param {function(*)} [callback] The callback to be called when the function - * completes. - * @returns {Promise|undefined} Must be void if `callback` is set, and a - * promise otherwise. The promise is resolved when the function completes. - */ - callParentAsyncFunction(path, args, callback) { - let callId = getUniqueId(); - let deferred = PromiseUtils.defer(); - this.callPromises.set(callId, deferred); - - this.messageManager.sendAsyncMessage("API:Call", { - childId: this.id, - callId, - path, - args, - }); - - return this.context.wrapPromise(deferred.promise, callback); - } - - /** - * Create a proxy for an event in the parent process. The returned event - * object shares its internal state with other instances. For instance, if - * `removeListener` is used on a listener that was added on another object - * through `addListener`, then the event is unregistered. - * - * @param {string} path The full name of the event, e.g. "tabs.onCreated". - * @returns {object} An object with the addListener, removeListener and - * hasListener methods. See SchemaAPIInterface for documentation. - */ - getParentEvent(path) { - path = path.split("."); - - let name = path.pop(); - let namespace = path.join("."); - - let impl = new ProxyAPIImplementation(namespace, name, this); - return { - addListener: (listener, ...args) => impl.addListener(listener, args), - removeListener: (listener) => impl.removeListener(listener), - hasListener: (listener) => impl.hasListener(listener), - }; - } - - close() { - this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id}); - } - - get cloneScope() { - return this.context.cloneScope; - } - - get principal() { - return this.context.principal; - } - - shouldInject(namespace, name, allowedContexts) { - // Do not generate content script APIs, unless explicitly allowed. - if (this.context.envType === "content_child" && - !allowedContexts.includes("content")) { - return false; - } - if (allowedContexts.includes("addon_parent_only")) { - return false; - } - return true; - } - - getImplementation(namespace, name) { - let obj = namespace.split(".").reduce( - (object, prop) => object && object[prop], - this.localApis); - - if (obj && name in obj) { - return new LocalAPIImplementation(obj, name, this.context); - } - - return this.getFallbackImplementation(namespace, name); - } - - getFallbackImplementation(namespace, name) { - // No local API found, defer implementation to the parent. - return new ProxyAPIImplementation(namespace, name, this); - } - - hasPermission(permission) { - return this.context.extension.hasPermission(permission); - } -} - -class ExtensionPageContextChild extends BaseContext { - /** - * This ExtensionPageContextChild represents a privileged addon - * execution environment that has full access to the WebExtensions - * APIs (provided that the correct permissions have been requested). - * - * This is the child side of the ExtensionPageContextParent class - * defined in ExtensionParent.jsm. - * - * @param {BrowserExtensionContent} extension This context's owner. - * @param {object} params - * @param {nsIDOMWindow} params.contentWindow The window where the addon runs. - * @param {string} params.viewType One of "background", "popup" or "tab". - * "background" and "tab" are used by `browser.extension.getViews`. - * "popup" is only used internally to identify page action and browser - * action popups and options_ui pages. - * @param {number} [params.tabId] This tab's ID, used if viewType is "tab". - */ - constructor(extension, params) { - super("addon_child", extension); - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - // This check is temporary. It should be removed once the proxy creation - // is asynchronous. - throw new Error("ExtensionPageContextChild cannot be created in child processes"); - } - - let {viewType, uri, contentWindow, tabId} = params; - this.viewType = viewType; - this.uri = uri || extension.baseURI; - - this.setContentWindow(contentWindow); - - // This is the MessageSender property passed to extension. - // It can be augmented by the "page-open" hook. - let sender = {id: extension.id}; - if (viewType == "tab") { - sender.tabId = tabId; - this.tabId = tabId; - } - if (uri) { - sender.url = uri.spec; - } - this.sender = sender; - - Schemas.exportLazyGetter(contentWindow, "browser", () => { - let browserObj = Cu.createObjectIn(contentWindow); - Schemas.inject(browserObj, this.childManager); - return browserObj; - }); - - Schemas.exportLazyGetter(contentWindow, "chrome", () => { - let chromeApiWrapper = Object.create(this.childManager); - chromeApiWrapper.isChromeCompat = true; - - let chromeObj = Cu.createObjectIn(contentWindow); - Schemas.inject(chromeObj, chromeApiWrapper); - return chromeObj; - }); - - this.extension.views.add(this); - } - - get cloneScope() { - return this.contentWindow; - } - - get principal() { - return this.contentWindow.document.nodePrincipal; - } - - get windowId() { - if (this.viewType == "tab" || this.viewType == "popup") { - let globalView = ExtensionChild.contentGlobals.get(this.messageManager); - return globalView ? globalView.windowId : -1; - } - } - - // Called when the extension shuts down. - shutdown() { - this.unload(); - } - - // This method is called when an extension page navigates away or - // its tab is closed. - unload() { - // Note that without this guard, we end up running unload code - // multiple times for tab pages closed by the "page-unload" handlers - // triggered below. - if (this.unloaded) { - return; - } - - if (this.contentWindow) { - this.contentWindow.close(); - } - - super.unload(); - this.extension.views.delete(this); - } -} - -defineLazyGetter(ExtensionPageContextChild.prototype, "messenger", function() { - let filter = {extensionId: this.extension.id}; - let optionalFilter = {}; - // Addon-generated messages (not necessarily from the same process as the - // addon itself) are sent to the main process, which forwards them via the - // parent process message manager. Specific replies can be sent to the frame - // message manager. - return new Messenger(this, [Services.cpmm, this.messageManager], this.sender, - filter, optionalFilter); -}); - -defineLazyGetter(ExtensionPageContextChild.prototype, "childManager", function() { - let localApis = {}; - apiManager.generateAPIs(this, localApis); - - if (this.viewType == "background") { - apiManager.global.initializeBackgroundPage(this.contentWindow); - } - - let childManager = new ChildAPIManager(this, this.messageManager, localApis, { - envType: "addon_parent", - viewType: this.viewType, - url: this.uri.spec, - incognito: this.incognito, - }); - - this.callOnClose(childManager); - - return childManager; -}); - -// All subframes in a tab, background page, popup, etc. have the same view type. -// This class keeps track of such global state. -// Note that this is created even for non-extension tabs because at present we -// do not have a way to distinguish regular tabs from extension tabs at the -// initialization of a frame script. -class ContentGlobal { - /** - * @param {nsIContentFrameMessageManager} global The frame script's global. - */ - constructor(global) { - this.global = global; - // Unless specified otherwise assume that the extension page is in a tab, - // because the majority of all class instances are going to be a tab. Any - // special views (background page, extension popup) will immediately send an - // Extension:InitExtensionView message to change the viewType. - this.viewType = "tab"; - this.tabId = -1; - this.windowId = -1; - this.initialized = false; - this.global.addMessageListener("Extension:InitExtensionView", this); - this.global.addMessageListener("Extension:SetTabAndWindowId", this); - - this.initialDocuments = new WeakSet(); - } - - uninit() { - this.global.removeMessageListener("Extension:InitExtensionView", this); - this.global.removeMessageListener("Extension:SetTabAndWindowId", this); - this.global.removeEventListener("DOMContentLoaded", this); - } - - ensureInitialized() { - if (!this.initialized) { - // Request tab and window ID in case "Extension:InitExtensionView" is not - // sent (e.g. when `viewType` is "tab"). - let reply = this.global.sendSyncMessage("Extension:GetTabAndWindowId"); - this.handleSetTabAndWindowId(reply[0] || {}); - } - return this; - } - - receiveMessage({name, data}) { - switch (name) { - case "Extension:InitExtensionView": - // The view type is initialized once and then fixed. - this.global.removeMessageListener("Extension:InitExtensionView", this); - let {viewType, url} = data; - this.viewType = viewType; - this.global.addEventListener("DOMContentLoaded", this); - if (url) { - // TODO(robwu): Remove this check. It is only here because the popup - // implementation does not always load a URL at the initialization, - // and the logic is too complex to fix at once. - let {document} = this.global.content; - this.initialDocuments.add(document); - document.location.replace(url); - } - /* Falls through to allow these properties to be initialized at once */ - case "Extension:SetTabAndWindowId": - this.handleSetTabAndWindowId(data); - break; - } - } - - handleSetTabAndWindowId(data) { - let {tabId, windowId} = data; - if (tabId) { - // Tab IDs are not expected to change. - if (this.tabId !== -1 && tabId !== this.tabId) { - throw new Error("Attempted to change a tabId after it was set"); - } - this.tabId = tabId; - } - if (windowId !== undefined) { - // Window IDs may change if a tab is moved to a different location. - // Note: This is the ID of the browser window for the extension API. - // Do not confuse it with the innerWindowID of DOMWindows! - this.windowId = windowId; - } - this.initialized = true; - } - - // "DOMContentLoaded" event. - handleEvent(event) { - let {document} = this.global.content; - if (event.target === document) { - // If the document was still being loaded at the time of navigation, then - // the DOMContentLoaded event is fired for the old document. Ignore it. - if (this.initialDocuments.has(document)) { - this.initialDocuments.delete(document); - return; - } - this.global.removeEventListener("DOMContentLoaded", this); - this.global.sendAsyncMessage("Extension:ExtensionViewLoaded"); - } - } -} - -ExtensionChild = { - // Map<nsIContentFrameMessageManager, ContentGlobal> - contentGlobals: new Map(), - - // Map<innerWindowId, ExtensionPageContextChild> - extensionContexts: new Map(), - - initOnce() { - // This initializes the default message handler for messages targeted at - // an addon process, in case the addon process receives a message before - // its Messenger has been instantiated. For example, if a content script - // sends a message while there is no background page. - MessageChannel.setupMessageManagers([Services.cpmm]); - }, - - init(global) { - this.contentGlobals.set(global, new ContentGlobal(global)); - }, - - uninit(global) { - this.contentGlobals.get(global).uninit(); - this.contentGlobals.delete(global); - }, - - /** - * Create a privileged context at document-element-inserted. - * - * @param {BrowserExtensionContent} extension - * The extension for which the context should be created. - * @param {nsIDOMWindow} contentWindow The global of the page. - */ - createExtensionContext(extension, contentWindow) { - let windowId = getInnerWindowID(contentWindow); - let context = this.extensionContexts.get(windowId); - if (context) { - if (context.extension !== extension) { - // Oops. This should never happen. - Cu.reportError("A different extension context already exists in this frame!"); - } else { - // This should not happen either. - Cu.reportError("The extension context was already initialized in this frame."); - } - return; - } - - let mm = contentWindow - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - let {viewType, tabId} = this.contentGlobals.get(mm).ensureInitialized(); - - let uri = contentWindow.document.documentURIObject; - - context = new ExtensionPageContextChild(extension, {viewType, contentWindow, uri, tabId}); - this.extensionContexts.set(windowId, context); - }, - - /** - * Close the ExtensionPageContextChild belonging to the given window, if any. - * - * @param {number} windowId The inner window ID of the destroyed context. - */ - destroyExtensionContext(windowId) { - let context = this.extensionContexts.get(windowId); - if (context) { - context.unload(); - this.extensionContexts.delete(windowId); - } - }, - - shutdownExtension(extensionId) { - for (let [windowId, context] of this.extensionContexts) { - if (context.extension.id == extensionId) { - context.shutdown(); - this.extensionContexts.delete(windowId); - } - } - }, -}; - -// TODO(robwu): Change this condition when addons move to a separate process. -if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - Object.keys(ExtensionChild).forEach(function(key) { - if (typeof ExtensionChild[key] == "function") { - // :/ - ExtensionChild[key] = () => {}; - } - }); -} - -Object.assign(ExtensionChild, { - ChildAPIManager, - Messenger, - Port, -}); - diff --git a/toolkit/components/webextensions/ExtensionCommon.jsm b/toolkit/components/webextensions/ExtensionCommon.jsm deleted file mode 100644 index 9ec84b5c7..000000000 --- a/toolkit/components/webextensions/ExtensionCommon.jsm +++ /dev/null @@ -1,679 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -/** - * This module contains utilities and base classes for logic which is - * common between the parent and child process, and in particular - * between ExtensionParent.jsm and ExtensionChild.jsm. - */ - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -/* exported ExtensionCommon */ - -this.EXPORTED_SYMBOLS = ["ExtensionCommon"]; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", - "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -var { - EventEmitter, - ExtensionError, - SpreadArgs, - getConsole, - getInnerWindowID, - getUniqueId, - runSafeSync, - runSafeSyncWithoutClone, - instanceOf, -} = ExtensionUtils; - -XPCOMUtils.defineLazyGetter(this, "console", getConsole); - -class BaseContext { - constructor(envType, extension) { - this.envType = envType; - this.onClose = new Set(); - this.checkedLastError = false; - this._lastError = null; - this.contextId = getUniqueId(); - this.unloaded = false; - this.extension = extension; - this.jsonSandbox = null; - this.active = true; - this.incognito = null; - this.messageManager = null; - this.docShell = null; - this.contentWindow = null; - this.innerWindowID = 0; - } - - setContentWindow(contentWindow) { - let {document} = contentWindow; - let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell); - - this.innerWindowID = getInnerWindowID(contentWindow); - this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - - if (this.incognito == null) { - this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow); - } - - MessageChannel.setupMessageManagers([this.messageManager]); - - let onPageShow = event => { - if (!event || event.target === document) { - this.docShell = docShell; - this.contentWindow = contentWindow; - this.active = true; - } - }; - let onPageHide = event => { - if (!event || event.target === document) { - // Put this off until the next tick. - Promise.resolve().then(() => { - this.docShell = null; - this.contentWindow = null; - this.active = false; - }); - } - }; - - onPageShow(); - contentWindow.addEventListener("pagehide", onPageHide, true); - contentWindow.addEventListener("pageshow", onPageShow, true); - this.callOnClose({ - close: () => { - onPageHide(); - if (this.active) { - contentWindow.removeEventListener("pagehide", onPageHide, true); - contentWindow.removeEventListener("pageshow", onPageShow, true); - } - }, - }); - } - - get cloneScope() { - throw new Error("Not implemented"); - } - - get principal() { - throw new Error("Not implemented"); - } - - runSafe(...args) { - if (this.unloaded) { - Cu.reportError("context.runSafe called after context unloaded"); - } else if (!this.active) { - Cu.reportError("context.runSafe called while context is inactive"); - } else { - return runSafeSync(this, ...args); - } - } - - runSafeWithoutClone(...args) { - if (this.unloaded) { - Cu.reportError("context.runSafeWithoutClone called after context unloaded"); - } else if (!this.active) { - Cu.reportError("context.runSafeWithoutClone called while context is inactive"); - } else { - return runSafeSyncWithoutClone(...args); - } - } - - checkLoadURL(url, options = {}) { - let ssm = Services.scriptSecurityManager; - - let flags = ssm.STANDARD; - if (!options.allowScript) { - flags |= ssm.DISALLOW_SCRIPT; - } - if (!options.allowInheritsPrincipal) { - flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; - } - if (options.dontReportErrors) { - flags |= ssm.DONT_REPORT_ERRORS; - } - - try { - ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags); - } catch (e) { - return false; - } - return true; - } - - /** - * Safely call JSON.stringify() on an object that comes from an - * extension. - * - * @param {array<any>} args Arguments for JSON.stringify() - * @returns {string} The stringified representation of obj - */ - jsonStringify(...args) { - if (!this.jsonSandbox) { - this.jsonSandbox = Cu.Sandbox(this.principal, { - sameZoneAs: this.cloneScope, - wantXrays: false, - }); - } - - return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args); - } - - callOnClose(obj) { - this.onClose.add(obj); - } - - forgetOnClose(obj) { - this.onClose.delete(obj); - } - - /** - * A wrapper around MessageChannel.sendMessage which adds the extension ID - * to the recipient object, and ensures replies are not processed after the - * context has been unloaded. - * - * @param {nsIMessageManager} target - * @param {string} messageName - * @param {object} data - * @param {object} [options] - * @param {object} [options.sender] - * @param {object} [options.recipient] - * - * @returns {Promise} - */ - sendMessage(target, messageName, data, options = {}) { - options.recipient = Object.assign({extensionId: this.extension.id}, options.recipient); - options.sender = options.sender || {}; - - options.sender.extensionId = this.extension.id; - options.sender.contextId = this.contextId; - - return MessageChannel.sendMessage(target, messageName, data, options); - } - - get lastError() { - this.checkedLastError = true; - return this._lastError; - } - - set lastError(val) { - this.checkedLastError = false; - this._lastError = val; - } - - /** - * Normalizes the given error object for use by the target scope. If - * the target is an error object which belongs to that scope, it is - * returned as-is. If it is an ordinary object with a `message` - * property, it is converted into an error belonging to the target - * scope. If it is an Error object which does *not* belong to the - * clone scope, it is reported, and converted to an unexpected - * exception error. - * - * @param {Error|object} error - * @returns {Error} - */ - normalizeError(error) { - if (error instanceof this.cloneScope.Error) { - return error; - } - let message; - if (instanceOf(error, "Object") || error instanceof ExtensionError) { - message = error.message; - } else if (typeof error == "object" && - this.principal.subsumes(Cu.getObjectPrincipal(error))) { - message = error.message; - } else { - Cu.reportError(error); - } - message = message || "An unexpected error occurred"; - return new this.cloneScope.Error(message); - } - - /** - * Sets the value of `.lastError` to `error`, calls the given - * callback, and reports an error if the value has not been checked - * when the callback returns. - * - * @param {object} error An object with a `message` property. May - * optionally be an `Error` object belonging to the target scope. - * @param {function} callback The callback to call. - * @returns {*} The return value of callback. - */ - withLastError(error, callback) { - this.lastError = this.normalizeError(error); - try { - return callback(); - } finally { - if (!this.checkedLastError) { - Cu.reportError(`Unchecked lastError value: ${this.lastError}`); - } - this.lastError = null; - } - } - - /** - * Wraps the given promise so it can be safely returned to extension - * code in this context. - * - * If `callback` is provided, however, it is used as a completion - * function for the promise, and no promise is returned. In this case, - * the callback is called when the promise resolves or rejects. In the - * latter case, `lastError` is set to the rejection value, and the - * callback function must check `browser.runtime.lastError` or - * `extension.runtime.lastError` in order to prevent it being reported - * to the console. - * - * @param {Promise} promise The promise with which to wrap the - * callback. May resolve to a `SpreadArgs` instance, in which case - * each element will be used as a separate argument. - * - * Unless the promise object belongs to the cloneScope global, its - * resolution value is cloned into cloneScope prior to calling the - * `callback` function or resolving the wrapped promise. - * - * @param {function} [callback] The callback function to wrap - * - * @returns {Promise|undefined} If callback is null, a promise object - * belonging to the target scope. Otherwise, undefined. - */ - wrapPromise(promise, callback = null) { - let runSafe = this.runSafe.bind(this); - if (promise instanceof this.cloneScope.Promise) { - runSafe = this.runSafeWithoutClone.bind(this); - } - - if (callback) { - promise.then( - args => { - if (this.unloaded) { - dump(`Promise resolved after context unloaded\n`); - } else if (!this.active) { - dump(`Promise resolved while context is inactive\n`); - } else if (args instanceof SpreadArgs) { - runSafe(callback, ...args); - } else { - runSafe(callback, args); - } - }, - error => { - this.withLastError(error, () => { - if (this.unloaded) { - dump(`Promise rejected after context unloaded\n`); - } else if (!this.active) { - dump(`Promise rejected while context is inactive\n`); - } else { - this.runSafeWithoutClone(callback); - } - }); - }); - } else { - return new this.cloneScope.Promise((resolve, reject) => { - promise.then( - value => { - if (this.unloaded) { - dump(`Promise resolved after context unloaded\n`); - } else if (!this.active) { - dump(`Promise resolved while context is inactive\n`); - } else if (value instanceof SpreadArgs) { - runSafe(resolve, value.length == 1 ? value[0] : value); - } else { - runSafe(resolve, value); - } - }, - value => { - if (this.unloaded) { - dump(`Promise rejected after context unloaded: ${value && value.message}\n`); - } else if (!this.active) { - dump(`Promise rejected while context is inactive: ${value && value.message}\n`); - } else { - this.runSafeWithoutClone(reject, this.normalizeError(value)); - } - }); - }); - } - } - - unload() { - this.unloaded = true; - - MessageChannel.abortResponses({ - extensionId: this.extension.id, - contextId: this.contextId, - }); - - for (let obj of this.onClose) { - obj.close(); - } - } - - /** - * A simple proxy for unload(), for use with callOnClose(). - */ - close() { - this.unload(); - } -} - -/** - * An object that runs the implementation of a schema API. Instantiations of - * this interfaces are used by Schemas.jsm. - * - * @interface - */ -class SchemaAPIInterface { - /** - * Calls this as a function that returns its return value. - * - * @abstract - * @param {Array} args The parameters for the function. - * @returns {*} The return value of the invoked function. - */ - callFunction(args) { - throw new Error("Not implemented"); - } - - /** - * Calls this as a function and ignores its return value. - * - * @abstract - * @param {Array} args The parameters for the function. - */ - callFunctionNoReturn(args) { - throw new Error("Not implemented"); - } - - /** - * Calls this as a function that completes asynchronously. - * - * @abstract - * @param {Array} args The parameters for the function. - * @param {function(*)} [callback] The callback to be called when the function - * completes. - * @returns {Promise|undefined} Must be void if `callback` is set, and a - * promise otherwise. The promise is resolved when the function completes. - */ - callAsyncFunction(args, callback) { - throw new Error("Not implemented"); - } - - /** - * Retrieves the value of this as a property. - * - * @abstract - * @returns {*} The value of the property. - */ - getProperty() { - throw new Error("Not implemented"); - } - - /** - * Assigns the value to this as property. - * - * @abstract - * @param {string} value The new value of the property. - */ - setProperty(value) { - throw new Error("Not implemented"); - } - - /** - * Registers a `listener` to this as an event. - * - * @abstract - * @param {function} listener The callback to be called when the event fires. - * @param {Array} args Extra parameters for EventManager.addListener. - * @see EventManager.addListener - */ - addListener(listener, args) { - throw new Error("Not implemented"); - } - - /** - * Checks whether `listener` is listening to this as an event. - * - * @abstract - * @param {function} listener The event listener. - * @returns {boolean} Whether `listener` is registered with this as an event. - * @see EventManager.hasListener - */ - hasListener(listener) { - throw new Error("Not implemented"); - } - - /** - * Unregisters `listener` from this as an event. - * - * @abstract - * @param {function} listener The event listener. - * @see EventManager.removeListener - */ - removeListener(listener) { - throw new Error("Not implemented"); - } -} - -/** - * An object that runs a locally implemented API. - */ -class LocalAPIImplementation extends SchemaAPIInterface { - /** - * Constructs an implementation of the `name` method or property of `pathObj`. - * - * @param {object} pathObj The object containing the member with name `name`. - * @param {string} name The name of the implemented member. - * @param {BaseContext} context The context in which the schema is injected. - */ - constructor(pathObj, name, context) { - super(); - this.pathObj = pathObj; - this.name = name; - this.context = context; - } - - callFunction(args) { - return this.pathObj[this.name](...args); - } - - callFunctionNoReturn(args) { - this.pathObj[this.name](...args); - } - - callAsyncFunction(args, callback) { - let promise; - try { - promise = this.pathObj[this.name](...args) || Promise.resolve(); - } catch (e) { - promise = Promise.reject(e); - } - return this.context.wrapPromise(promise, callback); - } - - getProperty() { - return this.pathObj[this.name]; - } - - setProperty(value) { - this.pathObj[this.name] = value; - } - - addListener(listener, args) { - try { - this.pathObj[this.name].addListener.call(null, listener, ...args); - } catch (e) { - throw this.context.normalizeError(e); - } - } - - hasListener(listener) { - return this.pathObj[this.name].hasListener.call(null, listener); - } - - removeListener(listener) { - this.pathObj[this.name].removeListener.call(null, listener); - } -} - -/** - * This object loads the ext-*.js scripts that define the extension API. - * - * This class instance is shared with the scripts that it loads, so that the - * ext-*.js scripts and the instantiator can communicate with each other. - */ -class SchemaAPIManager extends EventEmitter { - /** - * @param {string} processType - * "main" - The main, one and only chrome browser process. - * "addon" - An addon process. - * "content" - A content process. - */ - constructor(processType) { - super(); - this.processType = processType; - this.global = this._createExtGlobal(); - this._scriptScopes = []; - this._schemaApis = { - addon_parent: [], - addon_child: [], - content_parent: [], - content_child: [], - }; - } - - /** - * Create a global object that is used as the shared global for all ext-*.js - * scripts that are loaded via `loadScript`. - * - * @returns {object} A sandbox that is used as the global by `loadScript`. - */ - _createExtGlobal() { - let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { - wantXrays: false, - sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`, - }); - - Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this}); - - XPCOMUtils.defineLazyGetter(global, "console", getConsole); - - XPCOMUtils.defineLazyModuleGetter(global, "require", - "resource://devtools/shared/Loader.jsm"); - - return global; - } - - /** - * Load an ext-*.js script. The script runs in its own scope, if it wishes to - * share state with another script it can assign to the `global` variable. If - * it wishes to communicate with this API manager, use `extensions`. - * - * @param {string} scriptUrl The URL of the ext-*.js script. - */ - loadScript(scriptUrl) { - // Create the object in the context of the sandbox so that the script runs - // in the sandbox's context instead of here. - let scope = Cu.createObjectIn(this.global); - - Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8"); - - // Save the scope to avoid it being garbage collected. - this._scriptScopes.push(scope); - } - - /** - * Called by an ext-*.js script to register an API. - * - * @param {string} namespace The API namespace. - * Intended to match the namespace of the generated API, but not used at - * the moment - see bugzil.la/1295774. - * @param {string} envType Restricts the API to contexts that run in the - * given environment. Must be one of the following: - * - "addon_parent" - addon APIs that runs in the main process. - * - "addon_child" - addon APIs that runs in an addon process. - * - "content_parent" - content script APIs that runs in the main process. - * - "content_child" - content script APIs that runs in a content process. - * @param {function(BaseContext)} getAPI A function that returns an object - * that will be merged with |chrome| and |browser|. The next example adds - * the create, update and remove methods to the tabs API. - * - * registerSchemaAPI("tabs", "addon_parent", (context) => ({ - * tabs: { create, update }, - * })); - * registerSchemaAPI("tabs", "addon_parent", (context) => ({ - * tabs: { remove }, - * })); - */ - registerSchemaAPI(namespace, envType, getAPI) { - this._schemaApis[envType].push({namespace, getAPI}); - } - - /** - * Exports all registered scripts to `obj`. - * - * @param {BaseContext} context The context for which the API bindings are - * generated. - * @param {object} obj The destination of the API. - */ - generateAPIs(context, obj) { - let apis = this._schemaApis[context.envType]; - if (!apis) { - Cu.reportError(`No APIs have been registered for ${context.envType}`); - return; - } - SchemaAPIManager.generateAPIs(context, apis, obj); - } - - /** - * Mash together all the APIs from `apis` into `obj`. - * - * @param {BaseContext} context The context for which the API bindings are - * generated. - * @param {Array} apis A list of objects, see `registerSchemaAPI`. - * @param {object} obj The destination of the API. - */ - static generateAPIs(context, apis, obj) { - // Recursively copy properties from source to dest. - function copy(dest, source) { - for (let prop in source) { - let desc = Object.getOwnPropertyDescriptor(source, prop); - if (typeof(desc.value) == "object") { - if (!(prop in dest)) { - dest[prop] = {}; - } - copy(dest[prop], source[prop]); - } else { - Object.defineProperty(dest, prop, desc); - } - } - } - - for (let api of apis) { - if (Schemas.checkPermissions(api.namespace, context.extension)) { - api = api.getAPI(context); - copy(obj, api); - } - } - } -} - -const ExtensionCommon = { - BaseContext, - LocalAPIImplementation, - SchemaAPIInterface, - SchemaAPIManager, -}; diff --git a/toolkit/components/webextensions/ExtensionContent.jsm b/toolkit/components/webextensions/ExtensionContent.jsm deleted file mode 100644 index 5f9b88f35..000000000 --- a/toolkit/components/webextensions/ExtensionContent.jsm +++ /dev/null @@ -1,1050 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionContent"]; - -/* globals ExtensionContent */ - -/* - * This file handles the content process side of extensions. It mainly - * takes care of content script injection, content script APIs, and - * messaging. - * - * This file is also the initial entry point for addon processes. - * ExtensionChild.jsm is responsible for functionality specific to addon - * processes. - */ - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/AppConstants.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector", - "resource:///modules/translation/LanguageDetector.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", - "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", - "resource://gre/modules/PromiseUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames", - "resource://gre/modules/WebNavigationFrames.jsm"); - -Cu.import("resource://gre/modules/ExtensionChild.jsm"); -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -const { - EventEmitter, - LocaleData, - defineLazyGetter, - flushJarCache, - getInnerWindowID, - promiseDocumentReady, - runSafeSyncWithoutClone, -} = ExtensionUtils; - -const { - BaseContext, - SchemaAPIManager, -} = ExtensionCommon; - -const { - ChildAPIManager, - Messenger, -} = ExtensionChild; - -XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole); - -const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content"; - -function isWhenBeforeOrSame(when1, when2) { - let table = {"document_start": 0, - "document_end": 1, - "document_idle": 2}; - return table[when1] <= table[when2]; -} - -var apiManager = new class extends SchemaAPIManager { - constructor() { - super("content"); - this.initialized = false; - } - - generateAPIs(...args) { - if (!this.initialized) { - this.initialized = true; - for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) { - this.loadScript(value); - } - } - return super.generateAPIs(...args); - } - - registerSchemaAPI(namespace, envType, getAPI) { - if (envType == "content_child") { - super.registerSchemaAPI(namespace, envType, getAPI); - } - } -}(); - -// Represents a content script. -function Script(extension, options, deferred = PromiseUtils.defer()) { - this.extension = extension; - this.options = options; - this.run_at = this.options.run_at; - this.js = this.options.js || []; - this.css = this.options.css || []; - this.remove_css = this.options.remove_css; - this.match_about_blank = this.options.match_about_blank; - - this.deferred = deferred; - - this.matches_ = new MatchPattern(this.options.matches); - this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null); - // TODO: MatchPattern should pre-mangle host-only patterns so that we - // don't need to call a separate match function. - this.matches_host_ = new MatchPattern(this.options.matchesHost || null); - this.include_globs_ = new MatchGlobs(this.options.include_globs); - this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs); - - this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode); -} - -Script.prototype = { - get cssURLs() { - // We can handle CSS urls (css) and CSS code (cssCode). - let urls = []; - for (let url of this.css) { - urls.push(this.extension.baseURI.resolve(url)); - } - - if (this.options.cssCode) { - let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode); - urls.push(url); - } - - return urls; - }, - - matches(window) { - let uri = window.document.documentURIObject; - let principal = window.document.nodePrincipal; - - // If mozAddonManager is present on this page, don't allow - // content scripts. - if (window.navigator.mozAddonManager !== undefined) { - return false; - } - - if (this.match_about_blank && ["about:blank", "about:srcdoc"].includes(uri.spec)) { - // When matching about:blank/srcdoc documents, the checks below - // need to be performed against the "owner" document's URI. - uri = principal.URI; - } - - // Documents from data: URIs also inherit the principal. - if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) { - if (!this.match_about_blank) { - return false; - } - uri = principal.URI; - } - - if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) { - return false; - } - - if (this.exclude_matches_.matches(uri)) { - return false; - } - - if (this.options.include_globs != null) { - if (!this.include_globs_.matches(uri.spec)) { - return false; - } - } - - if (this.exclude_globs_.matches(uri.spec)) { - return false; - } - - if (this.options.frame_id != null) { - if (WebNavigationFrames.getFrameId(window) != this.options.frame_id) { - return false; - } - } else if (!this.options.all_frames && window.top != window) { - return false; - } - - return true; - }, - - cleanup(window) { - if (!this.remove_css) { - let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - - for (let url of this.cssURLs) { - runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, winUtils.AUTHOR_SHEET); - } - } - }, - - /** - * Tries to inject this script into the given window and sandbox, if - * there are pending operations for the window's current load state. - * - * @param {Window} window - * The DOM Window to inject the scripts and CSS into. - * @param {Sandbox} sandbox - * A Sandbox inheriting from `window` in which to evaluate the - * injected scripts. - * @param {function} shouldRun - * A function which, when passed the document load state that a - * script is expected to run at, returns `true` if we should - * currently be injecting scripts for that load state. - * - * For initial injection of a script, this function should - * return true if the document is currently in or has already - * passed through the given state. For injections triggered by - * document state changes, it should only return true if the - * given state exactly matches the state that triggered the - * change. - * @param {string} when - * The document's current load state, or if triggered by a - * document state change, the new document state that triggered - * the injection. - */ - tryInject(window, sandbox, shouldRun, when) { - if (shouldRun("document_start")) { - let {cssURLs} = this; - if (cssURLs.length > 0) { - let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - - let method = this.remove_css ? winUtils.removeSheetUsingURIString : winUtils.loadSheetUsingURIString; - for (let url of cssURLs) { - runSafeSyncWithoutClone(method, url, winUtils.AUTHOR_SHEET); - } - - this.deferred.resolve(); - } - } - - let result; - let scheduled = this.run_at || "document_idle"; - if (shouldRun(scheduled)) { - for (let [i, url] of this.js.entries()) { - let options = { - target: sandbox, - charset: "UTF-8", - // Inject the last script asynchronously unless we're expected to - // inject before any page scripts have run, and we haven't already - // missed that boat. - async: (i === this.js.length - 1) && - (this.run_at !== "document_start" || when !== "document_start"), - }; - try { - result = Services.scriptloader.loadSubScriptWithOptions(url, options); - } catch (e) { - Cu.reportError(e); - this.deferred.reject(e); - } - } - - if (this.options.jsCode) { - try { - result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest"); - } catch (e) { - Cu.reportError(e); - this.deferred.reject(e); - } - } - - this.deferred.resolve(result); - } - }, -}; - -function getWindowMessageManager(contentWindow) { - let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor); - try { - return ir.getInterface(Ci.nsIContentFrameMessageManager); - } catch (e) { - // Some windows don't support this interface (hidden window). - return null; - } -} - -var DocumentManager; -var ExtensionManager; - -/** - * An execution context for semi-privileged extension content scripts. - * - * This is the child side of the ContentScriptContextParent class - * defined in ExtensionParent.jsm. - */ -class ContentScriptContextChild extends BaseContext { - constructor(extension, contentWindow, contextOptions = {}) { - super("content_child", extension); - - let {isExtensionPage} = contextOptions; - - this.isExtensionPage = isExtensionPage; - - this.setContentWindow(contentWindow); - - let frameId = WebNavigationFrames.getFrameId(contentWindow); - this.frameId = frameId; - - this.scripts = []; - - let contentPrincipal = contentWindow.document.nodePrincipal; - let ssm = Services.scriptSecurityManager; - - // copy origin attributes from the content window origin attributes to - // preserve the user context id. overwrite the addonId. - let attrs = contentPrincipal.originAttributes; - attrs.addonId = this.extension.id; - let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs); - - let principal; - if (ssm.isSystemPrincipal(contentPrincipal)) { - // Make sure we don't hand out the system principal by accident. - // also make sure that the null principal has the right origin attributes - principal = ssm.createNullPrincipal(attrs); - } else { - principal = [contentPrincipal, extensionPrincipal]; - } - - if (isExtensionPage) { - if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) { - throw new Error("Invalid target window for this extension context"); - } - // This is an iframe with content script API enabled and its principal should be the - // contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled - // because it enables us to create the APIs object in this sandbox object and then copying it - // into the iframe's window, see Bug 1214658 for rationale) - this.sandbox = Cu.Sandbox(contentWindow, { - sandboxName: `Web-Accessible Extension Page ${this.extension.id}`, - sandboxPrototype: contentWindow, - sameZoneAs: contentWindow, - wantXrays: false, - isWebExtensionContentScript: true, - }); - } else { - // This metadata is required by the Developer Tools, in order for - // the content script to be associated with both the extension and - // the tab holding the content page. - let metadata = { - "inner-window-id": this.innerWindowID, - addonId: attrs.addonId, - }; - - this.sandbox = Cu.Sandbox(principal, { - metadata, - sandboxName: `Content Script ${this.extension.id}`, - sandboxPrototype: contentWindow, - sameZoneAs: contentWindow, - wantXrays: true, - isWebExtensionContentScript: true, - wantExportHelpers: true, - wantGlobalProperties: ["XMLHttpRequest", "fetch"], - originAttributes: attrs, - }); - - Cu.evalInSandbox(` - window.JSON = JSON; - window.XMLHttpRequest = XMLHttpRequest; - window.fetch = fetch; - `, this.sandbox); - } - - Object.defineProperty(this, "principal", { - value: Cu.getObjectPrincipal(this.sandbox), - enumerable: true, - configurable: true, - }); - - this.url = contentWindow.location.href; - - defineLazyGetter(this, "chromeObj", () => { - let chromeObj = Cu.createObjectIn(this.sandbox); - - Schemas.inject(chromeObj, this.childManager); - return chromeObj; - }); - - Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj); - Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj); - - // This is an iframe with content script API enabled (bug 1214658) - if (isExtensionPage) { - Schemas.exportLazyGetter(this.contentWindow, - "browser", () => this.chromeObj); - Schemas.exportLazyGetter(this.contentWindow, - "chrome", () => this.chromeObj); - } - } - - get cloneScope() { - return this.sandbox; - } - - execute(script, shouldRun, when) { - script.tryInject(this.contentWindow, this.sandbox, shouldRun, when); - } - - addScript(script, when) { - let state = DocumentManager.getWindowState(this.contentWindow); - this.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state), when); - - // Save the script in case it has pending operations in later load - // states, but only if we're before document_idle, or require cleanup. - if (state != "document_idle" || script.requiresCleanup) { - this.scripts.push(script); - } - } - - triggerScripts(documentState) { - for (let script of this.scripts) { - this.execute(script, scheduled => scheduled == documentState, documentState); - } - if (documentState == "document_idle") { - // Don't bother saving scripts after document_idle. - this.scripts = this.scripts.filter(script => script.requiresCleanup); - } - } - - close() { - super.unload(); - - if (this.contentWindow) { - for (let script of this.scripts) { - if (script.requiresCleanup) { - script.cleanup(this.contentWindow); - } - } - - // Overwrite the content script APIs with an empty object if the APIs objects are still - // defined in the content window (bug 1214658). - if (this.isExtensionPage) { - Cu.createObjectIn(this.contentWindow, {defineAs: "browser"}); - Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"}); - } - } - Cu.nukeSandbox(this.sandbox); - this.sandbox = null; - } -} - -defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() { - // The |sender| parameter is passed directly to the extension. - let sender = {id: this.extension.id, frameId: this.frameId, url: this.url}; - let filter = {extensionId: this.extension.id}; - let optionalFilter = {frameId: this.frameId}; - - return new Messenger(this, [this.messageManager], sender, filter, optionalFilter); -}); - -defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() { - let localApis = {}; - apiManager.generateAPIs(this, localApis); - - let childManager = new ChildAPIManager(this, this.messageManager, localApis, { - envType: "content_parent", - url: this.url, - }); - - this.callOnClose(childManager); - - return childManager; -}); - -// Responsible for creating ExtensionContexts and injecting content -// scripts into them when new documents are created. -DocumentManager = { - extensionCount: 0, - - // Map[windowId -> Map[extensionId -> ContentScriptContextChild]] - contentScriptWindows: new Map(), - - // Map[windowId -> ContentScriptContextChild] - extensionPageWindows: new Map(), - - init() { - Services.obs.addObserver(this, "content-document-global-created", false); - Services.obs.addObserver(this, "document-element-inserted", false); - Services.obs.addObserver(this, "inner-window-destroyed", false); - }, - - uninit() { - Services.obs.removeObserver(this, "content-document-global-created"); - Services.obs.removeObserver(this, "document-element-inserted"); - Services.obs.removeObserver(this, "inner-window-destroyed"); - }, - - getWindowState(contentWindow) { - let readyState = contentWindow.document.readyState; - if (readyState == "complete") { - return "document_idle"; - } - if (readyState == "interactive") { - return "document_end"; - } - return "document_start"; - }, - - loadInto(window) { - // Enable the content script APIs should be available in subframes' window - // if it is recognized as a valid addon id (see Bug 1214658 for rationale). - const { - NO_PRIVILEGES, - CONTENTSCRIPT_PRIVILEGES, - FULL_PRIVILEGES, - } = ExtensionManagement.API_LEVELS; - let extensionId = ExtensionManagement.getAddonIdForWindow(window); - let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId); - - if (apiLevel != NO_PRIVILEGES) { - let extension = ExtensionManager.get(extensionId); - if (extension) { - if (apiLevel == CONTENTSCRIPT_PRIVILEGES) { - DocumentManager.getExtensionPageContext(extension, window); - } else if (apiLevel == FULL_PRIVILEGES) { - ExtensionChild.createExtensionContext(extension, window); - } - } - } - }, - - observe: function(subject, topic, data) { - // For some types of documents (about:blank), we only see the first - // notification, for others (data: URIs) we only observe the second. - if (topic == "content-document-global-created" || topic == "document-element-inserted") { - let document = subject; - let window = document && document.defaultView; - - if (topic == "content-document-global-created") { - window = subject; - document = window && window.document; - } - - if (!document || !document.location || !window) { - return; - } - - // Make sure we only load into frames that ExtensionContent.init - // was called on (i.e., not frames for social or sidebars). - let mm = getWindowMessageManager(window); - if (!mm || !ExtensionContent.globals.has(mm)) { - return; - } - - // Load on document-element-inserted, except for about:blank which doesn't - // see it, and needs special late handling on DOMContentLoaded event. - if (topic === "document-element-inserted") { - this.loadInto(window); - this.trigger("document_start", window); - } - - /* eslint-disable mozilla/balanced-listeners */ - window.addEventListener("DOMContentLoaded", this, true); - window.addEventListener("load", this, true); - /* eslint-enable mozilla/balanced-listeners */ - } else if (topic == "inner-window-destroyed") { - let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - - MessageChannel.abortResponses({innerWindowID: windowId}); - - // Close any existent content-script context for the destroyed window. - if (this.contentScriptWindows.has(windowId)) { - let extensions = this.contentScriptWindows.get(windowId); - for (let [, context] of extensions) { - context.close(); - } - - this.contentScriptWindows.delete(windowId); - } - - // Close any existent iframe extension page context for the destroyed window. - if (this.extensionPageWindows.has(windowId)) { - let context = this.extensionPageWindows.get(windowId); - context.close(); - this.extensionPageWindows.delete(windowId); - } - - ExtensionChild.destroyExtensionContext(windowId); - } - }, - - handleEvent: function(event) { - let window = event.currentTarget; - if (event.target != window.document) { - // We use capturing listeners so we have precedence over content script - // listeners, but only care about events targeted to the element we're - // listening on. - return; - } - window.removeEventListener(event.type, this, true); - - // Need to check if we're still on the right page? Greasemonkey does this. - - if (event.type == "DOMContentLoaded") { - // By this time, we can be sure if this is an explicit about:blank - // document, and if it needs special late loading and fake trigger. - if (window.location.href === "about:blank") { - this.loadInto(window); - this.trigger("document_start", window); - } - this.trigger("document_end", window); - } else if (event.type == "load") { - this.trigger("document_idle", window); - } - }, - - // Used to executeScript, insertCSS and removeCSS. - executeScript(global, extensionId, options) { - let extension = ExtensionManager.get(extensionId); - - let executeInWin = (window) => { - let deferred = PromiseUtils.defer(); - let script = new Script(extension, options, deferred); - - if (script.matches(window)) { - let context = this.getContentScriptContext(extension, window); - context.addScript(script); - return deferred.promise; - } - return null; - }; - - let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin) - .filter(promise => promise); - - if (!promises.length) { - let details = {}; - for (let key of ["all_frames", "frame_id", "matches_about_blank", "matchesHost"]) { - if (key in options) { - details[key] = options[key]; - } - } - - return Promise.reject({message: `No window matching ${JSON.stringify(details)}`}); - } - if (!options.all_frames && promises.length > 1) { - return Promise.reject({message: `Internal error: Script matched multiple windows`}); - } - return Promise.all(promises); - }, - - enumerateWindows: function* (docShell) { - let window = docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - yield window; - - for (let i = 0; i < docShell.childCount; i++) { - let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell); - yield* this.enumerateWindows(child); - } - }, - - getContentScriptGlobalsForWindow(window) { - let winId = getInnerWindowID(window); - let extensions = this.contentScriptWindows.get(winId); - - if (extensions) { - return Array.from(extensions.values(), ctx => ctx.sandbox); - } - - return []; - }, - - getContentScriptContext(extension, window) { - let winId = getInnerWindowID(window); - if (!this.contentScriptWindows.has(winId)) { - this.contentScriptWindows.set(winId, new Map()); - } - - let extensions = this.contentScriptWindows.get(winId); - if (!extensions.has(extension.id)) { - let context = new ContentScriptContextChild(extension, window); - extensions.set(extension.id, context); - } - - return extensions.get(extension.id); - }, - - getExtensionPageContext(extension, window) { - let winId = getInnerWindowID(window); - - let context = this.extensionPageWindows.get(winId); - if (!context) { - let context = new ContentScriptContextChild(extension, window, {isExtensionPage: true}); - this.extensionPageWindows.set(winId, context); - } - - return context; - }, - - startupExtension(extensionId) { - if (this.extensionCount == 0) { - this.init(); - } - this.extensionCount++; - - let extension = ExtensionManager.get(extensionId); - for (let global of ExtensionContent.globals.keys()) { - // Note that we miss windows in the bfcache here. In theory we - // could execute content scripts on a pageshow event for that - // window, but that seems extreme. - for (let window of this.enumerateWindows(global.docShell)) { - for (let script of extension.scripts) { - if (script.matches(window)) { - let context = this.getContentScriptContext(extension, window); - context.addScript(script); - } - } - } - } - }, - - shutdownExtension(extensionId) { - // Clean up content-script contexts on extension shutdown. - for (let [, extensions] of this.contentScriptWindows) { - let context = extensions.get(extensionId); - if (context) { - context.close(); - extensions.delete(extensionId); - } - } - - // Clean up iframe extension page contexts on extension shutdown. - for (let [winId, context] of this.extensionPageWindows) { - if (context.extension.id == extensionId) { - context.close(); - this.extensionPageWindows.delete(winId); - } - } - - ExtensionChild.shutdownExtension(extensionId); - - MessageChannel.abortResponses({extensionId}); - - this.extensionCount--; - if (this.extensionCount == 0) { - this.uninit(); - } - }, - - trigger(when, window) { - if (when === "document_start") { - for (let extension of ExtensionManager.extensions.values()) { - for (let script of extension.scripts) { - if (script.matches(window)) { - let context = this.getContentScriptContext(extension, window); - context.addScript(script, when); - } - } - } - } else { - let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map(); - for (let context of contexts.values()) { - context.triggerScripts(when); - } - } - }, -}; - -// Represents a browser extension in the content process. -class BrowserExtensionContent extends EventEmitter { - constructor(data) { - super(); - - this.id = data.id; - this.uuid = data.uuid; - this.data = data; - this.instanceId = data.instanceId; - - this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`; - Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this); - - this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData)); - this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources); - this.whiteListedHosts = new MatchPattern(data.whiteListedHosts); - this.permissions = data.permissions; - this.principal = data.principal; - - this.localeData = new LocaleData(data.localeData); - - this.manifest = data.manifest; - this.baseURI = Services.io.newURI(data.baseURL, null, null); - - // Only used in addon processes. - this.views = new Set(); - - let uri = Services.io.newURI(data.resourceURL, null, null); - - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { - // Extension.jsm takes care of this in the parent. - ExtensionManagement.startupExtension(this.uuid, uri, this); - } - } - - shutdown() { - Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this); - - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { - ExtensionManagement.shutdownExtension(this.uuid); - } - } - - emit(event, ...args) { - Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args}); - - super.emit(event, ...args); - } - - receiveMessage({name, data}) { - if (name === this.MESSAGE_EMIT_EVENT) { - super.emit(data.event, ...data.args); - } - } - - localizeMessage(...args) { - return this.localeData.localizeMessage(...args); - } - - localize(...args) { - return this.localeData.localize(...args); - } - - hasPermission(perm) { - let match = /^manifest:(.*)/.exec(perm); - if (match) { - return this.manifest[match[1]] != null; - } - return this.permissions.has(perm); - } -} - -ExtensionManager = { - // Map[extensionId, BrowserExtensionContent] - extensions: new Map(), - - init() { - Schemas.init(); - ExtensionChild.initOnce(); - - Services.cpmm.addMessageListener("Extension:Startup", this); - Services.cpmm.addMessageListener("Extension:Shutdown", this); - Services.cpmm.addMessageListener("Extension:FlushJarCache", this); - - if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) { - let extensions = Services.cpmm.initialProcessData["Extension:Extensions"]; - for (let data of extensions) { - this.extensions.set(data.id, new BrowserExtensionContent(data)); - DocumentManager.startupExtension(data.id); - } - } - }, - - get(extensionId) { - return this.extensions.get(extensionId); - }, - - receiveMessage({name, data}) { - let extension; - switch (name) { - case "Extension:Startup": { - extension = new BrowserExtensionContent(data); - - this.extensions.set(data.id, extension); - - DocumentManager.startupExtension(data.id); - - Services.cpmm.sendAsyncMessage("Extension:StartupComplete"); - break; - } - - case "Extension:Shutdown": { - extension = this.extensions.get(data.id); - extension.shutdown(); - - DocumentManager.shutdownExtension(data.id); - - this.extensions.delete(data.id); - break; - } - - case "Extension:FlushJarCache": { - let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", - "initWithPath"); - let file = new nsIFile(data.path); - flushJarCache(file); - Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete"); - break; - } - } - }, -}; - -class ExtensionGlobal { - constructor(global) { - this.global = global; - - MessageChannel.addListener(global, "Extension:Capture", this); - MessageChannel.addListener(global, "Extension:DetectLanguage", this); - MessageChannel.addListener(global, "Extension:Execute", this); - MessageChannel.addListener(global, "WebNavigation:GetFrame", this); - MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this); - - this.windowId = global.content - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .outerWindowID; - - global.sendAsyncMessage("Extension:TopWindowID", {windowId: this.windowId}); - } - - uninit() { - this.global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId: this.windowId}); - } - - get messageFilterStrict() { - return { - innerWindowID: getInnerWindowID(this.global.content), - }; - } - - receiveMessage({target, messageName, recipient, data}) { - switch (messageName) { - case "Extension:Capture": - return this.handleExtensionCapture(data.width, data.height, data.options); - case "Extension:DetectLanguage": - return this.handleDetectLanguage(target); - case "Extension:Execute": - return this.handleExtensionExecute(target, recipient.extensionId, data.options); - case "WebNavigation:GetFrame": - return this.handleWebNavigationGetFrame(data.options); - case "WebNavigation:GetAllFrames": - return this.handleWebNavigationGetAllFrames(); - } - } - - handleExtensionCapture(width, height, options) { - let win = this.global.content; - - const XHTML_NS = "http://www.w3.org/1999/xhtml"; - let canvas = win.document.createElementNS(XHTML_NS, "canvas"); - canvas.width = width; - canvas.height = height; - canvas.mozOpaque = true; - - let ctx = canvas.getContext("2d"); - - // We need to scale the image to the visible size of the browser, - // in order for the result to appear as the user sees it when - // settings like full zoom come into play. - ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight); - - ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff"); - - return canvas.toDataURL(`image/${options.format}`, options.quality / 100); - } - - handleDetectLanguage(target) { - let doc = target.content.document; - - return promiseDocumentReady(doc).then(() => { - let elem = doc.documentElement; - - let language = (elem.getAttribute("xml:lang") || elem.getAttribute("lang") || - doc.contentLanguage || null); - - // We only want the last element of the TLD here. - // Only country codes have any effect on the results, but other - // values cause no harm. - let tld = doc.location.hostname.match(/[a-z]*$/)[0]; - - // The CLD2 library used by the language detector is capable of - // analyzing raw HTML. Unfortunately, that takes much more memory, - // and since it's hosted by emscripten, and therefore can't shrink - // its heap after it's grown, it has a performance cost. - // So we send plain text instead. - let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"].createInstance(Ci.nsIDocumentEncoder); - encoder.init(doc, "text/plain", encoder.SkipInvisibleContent); - let text = encoder.encodeToStringWithMaxLength(60 * 1024); - - let encoding = doc.characterSet; - - return LanguageDetector.detectLanguage({language, tld, text, encoding}) - .then(result => result.language === "un" ? "und" : result.language); - }); - } - - // Used to executeScript, insertCSS and removeCSS. - handleExtensionExecute(target, extensionId, options) { - return DocumentManager.executeScript(target, extensionId, options).then(result => { - try { - // Make sure we can structured-clone the result value before - // we try to send it back over the message manager. - Cu.cloneInto(result, target); - } catch (e) { - return Promise.reject({message: "Script returned non-structured-clonable data"}); - } - return result; - }); - } - - handleWebNavigationGetFrame({frameId}) { - return WebNavigationFrames.getFrame(this.global.docShell, frameId); - } - - handleWebNavigationGetAllFrames() { - return WebNavigationFrames.getAllFrames(this.global.docShell); - } -} - -this.ExtensionContent = { - globals: new Map(), - - init(global) { - this.globals.set(global, new ExtensionGlobal(global)); - ExtensionChild.init(global); - }, - - uninit(global) { - ExtensionChild.uninit(global); - this.globals.get(global).uninit(); - this.globals.delete(global); - }, - - // This helper is exported to be integrated in the devtools RDP actors, - // that can use it to retrieve the existent WebExtensions ContentScripts - // of a target window and be able to show the ContentScripts source in the - // DevTools Debugger panel. - getContentScriptGlobalsForWindow(window) { - return DocumentManager.getContentScriptGlobalsForWindow(window); - }, -}; - -ExtensionManager.init(); diff --git a/toolkit/components/webextensions/ExtensionManagement.jsm b/toolkit/components/webextensions/ExtensionManagement.jsm deleted file mode 100644 index 324c5b71b..000000000 --- a/toolkit/components/webextensions/ExtensionManagement.jsm +++ /dev/null @@ -1,321 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionManagement"]; - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.import("resource://gre/modules/AppConstants.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", - "resource://gre/modules/ExtensionUtils.jsm"); - -XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole()); - -XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => { - let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {}); - return UUIDMap; -}); - -/* - * This file should be kept short and simple since it's loaded even - * when no extensions are running. - */ - -// Keep track of frame IDs for content windows. Mostly we can just use -// the outer window ID as the frame ID. However, the API specifies -// that top-level windows have a frame ID of 0. So we need to keep -// track of which windows are top-level. This code listens to messages -// from ExtensionContent to do that. -var Frames = { - // Window IDs of top-level content windows. - topWindowIds: new Set(), - - init() { - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { - return; - } - - Services.mm.addMessageListener("Extension:TopWindowID", this); - Services.mm.addMessageListener("Extension:RemoveTopWindowID", this, true); - }, - - isTopWindowId(windowId) { - return this.topWindowIds.has(windowId); - }, - - // Convert an outer window ID to a frame ID. An outer window ID of 0 - // is invalid. - getId(windowId) { - if (this.isTopWindowId(windowId)) { - return 0; - } - if (windowId == 0) { - return -1; - } - return windowId; - }, - - // Convert an outer window ID for a parent window to a frame - // ID. Outer window IDs follow the same convention that - // |window.top.parent === window.top|. The API works differently, - // giving a frame ID of -1 for the the parent of a top-level - // window. This function handles the conversion. - getParentId(parentWindowId, windowId) { - if (parentWindowId == windowId) { - // We have a top-level window. - return -1; - } - - // Not a top-level window. Just return the ID as normal. - return this.getId(parentWindowId); - }, - - receiveMessage({name, data}) { - switch (name) { - case "Extension:TopWindowID": - // FIXME: Need to handle the case where the content process - // crashes. Right now we leak its top window IDs. - this.topWindowIds.add(data.windowId); - break; - - case "Extension:RemoveTopWindowID": - this.topWindowIds.delete(data.windowId); - break; - } - }, -}; -Frames.init(); - -var APIs = { - apis: new Map(), - - register(namespace, schema, script) { - if (this.apis.has(namespace)) { - throw new Error(`API namespace already exists: ${namespace}`); - } - - this.apis.set(namespace, {schema, script}); - }, - - unregister(namespace) { - if (!this.apis.has(namespace)) { - throw new Error(`API namespace does not exist: ${namespace}`); - } - - this.apis.delete(namespace); - }, -}; - -function getURLForExtension(id, path = "") { - let uuid = UUIDMap.get(id, false); - if (!uuid) { - Cu.reportError(`Called getURLForExtension on unmapped extension ${id}`); - return null; - } - return `moz-extension://${uuid}/${path}`; -} - -// This object manages various platform-level issues related to -// moz-extension:// URIs. It lives here so that it can be used in both -// the parent and child processes. -// -// moz-extension URIs have the form moz-extension://uuid/path. Each -// extension has its own UUID, unique to the machine it's installed -// on. This is easier and more secure than using the extension ID, -// since it makes it slightly harder to fingerprint for extensions if -// each user uses different URIs for the extension. -var Service = { - initialized: false, - - // Map[uuid -> extension]. - // extension can be an Extension (parent process) or BrowserExtensionContent (child process). - uuidMap: new Map(), - - init() { - let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService); - aps = aps.wrappedJSObject; - this.aps = aps; - aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this)); - aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this)); - }, - - // Called when a new extension is loaded. - startupExtension(uuid, uri, extension) { - if (!this.initialized) { - this.initialized = true; - this.init(); - } - - // Create the moz-extension://uuid mapping. - let handler = Services.io.getProtocolHandler("moz-extension"); - handler.QueryInterface(Ci.nsISubstitutingProtocolHandler); - handler.setSubstitution(uuid, uri); - - this.uuidMap.set(uuid, extension); - this.aps.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension)); - this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension)); - this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension)); - this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy); - this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension)); - }, - - // Called when an extension is unloaded. - shutdownExtension(uuid) { - let extension = this.uuidMap.get(uuid); - this.uuidMap.delete(uuid); - this.aps.setAddonHasPermissionCallback(extension.id, null); - this.aps.setAddonLoadURICallback(extension.id, null); - this.aps.setAddonLocalizeCallback(extension.id, null); - this.aps.setAddonCSP(extension.id, null); - this.aps.setBackgroundPageUrlCallback(uuid, null); - - let handler = Services.io.getProtocolHandler("moz-extension"); - handler.QueryInterface(Ci.nsISubstitutingProtocolHandler); - handler.setSubstitution(uuid, null); - }, - - // Return true if the given URI can be loaded from arbitrary web - // content. The manifest.json |web_accessible_resources| directive - // determines this. - extensionURILoadableByAnyone(uri) { - let uuid = uri.host; - let extension = this.uuidMap.get(uuid); - if (!extension || !extension.webAccessibleResources) { - return false; - } - - let path = uri.QueryInterface(Ci.nsIURL).filePath; - if (path.length > 0 && path[0] == "/") { - path = path.substr(1); - } - return extension.webAccessibleResources.matches(path); - }, - - // Checks whether a given extension can load this URI (typically via - // an XML HTTP request). The manifest.json |permissions| directive - // determines this. - checkAddonMayLoad(extension, uri) { - return extension.whiteListedHosts.matchesIgnoringPath(uri); - }, - - generateBackgroundPageUrl(extension) { - let background_scripts = extension.manifest.background && - extension.manifest.background.scripts; - if (!background_scripts) { - return; - } - let html = "<!DOCTYPE html>\n<body>\n"; - for (let script of background_scripts) { - script = script.replace(/"/g, """); - html += `<script src="${script}"></script>\n`; - } - html += "</body>\n</html>\n"; - return "data:text/html;charset=utf-8," + encodeURIComponent(html); - }, - - // Finds the add-on ID associated with a given moz-extension:// URI. - // This is used to set the addonId on the originAttributes for the - // nsIPrincipal attached to the URI. - extensionURIToAddonID(uri) { - let uuid = uri.host; - let extension = this.uuidMap.get(uuid); - return extension ? extension.id : undefined; - }, -}; - -// API Levels Helpers - -// Find the add-on associated with this document via the -// principal's originAttributes. This value is computed by -// extensionURIToAddonID, which ensures that we don't inject our -// API into webAccessibleResources or remote web pages. -function getAddonIdForWindow(window) { - return Cu.getObjectPrincipal(window).originAttributes.addonId; -} - -const API_LEVELS = Object.freeze({ - NO_PRIVILEGES: 0, - CONTENTSCRIPT_PRIVILEGES: 1, - FULL_PRIVILEGES: 2, -}); - -// Finds the API Level ("FULL_PRIVILEGES", "CONTENTSCRIPT_PRIVILEGES", "NO_PRIVILEGES") -// with a given a window object. -function getAPILevelForWindow(window, addonId) { - const {NO_PRIVILEGES, CONTENTSCRIPT_PRIVILEGES, FULL_PRIVILEGES} = API_LEVELS; - - // Non WebExtension URLs and WebExtension URLs from a different extension - // has no access to APIs. - if (!addonId || getAddonIdForWindow(window) != addonId) { - return NO_PRIVILEGES; - } - - // Extension pages running in the content process always defaults to - // "content script API level privileges". - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { - return CONTENTSCRIPT_PRIVILEGES; - } - - let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell); - - // Handling of ExtensionPages running inside sub-frames. - if (docShell.sameTypeParent) { - let parentWindow = docShell.sameTypeParent.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - - // The option page iframe embedded in the about:addons tab should have - // full API level privileges. (see Bug 1256282 for rationale) - let parentDocument = parentWindow.document; - let parentIsSystemPrincipal = Services.scriptSecurityManager - .isSystemPrincipal(parentDocument.nodePrincipal); - if (parentDocument.location.href == "about:addons" && parentIsSystemPrincipal) { - return FULL_PRIVILEGES; - } - - // The addon iframes embedded in a addon page from with the same addonId - // should have the same privileges of the sameTypeParent. - // (see Bug 1258347 for rationale) - let parentSameAddonPrivileges = getAPILevelForWindow(parentWindow, addonId); - if (parentSameAddonPrivileges > NO_PRIVILEGES) { - return parentSameAddonPrivileges; - } - - // In all the other cases, WebExtension URLs loaded into sub-frame UI - // will have "content script API level privileges". - // (see Bug 1214658 for rationale) - return CONTENTSCRIPT_PRIVILEGES; - } - - // WebExtension URLs loaded into top frames UI could have full API level privileges. - return FULL_PRIVILEGES; -} - -this.ExtensionManagement = { - startupExtension: Service.startupExtension.bind(Service), - shutdownExtension: Service.shutdownExtension.bind(Service), - - registerAPI: APIs.register.bind(APIs), - unregisterAPI: APIs.unregister.bind(APIs), - - getFrameId: Frames.getId.bind(Frames), - getParentFrameId: Frames.getParentId.bind(Frames), - - getURLForExtension, - - // exported API Level Helpers - getAddonIdForWindow, - getAPILevelForWindow, - API_LEVELS, - - APIs, -}; diff --git a/toolkit/components/webextensions/ExtensionParent.jsm b/toolkit/components/webextensions/ExtensionParent.jsm deleted file mode 100644 index b88500d1e..000000000 --- a/toolkit/components/webextensions/ExtensionParent.jsm +++ /dev/null @@ -1,551 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -/** - * This module contains code for managing APIs that need to run in the - * parent process, and handles the parent side of operations that need - * to be proxied from ExtensionChild.jsm. - */ - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -/* exported ExtensionParent */ - -this.EXPORTED_SYMBOLS = ["ExtensionParent"]; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", - "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NativeApp", - "resource://gre/modules/NativeMessaging.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); - -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -var { - BaseContext, - SchemaAPIManager, -} = ExtensionCommon; - -var { - MessageManagerProxy, - SpreadArgs, - defineLazyGetter, - findPathInObject, -} = ExtensionUtils; - -const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json"; -const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas"; -const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts"; - -let schemaURLs = new Set(); - -if (!AppConstants.RELEASE_OR_BETA) { - schemaURLs.add("chrome://extensions/content/schemas/experiments.json"); -} - -let GlobalManager; -let ParentAPIManager; -let ProxyMessenger; - -// This object loads the ext-*.js scripts that define the extension API. -let apiManager = new class extends SchemaAPIManager { - constructor() { - super("main"); - this.initialized = null; - } - - // Loads all the ext-*.js scripts currently registered. - lazyInit() { - if (this.initialized) { - return this.initialized; - } - - // Load order matters here. The base manifest defines types which are - // extended by other schemas, so needs to be loaded first. - let promise = Schemas.load(BASE_SCHEMA).then(() => { - let promises = []; - for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) { - promises.push(Schemas.load(url)); - } - for (let url of schemaURLs) { - promises.push(Schemas.load(url)); - } - return Promise.all(promises); - }); - - for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) { - this.loadScript(value); - } - - this.initialized = promise; - return this.initialized; - } - - registerSchemaAPI(namespace, envType, getAPI) { - if (envType == "addon_parent" || envType == "content_parent") { - super.registerSchemaAPI(namespace, envType, getAPI); - } - } -}(); - -// Subscribes to messages related to the extension messaging API and forwards it -// to the relevant message manager. The "sender" field for the `onMessage` and -// `onConnect` events are updated if needed. -ProxyMessenger = { - _initialized: false, - init() { - if (this._initialized) { - return; - } - this._initialized = true; - - // TODO(robwu): When addons move to a separate process, we should use the - // parent process manager(s) of the addon process(es) instead of the - // in-process one. - let pipmm = Services.ppmm.getChildAt(0); - // Listen on the global frame message manager because content scripts send - // and receive extension messages via their frame. - // Listen on the parent process message manager because `runtime.connect` - // and `runtime.sendMessage` requests must be delivered to all frames in an - // addon process (by the API contract). - // And legacy addons are not associated with a frame, so that is another - // reason for having a parent process manager here. - let messageManagers = [Services.mm, pipmm]; - - MessageChannel.addListener(messageManagers, "Extension:Connect", this); - MessageChannel.addListener(messageManagers, "Extension:Message", this); - MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this); - MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this); - }, - - receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) { - if (recipient.toNativeApp) { - let {childId, toNativeApp} = recipient; - if (messageName == "Extension:Message") { - let context = ParentAPIManager.getContextById(childId); - return new NativeApp(context, toNativeApp).sendMessage(data); - } - if (messageName == "Extension:Connect") { - let context = ParentAPIManager.getContextById(childId); - NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp); - return true; - } - // "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for - // native messages are handled by NativeApp. - return; - } - let extension = GlobalManager.extensionMap.get(sender.extensionId); - let receiverMM = this._getMessageManagerForRecipient(recipient); - if (!extension || !receiverMM) { - return Promise.reject({ - result: MessageChannel.RESULT_NO_HANDLER, - message: "No matching message handler for the given recipient.", - }); - } - - if ((messageName == "Extension:Message" || - messageName == "Extension:Connect") && - apiManager.global.tabGetSender) { - // From ext-tabs.js, undefined on Android. - apiManager.global.tabGetSender(extension, target, sender); - } - return MessageChannel.sendMessage(receiverMM, messageName, data, { - sender, - recipient, - responseType, - }); - }, - - /** - * @param {object} recipient An object that was passed to - * `MessageChannel.sendMessage`. - * @returns {object|null} The message manager matching the recipient if found. - */ - _getMessageManagerForRecipient(recipient) { - let {extensionId, tabId} = recipient; - // tabs.sendMessage / tabs.connect - if (tabId) { - // `tabId` being set implies that the tabs API is supported, so we don't - // need to check whether `TabManager` exists. - let tab = apiManager.global.TabManager.getTab(tabId, null, null); - return tab && tab.linkedBrowser.messageManager; - } - - // runtime.sendMessage / runtime.connect - if (extensionId) { - // TODO(robwu): map the extensionId to the addon parent process's message - // manager when they run in a separate process. - return Services.ppmm.getChildAt(0); - } - - return null; - }, -}; - -// Responsible for loading extension APIs into the right globals. -GlobalManager = { - // Map[extension ID -> Extension]. Determines which extension is - // responsible for content under a particular extension ID. - extensionMap: new Map(), - initialized: false, - - init(extension) { - if (this.extensionMap.size == 0) { - ProxyMessenger.init(); - apiManager.on("extension-browser-inserted", this._onExtensionBrowser); - this.initialized = true; - } - - this.extensionMap.set(extension.id, extension); - }, - - uninit(extension) { - this.extensionMap.delete(extension.id); - - if (this.extensionMap.size == 0 && this.initialized) { - apiManager.off("extension-browser-inserted", this._onExtensionBrowser); - this.initialized = false; - } - }, - - _onExtensionBrowser(type, browser) { - browser.messageManager.loadFrameScript(`data:, - Components.utils.import("resource://gre/modules/ExtensionContent.jsm"); - ExtensionContent.init(this); - addEventListener("unload", function() { - ExtensionContent.uninit(this); - }); - `, false); - }, - - getExtension(extensionId) { - return this.extensionMap.get(extensionId); - }, - - injectInObject(context, isChromeCompat, dest) { - apiManager.generateAPIs(context, dest); - SchemaAPIManager.generateAPIs(context, context.extension.apis, dest); - }, -}; - -/** - * The proxied parent side of a context in ExtensionChild.jsm, for the - * parent side of a proxied API. - */ -class ProxyContextParent extends BaseContext { - constructor(envType, extension, params, xulBrowser, principal) { - super(envType, extension); - - this.uri = NetUtil.newURI(params.url); - - this.incognito = params.incognito; - - // This message manager is used by ParentAPIManager to send messages and to - // close the ProxyContext if the underlying message manager closes. This - // message manager object may change when `xulBrowser` swaps docshells, e.g. - // when a tab is moved to a different window. - this.messageManagerProxy = new MessageManagerProxy(xulBrowser); - - Object.defineProperty(this, "principal", { - value: principal, enumerable: true, configurable: true, - }); - - this.listenerProxies = new Map(); - - apiManager.emit("proxy-context-load", this); - } - - get cloneScope() { - return this.sandbox; - } - - get xulBrowser() { - return this.messageManagerProxy.eventTarget; - } - - get parentMessageManager() { - return this.messageManagerProxy.messageManager; - } - - shutdown() { - this.unload(); - } - - unload() { - if (this.unloaded) { - return; - } - this.messageManagerProxy.dispose(); - super.unload(); - apiManager.emit("proxy-context-unload", this); - } -} - -defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() { - let obj = {}; - GlobalManager.injectInObject(this, false, obj); - return obj; -}); - -defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() { - return Cu.Sandbox(this.principal); -}); - -/** - * The parent side of proxied API context for extension content script - * running in ExtensionContent.jsm. - */ -class ContentScriptContextParent extends ProxyContextParent { -} - -/** - * The parent side of proxied API context for extension page, such as a - * background script, a tab page, or a popup, running in - * ExtensionChild.jsm. - */ -class ExtensionPageContextParent extends ProxyContextParent { - constructor(envType, extension, params, xulBrowser) { - super(envType, extension, params, xulBrowser, extension.principal); - - this.viewType = params.viewType; - } - - // The window that contains this context. This may change due to moving tabs. - get xulWindow() { - return this.xulBrowser.ownerGlobal; - } - - get windowId() { - if (!apiManager.global.WindowManager || this.viewType == "background") { - return; - } - // viewType popup or tab: - return apiManager.global.WindowManager.getId(this.xulWindow); - } - - get tabId() { - if (!apiManager.global.TabManager) { - return; // Not yet supported on Android. - } - let {gBrowser} = this.xulBrowser.ownerGlobal; - let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser); - return tab && apiManager.global.TabManager.getId(tab); - } - - onBrowserChange(browser) { - super.onBrowserChange(browser); - this.xulBrowser = browser; - } - - shutdown() { - apiManager.emit("page-shutdown", this); - super.shutdown(); - } -} - -ParentAPIManager = { - proxyContexts: new Map(), - - init() { - Services.obs.addObserver(this, "message-manager-close", false); - - Services.mm.addMessageListener("API:CreateProxyContext", this); - Services.mm.addMessageListener("API:CloseProxyContext", this, true); - Services.mm.addMessageListener("API:Call", this); - Services.mm.addMessageListener("API:AddListener", this); - Services.mm.addMessageListener("API:RemoveListener", this); - }, - - observe(subject, topic, data) { - if (topic === "message-manager-close") { - let mm = subject; - for (let [childId, context] of this.proxyContexts) { - if (context.parentMessageManager === mm) { - this.closeProxyContext(childId); - } - } - } - }, - - shutdownExtension(extensionId) { - for (let [childId, context] of this.proxyContexts) { - if (context.extension.id == extensionId) { - context.shutdown(); - this.proxyContexts.delete(childId); - } - } - }, - - receiveMessage({name, data, target}) { - switch (name) { - case "API:CreateProxyContext": - this.createProxyContext(data, target); - break; - - case "API:CloseProxyContext": - this.closeProxyContext(data.childId); - break; - - case "API:Call": - this.call(data, target); - break; - - case "API:AddListener": - this.addListener(data, target); - break; - - case "API:RemoveListener": - this.removeListener(data); - break; - } - }, - - createProxyContext(data, target) { - let {envType, extensionId, childId, principal} = data; - if (this.proxyContexts.has(childId)) { - throw new Error("A WebExtension context with the given ID already exists!"); - } - - let extension = GlobalManager.getExtension(extensionId); - if (!extension) { - throw new Error(`No WebExtension found with ID ${extensionId}`); - } - - let context; - if (envType == "addon_parent") { - // Privileged addon contexts can only be loaded in documents whose main - // frame is also the same addon. - if (principal.URI.prePath !== extension.baseURI.prePath || - !target.contentPrincipal.subsumes(principal)) { - throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`); - } - context = new ExtensionPageContextParent(envType, extension, data, target); - } else if (envType == "content_parent") { - context = new ContentScriptContextParent(envType, extension, data, target, principal); - } else { - throw new Error(`Invalid WebExtension context envType: ${envType}`); - } - this.proxyContexts.set(childId, context); - }, - - closeProxyContext(childId) { - let context = this.proxyContexts.get(childId); - if (context) { - context.unload(); - this.proxyContexts.delete(childId); - } - }, - - call(data, target) { - let context = this.getContextById(data.childId); - if (context.parentMessageManager !== target.messageManager) { - throw new Error("Got message on unexpected message manager"); - } - - let reply = result => { - if (!context.parentMessageManager) { - Cu.reportError("Cannot send function call result: other side closed connection"); - return; - } - - context.parentMessageManager.sendAsyncMessage( - "API:CallResult", - Object.assign({ - childId: data.childId, - callId: data.callId, - }, result)); - }; - - try { - let args = Cu.cloneInto(data.args, context.sandbox); - let result = findPathInObject(context.apiObj, data.path)(...args); - - if (data.callId) { - result = result || Promise.resolve(); - - result.then(result => { - result = result instanceof SpreadArgs ? [...result] : [result]; - - reply({result}); - }, error => { - error = context.normalizeError(error); - reply({error: {message: error.message}}); - }); - } - } catch (e) { - if (data.callId) { - let error = context.normalizeError(e); - reply({error: {message: error.message}}); - } else { - Cu.reportError(e); - } - } - }, - - addListener(data, target) { - let context = this.getContextById(data.childId); - if (context.parentMessageManager !== target.messageManager) { - throw new Error("Got message on unexpected message manager"); - } - - let {childId} = data; - - function listener(...listenerArgs) { - return context.sendMessage( - context.parentMessageManager, - "API:RunListener", - { - childId, - listenerId: data.listenerId, - path: data.path, - args: listenerArgs, - }, - { - recipient: {childId}, - }); - } - - context.listenerProxies.set(data.listenerId, listener); - - let args = Cu.cloneInto(data.args, context.sandbox); - findPathInObject(context.apiObj, data.path).addListener(listener, ...args); - }, - - removeListener(data) { - let context = this.getContextById(data.childId); - let listener = context.listenerProxies.get(data.listenerId); - findPathInObject(context.apiObj, data.path).removeListener(listener); - }, - - getContextById(childId) { - let context = this.proxyContexts.get(childId); - if (!context) { - let error = new Error("WebExtension context not found!"); - Cu.reportError(error); - throw error; - } - return context; - }, -}; - -ParentAPIManager.init(); - - -const ExtensionParent = { - GlobalManager, - ParentAPIManager, - apiManager, -}; diff --git a/toolkit/components/webextensions/ExtensionStorage.jsm b/toolkit/components/webextensions/ExtensionStorage.jsm deleted file mode 100644 index 0b0ffb000..000000000 --- a/toolkit/components/webextensions/ExtensionStorage.jsm +++ /dev/null @@ -1,241 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionStorage"]; - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", - "resource://gre/modules/AsyncShutdown.jsm"); - -function jsonReplacer(key, value) { - switch (typeof(value)) { - // Serialize primitive types as-is. - case "string": - case "number": - case "boolean": - return value; - - case "object": - if (value === null) { - return value; - } - - switch (Cu.getClassName(value, true)) { - // Serialize arrays and ordinary objects as-is. - case "Array": - case "Object": - return value; - - // Serialize Date objects and regular expressions as their - // string representations. - case "Date": - case "RegExp": - return String(value); - } - break; - } - - if (!key) { - // If this is the root object, and we can't serialize it, serialize - // the value to an empty object. - return {}; - } - - // Everything else, omit entirely. - return undefined; -} - -this.ExtensionStorage = { - cache: new Map(), - listeners: new Map(), - - /** - * Sanitizes the given value, and returns a JSON-compatible - * representation of it, based on the privileges of the given global. - * - * @param {value} value - * The value to sanitize. - * @param {Context} context - * The extension context in which to sanitize the value - * @returns {value} - * The sanitized value. - */ - sanitize(value, context) { - let json = context.jsonStringify(value, jsonReplacer); - return JSON.parse(json); - }, - - getExtensionDir(extensionId) { - return OS.Path.join(this.extensionDir, extensionId); - }, - - getStorageFile(extensionId) { - return OS.Path.join(this.extensionDir, extensionId, "storage.js"); - }, - - read(extensionId) { - if (this.cache.has(extensionId)) { - return this.cache.get(extensionId); - } - - let path = this.getStorageFile(extensionId); - let decoder = new TextDecoder(); - let promise = OS.File.read(path); - promise = promise.then(array => { - return JSON.parse(decoder.decode(array)); - }).catch((error) => { - if (!error.becauseNoSuchFile) { - Cu.reportError("Unable to parse JSON data for extension storage."); - } - return {}; - }); - this.cache.set(extensionId, promise); - return promise; - }, - - write(extensionId) { - let promise = this.read(extensionId).then(extData => { - let encoder = new TextEncoder(); - let array = encoder.encode(JSON.stringify(extData)); - let path = this.getStorageFile(extensionId); - OS.File.makeDir(this.getExtensionDir(extensionId), { - ignoreExisting: true, - from: OS.Constants.Path.profileDir, - }); - let promise = OS.File.writeAtomic(path, array); - return promise; - }).catch(() => { - // Make sure this promise is never rejected. - Cu.reportError("Unable to write JSON data for extension storage."); - }); - - AsyncShutdown.profileBeforeChange.addBlocker( - "ExtensionStorage: Finish writing extension data", - promise); - - return promise.then(() => { - AsyncShutdown.profileBeforeChange.removeBlocker(promise); - }); - }, - - set(extensionId, items, context) { - return this.read(extensionId).then(extData => { - let changes = {}; - for (let prop in items) { - let item = this.sanitize(items[prop], context); - changes[prop] = {oldValue: extData[prop], newValue: item}; - extData[prop] = item; - } - - this.notifyListeners(extensionId, changes); - - return this.write(extensionId); - }); - }, - - remove(extensionId, items) { - return this.read(extensionId).then(extData => { - let changes = {}; - for (let prop of [].concat(items)) { - changes[prop] = {oldValue: extData[prop]}; - delete extData[prop]; - } - - this.notifyListeners(extensionId, changes); - - return this.write(extensionId); - }); - }, - - clear(extensionId) { - return this.read(extensionId).then(extData => { - let changes = {}; - for (let prop of Object.keys(extData)) { - changes[prop] = {oldValue: extData[prop]}; - delete extData[prop]; - } - - this.notifyListeners(extensionId, changes); - - return this.write(extensionId); - }); - }, - - get(extensionId, keys) { - return this.read(extensionId).then(extData => { - let result = {}; - if (keys === null) { - Object.assign(result, extData); - } else if (typeof(keys) == "object" && !Array.isArray(keys)) { - for (let prop in keys) { - if (prop in extData) { - result[prop] = extData[prop]; - } else { - result[prop] = keys[prop]; - } - } - } else { - for (let prop of [].concat(keys)) { - if (prop in extData) { - result[prop] = extData[prop]; - } - } - } - - return result; - }); - }, - - addOnChangedListener(extensionId, listener) { - let listeners = this.listeners.get(extensionId) || new Set(); - listeners.add(listener); - this.listeners.set(extensionId, listeners); - }, - - removeOnChangedListener(extensionId, listener) { - let listeners = this.listeners.get(extensionId); - listeners.delete(listener); - }, - - notifyListeners(extensionId, changes) { - let listeners = this.listeners.get(extensionId); - if (listeners) { - for (let listener of listeners) { - listener(changes); - } - } - }, - - init() { - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - return; - } - Services.obs.addObserver(this, "extension-invalidate-storage-cache", false); - Services.obs.addObserver(this, "xpcom-shutdown", false); - }, - - observe(subject, topic, data) { - if (topic == "xpcom-shutdown") { - Services.obs.removeObserver(this, "extension-invalidate-storage-cache"); - Services.obs.removeObserver(this, "xpcom-shutdown"); - } else if (topic == "extension-invalidate-storage-cache") { - this.cache.clear(); - } - }, -}; - -XPCOMUtils.defineLazyGetter(ExtensionStorage, "extensionDir", - () => OS.Path.join(OS.Constants.Path.profileDir, "browser-extension-data")); - -ExtensionStorage.init(); diff --git a/toolkit/components/webextensions/ExtensionTestCommon.jsm b/toolkit/components/webextensions/ExtensionTestCommon.jsm deleted file mode 100644 index 02453ddfd..000000000 --- a/toolkit/components/webextensions/ExtensionTestCommon.jsm +++ /dev/null @@ -1,343 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -/** - * This module contains extension testing helper logic which is common - * between all test suites. - */ - -/* exported ExtensionTestCommon, MockExtension */ - -this.EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.importGlobalProperties(["TextEncoder"]); - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Extension", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent", - "resource://gre/modules/ExtensionParent.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); - -XPCOMUtils.defineLazyGetter(this, "apiManager", - () => ExtensionParent.apiManager); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "uuidGen", - "@mozilla.org/uuid-generator;1", - "nsIUUIDGenerator"); - -const { - flushJarCache, - instanceOf, -} = ExtensionUtils; - -XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole); - - -/** - * A skeleton Extension-like object, used for testing, which installs an - * add-on via the add-on manager when startup() is called, and - * uninstalles it on shutdown(). - * - * @param {string} id - * @param {nsIFile} file - * @param {nsIURI} rootURI - * @param {string} installType - */ -class MockExtension { - constructor(file, rootURI, installType) { - this.id = null; - this.file = file; - this.rootURI = rootURI; - this.installType = installType; - this.addon = null; - - let promiseEvent = eventName => new Promise(resolve => { - let onstartup = (msg, extension) => { - if (this.addon && extension.id == this.addon.id) { - apiManager.off(eventName, onstartup); - - this.id = extension.id; - this._extension = extension; - resolve(extension); - } - }; - apiManager.on(eventName, onstartup); - }); - - this._extension = null; - this._extensionPromise = promiseEvent("startup"); - this._readyPromise = promiseEvent("ready"); - } - - testMessage(...args) { - return this._extension.testMessage(...args); - } - - on(...args) { - this._extensionPromise.then(extension => { - extension.on(...args); - }); - } - - off(...args) { - this._extensionPromise.then(extension => { - extension.off(...args); - }); - } - - startup() { - if (this.installType == "temporary") { - return AddonManager.installTemporaryAddon(this.file).then(addon => { - this.addon = addon; - return this._readyPromise; - }); - } else if (this.installType == "permanent") { - return new Promise((resolve, reject) => { - AddonManager.getInstallForFile(this.file, install => { - let listener = { - onInstallFailed: reject, - onInstallEnded: (install, newAddon) => { - this.addon = newAddon; - resolve(this._readyPromise); - }, - }; - - install.addListener(listener); - install.install(); - }); - }); - } - throw new Error("installType must be one of: temporary, permanent"); - } - - shutdown() { - this.addon.uninstall(); - return this.cleanupGeneratedFile(); - } - - cleanupGeneratedFile() { - flushJarCache(this.file); - return OS.File.remove(this.file.path); - } -} - -class ExtensionTestCommon { - /** - * This code is designed to make it easy to test a WebExtension - * without creating a bunch of files. Everything is contained in a - * single JSON blob. - * - * Properties: - * "background": "<JS code>" - * A script to be loaded as the background script. - * The "background" section of the "manifest" property is overwritten - * if this is provided. - * "manifest": {...} - * Contents of manifest.json - * "files": {"filename1": "contents1", ...} - * Data to be included as files. Can be referenced from the manifest. - * If a manifest file is provided here, it takes precedence over - * a generated one. Always use "/" as a directory separator. - * Directories should appear here only implicitly (as a prefix - * to file names) - * - * To make things easier, the value of "background" and "files"[] can - * be a function, which is converted to source that is run. - * - * The generated extension is stored in the system temporary directory, - * and an nsIFile object pointing to it is returned. - * - * @param {object} data - * @returns {nsIFile} - */ - static generateXPI(data) { - let manifest = data.manifest; - if (!manifest) { - manifest = {}; - } - - let files = data.files; - if (!files) { - files = {}; - } - - function provide(obj, keys, value, override = false) { - if (keys.length == 1) { - if (!(keys[0] in obj) || override) { - obj[keys[0]] = value; - } - } else { - if (!(keys[0] in obj)) { - obj[keys[0]] = {}; - } - provide(obj[keys[0]], keys.slice(1), value, override); - } - } - - provide(manifest, ["name"], "Generated extension"); - provide(manifest, ["manifest_version"], 2); - provide(manifest, ["version"], "1.0"); - - if (data.background) { - let bgScript = uuidGen.generateUUID().number + ".js"; - - provide(manifest, ["background", "scripts"], [bgScript], true); - files[bgScript] = data.background; - } - - provide(files, ["manifest.json"], manifest); - - if (data.embedded) { - // Package this as a webextension embedded inside a legacy - // extension. - - let xpiFiles = { - "install.rdf": `<?xml version="1.0" encoding="UTF-8"?> - <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:em="http://www.mozilla.org/2004/em-rdf#"> - <Description about="urn:mozilla:install-manifest" - em:id="${manifest.applications.gecko.id}" - em:name="${manifest.name}" - em:type="2" - em:version="${manifest.version}" - em:description="" - em:hasEmbeddedWebExtension="true" - em:bootstrap="true"> - - <!-- Firefox --> - <em:targetApplication> - <Description - em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" - em:minVersion="51.0a1" - em:maxVersion="*"/> - </em:targetApplication> - </Description> - </RDF> - `, - - "bootstrap.js": ` - function install() {} - function uninstall() {} - function shutdown() {} - - function startup(data) { - data.webExtension.startup(); - } - `, - }; - - for (let [path, data] of Object.entries(files)) { - xpiFiles[`webextension/${path}`] = data; - } - - files = xpiFiles; - } - - return this.generateZipFile(files); - } - - static generateZipFile(files, baseName = "generated-extension.xpi") { - let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter"); - let zipW = new ZipWriter(); - - let file = FileUtils.getFile("TmpD", [baseName]); - file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); - - const MODE_WRONLY = 0x02; - const MODE_TRUNCATE = 0x20; - zipW.open(file, MODE_WRONLY | MODE_TRUNCATE); - - // Needs to be in microseconds for some reason. - let time = Date.now() * 1000; - - function generateFile(filename) { - let components = filename.split("/"); - let path = ""; - for (let component of components.slice(0, -1)) { - path += component + "/"; - if (!zipW.hasEntry(path)) { - zipW.addEntryDirectory(path, time, false); - } - } - } - - for (let filename in files) { - let script = files[filename]; - if (typeof(script) == "function") { - script = "(" + script.toString() + ")()"; - } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) { - script = JSON.stringify(script); - } - - if (!instanceOf(script, "ArrayBuffer")) { - script = new TextEncoder("utf-8").encode(script).buffer; - } - - let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream); - stream.setData(script, 0, script.byteLength); - - generateFile(filename); - zipW.addEntryStream(filename, time, 0, stream, false); - } - - zipW.close(); - - return file; - } - - /** - * Generates a new extension using |Extension.generateXPI|, and initializes a - * new |Extension| instance which will execute it. - * - * @param {object} data - * @returns {Extension} - */ - static generate(data) { - let file = this.generateXPI(data); - - flushJarCache(file); - Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path}); - - let fileURI = Services.io.newFileURI(file); - let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null); - - // This may be "temporary" or "permanent". - if (data.useAddonManager) { - return new MockExtension(file, jarURI, data.useAddonManager); - } - - let id; - if (data.manifest) { - if (data.manifest.applications && data.manifest.applications.gecko) { - id = data.manifest.applications.gecko.id; - } else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) { - id = data.manifest.browser_specific_settings.gecko.id; - } - } - if (!id) { - id = uuidGen.generateUUID().number; - } - - return new Extension({ - id, - resourceURI: jarURI, - cleanupFile: file, - }); - } -} diff --git a/toolkit/components/webextensions/ExtensionUtils.jsm b/toolkit/components/webextensions/ExtensionUtils.jsm deleted file mode 100644 index e7f768c07..000000000 --- a/toolkit/components/webextensions/ExtensionUtils.jsm +++ /dev/null @@ -1,1215 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionUtils"]; - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -const INTEGER = /^[1-9]\d*$/; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector", - "resource:///modules/translation/LanguageDetector.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Locale", - "resource://gre/modules/Locale.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", - "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Preferences", - "resource://gre/modules/Preferences.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService", - "@mozilla.org/content/style-sheet-service;1", - "nsIStyleSheetService"); - -function getConsole() { - return new ConsoleAPI({ - maxLogLevelPref: "extensions.webextensions.log.level", - prefix: "WebExtensions", - }); -} - -XPCOMUtils.defineLazyGetter(this, "console", getConsole); - -let nextId = 0; -const {uniqueProcessID} = Services.appinfo; - -function getUniqueId() { - return `${nextId++}-${uniqueProcessID}`; -} - -/** - * An Error subclass for which complete error messages are always passed - * to extensions, rather than being interpreted as an unknown error. - */ -class ExtensionError extends Error {} - -function filterStack(error) { - return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n"); -} - -// Run a function and report exceptions. -function runSafeSyncWithoutClone(f, ...args) { - try { - return f(...args); - } catch (e) { - dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`); - Cu.reportError(e); - } -} - -// Run a function and report exceptions. -function runSafeWithoutClone(f, ...args) { - if (typeof(f) != "function") { - dump(`Extension error: expected function\n${filterStack(Error())}`); - return; - } - - Promise.resolve().then(() => { - runSafeSyncWithoutClone(f, ...args); - }); -} - -// Run a function, cloning arguments into context.cloneScope, and -// report exceptions. |f| is expected to be in context.cloneScope. -function runSafeSync(context, f, ...args) { - if (context.unloaded) { - Cu.reportError("runSafeSync called after context unloaded"); - return; - } - - try { - args = Cu.cloneInto(args, context.cloneScope); - } catch (e) { - Cu.reportError(e); - dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`); - } - return runSafeSyncWithoutClone(f, ...args); -} - -// Run a function, cloning arguments into context.cloneScope, and -// report exceptions. |f| is expected to be in context.cloneScope. -function runSafe(context, f, ...args) { - try { - args = Cu.cloneInto(args, context.cloneScope); - } catch (e) { - Cu.reportError(e); - dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`); - } - if (context.unloaded) { - dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`); - return undefined; - } - return runSafeWithoutClone(f, ...args); -} - -function getInnerWindowID(window) { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .currentInnerWindowID; -} - -// Return true if the given value is an instance of the given -// native type. -function instanceOf(value, type) { - return {}.toString.call(value) == `[object ${type}]`; -} - -// Extend the object |obj| with the property descriptors of each object in -// |args|. -function extend(obj, ...args) { - for (let arg of args) { - let props = [...Object.getOwnPropertyNames(arg), - ...Object.getOwnPropertySymbols(arg)]; - for (let prop of props) { - let descriptor = Object.getOwnPropertyDescriptor(arg, prop); - Object.defineProperty(obj, prop, descriptor); - } - } - - return obj; -} - -/** - * Similar to a WeakMap, but creates a new key with the given - * constructor if one is not present. - */ -class DefaultWeakMap extends WeakMap { - constructor(defaultConstructor, init) { - super(init); - this.defaultConstructor = defaultConstructor; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.defaultConstructor(key)); - } - return super.get(key); - } -} - -class DefaultMap extends Map { - constructor(defaultConstructor, init) { - super(init); - this.defaultConstructor = defaultConstructor; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.defaultConstructor(key)); - } - return super.get(key); - } -} - -class SpreadArgs extends Array { - constructor(args) { - super(); - this.push(...args); - } -} - -// Manages icon details for toolbar buttons in the |pageAction| and -// |browserAction| APIs. -let IconDetails = { - // Normalizes the various acceptable input formats into an object - // with icon size as key and icon URL as value. - // - // If a context is specified (function is called from an extension): - // Throws an error if an invalid icon size was provided or the - // extension is not allowed to load the specified resources. - // - // If no context is specified, instead of throwing an error, this - // function simply logs a warning message. - normalize(details, extension, context = null) { - let result = {}; - - try { - if (details.imageData) { - let imageData = details.imageData; - - if (typeof imageData == "string") { - imageData = {"19": imageData}; - } - - for (let size of Object.keys(imageData)) { - if (!INTEGER.test(size)) { - throw new ExtensionError(`Invalid icon size ${size}, must be an integer`); - } - result[size] = imageData[size]; - } - } - - if (details.path) { - let path = details.path; - if (typeof path != "object") { - path = {"19": path}; - } - - let baseURI = context ? context.uri : extension.baseURI; - - for (let size of Object.keys(path)) { - if (!INTEGER.test(size)) { - throw new ExtensionError(`Invalid icon size ${size}, must be an integer`); - } - - let url = baseURI.resolve(path[size]); - - // The Chrome documentation specifies these parameters as - // relative paths. We currently accept absolute URLs as well, - // which means we need to check that the extension is allowed - // to load them. This will throw an error if it's not allowed. - try { - Services.scriptSecurityManager.checkLoadURIStrWithPrincipal( - extension.principal, url, - Services.scriptSecurityManager.DISALLOW_SCRIPT); - } catch (e) { - throw new ExtensionError(`Illegal URL ${url}`); - } - - result[size] = url; - } - } - } catch (e) { - // Function is called from extension code, delegate error. - if (context) { - throw e; - } - // If there's no context, it's because we're handling this - // as a manifest directive. Log a warning rather than - // raising an error. - extension.manifestError(`Invalid icon data: ${e}`); - } - - return result; - }, - - // Returns the appropriate icon URL for the given icons object and the - // screen resolution of the given window. - getPreferredIcon(icons, extension = null, size = 16) { - const DEFAULT = "chrome://browser/content/extension.svg"; - - let bestSize = null; - if (icons[size]) { - bestSize = size; - } else if (icons[2 * size]) { - bestSize = 2 * size; - } else { - let sizes = Object.keys(icons) - .map(key => parseInt(key, 10)) - .sort((a, b) => a - b); - - bestSize = sizes.find(candidate => candidate > size) || sizes.pop(); - } - - if (bestSize) { - return {size: bestSize, icon: icons[bestSize]}; - } - - return {size, icon: DEFAULT}; - }, - - convertImageURLToDataURL(imageURL, contentWindow, browserWindow, size = 18) { - return new Promise((resolve, reject) => { - let image = new contentWindow.Image(); - image.onload = function() { - let canvas = contentWindow.document.createElement("canvas"); - let ctx = canvas.getContext("2d"); - let dSize = size * browserWindow.devicePixelRatio; - - // Scales the image while maintaing width to height ratio. - // If the width and height differ, the image is centered using the - // smaller of the two dimensions. - let dWidth, dHeight, dx, dy; - if (this.width > this.height) { - dWidth = dSize; - dHeight = image.height * (dSize / image.width); - dx = 0; - dy = (dSize - dHeight) / 2; - } else { - dWidth = image.width * (dSize / image.height); - dHeight = dSize; - dx = (dSize - dWidth) / 2; - dy = 0; - } - - ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight); - resolve(canvas.toDataURL("image/png")); - }; - image.onerror = reject; - image.src = imageURL; - }); - }, -}; - -const LISTENERS = Symbol("listeners"); - -class EventEmitter { - constructor() { - this[LISTENERS] = new Map(); - } - - /** - * Adds the given function as a listener for the given event. - * - * The listener function may optionally return a Promise which - * resolves when it has completed all operations which event - * dispatchers may need to block on. - * - * @param {string} event - * The name of the event to listen for. - * @param {function(string, ...any)} listener - * The listener to call when events are emitted. - */ - on(event, listener) { - if (!this[LISTENERS].has(event)) { - this[LISTENERS].set(event, new Set()); - } - - this[LISTENERS].get(event).add(listener); - } - - /** - * Removes the given function as a listener for the given event. - * - * @param {string} event - * The name of the event to stop listening for. - * @param {function(string, ...any)} listener - * The listener function to remove. - */ - off(event, listener) { - if (this[LISTENERS].has(event)) { - let set = this[LISTENERS].get(event); - - set.delete(listener); - if (!set.size) { - this[LISTENERS].delete(event); - } - } - } - - /** - * Triggers all listeners for the given event, and returns a promise - * which resolves when all listeners have been called, and any - * promises they have returned have likewise resolved. - * - * @param {string} event - * The name of the event to emit. - * @param {any} args - * Arbitrary arguments to pass to the listener functions, after - * the event name. - * @returns {Promise} - */ - emit(event, ...args) { - let listeners = this[LISTENERS].get(event) || new Set(); - - let promises = Array.from(listeners, listener => { - return runSafeSyncWithoutClone(listener, event, ...args); - }); - - return Promise.all(promises); - } -} - -function LocaleData(data) { - this.defaultLocale = data.defaultLocale; - this.selectedLocale = data.selectedLocale; - this.locales = data.locales || new Map(); - this.warnedMissingKeys = new Set(); - - // Map(locale-name -> Map(message-key -> localized-string)) - // - // Contains a key for each loaded locale, each of which is a - // Map of message keys to their localized strings. - this.messages = data.messages || new Map(); - - if (data.builtinMessages) { - this.messages.set(this.BUILTIN, data.builtinMessages); - } -} - - -LocaleData.prototype = { - // Representation of the object to send to content processes. This - // should include anything the content process might need. - serialize() { - return { - defaultLocale: this.defaultLocale, - selectedLocale: this.selectedLocale, - messages: this.messages, - locales: this.locales, - }; - }, - - BUILTIN: "@@BUILTIN_MESSAGES", - - has(locale) { - return this.messages.has(locale); - }, - - // https://developer.chrome.com/extensions/i18n - localizeMessage(message, substitutions = [], options = {}) { - let defaultOptions = { - locale: this.selectedLocale, - defaultValue: "", - cloneScope: null, - }; - - options = Object.assign(defaultOptions, options); - - let locales = new Set([this.BUILTIN, options.locale, this.defaultLocale] - .filter(locale => this.messages.has(locale))); - - // Message names are case-insensitive, so normalize them to lower-case. - message = message.toLowerCase(); - for (let locale of locales) { - let messages = this.messages.get(locale); - if (messages.has(message)) { - let str = messages.get(message); - - if (!Array.isArray(substitutions)) { - substitutions = [substitutions]; - } - - let replacer = (matched, index, dollarSigns) => { - if (index) { - // This is not quite Chrome-compatible. Chrome consumes any number - // of digits following the $, but only accepts 9 substitutions. We - // accept any number of substitutions. - index = parseInt(index, 10) - 1; - return index in substitutions ? substitutions[index] : ""; - } - // For any series of contiguous `$`s, the first is dropped, and - // the rest remain in the output string. - return dollarSigns; - }; - return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer); - } - } - - // Check for certain pre-defined messages. - if (message == "@@ui_locale") { - return this.uiLocale; - } else if (message.startsWith("@@bidi_")) { - let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry); - let rtl = registry.isLocaleRTL("global"); - - if (message == "@@bidi_dir") { - return rtl ? "rtl" : "ltr"; - } else if (message == "@@bidi_reversed_dir") { - return rtl ? "ltr" : "rtl"; - } else if (message == "@@bidi_start_edge") { - return rtl ? "right" : "left"; - } else if (message == "@@bidi_end_edge") { - return rtl ? "left" : "right"; - } - } - - if (!this.warnedMissingKeys.has(message)) { - let error = `Unknown localization message ${message}`; - if (options.cloneScope) { - error = new options.cloneScope.Error(error); - } - Cu.reportError(error); - this.warnedMissingKeys.add(message); - } - return options.defaultValue; - }, - - // Localize a string, replacing all |__MSG_(.*)__| tokens with the - // matching string from the current locale, as determined by - // |this.selectedLocale|. - // - // This may not be called before calling either |initLocale| or - // |initAllLocales|. - localize(str, locale = this.selectedLocale) { - if (!str) { - return str; - } - - return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => { - return this.localizeMessage(message, [], {locale, defaultValue: matched}); - }); - }, - - // Validates the contents of a locale JSON file, normalizes the - // messages into a Map of message key -> localized string pairs. - addLocale(locale, messages, extension) { - let result = new Map(); - - // Chrome does not document the semantics of its localization - // system very well. It handles replacements by pre-processing - // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their - // replacements. Later, it processes the resulting string for - // |$[0-9]| replacements. - // - // Again, it does not document this, but it accepts any number - // of sequential |$|s, and replaces them with that number minus - // 1. It also accepts |$| followed by any number of sequential - // digits, but refuses to process a localized string which - // provides more than 9 substitutions. - if (!instanceOf(messages, "Object")) { - extension.packagingError(`Invalid locale data for ${locale}`); - return result; - } - - for (let key of Object.keys(messages)) { - let msg = messages[key]; - - if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") { - extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`); - continue; - } - - // Substitutions are case-insensitive, so normalize all of their names - // to lower-case. - let placeholders = new Map(); - if (instanceOf(msg.placeholders, "Object")) { - for (let key of Object.keys(msg.placeholders)) { - placeholders.set(key.toLowerCase(), msg.placeholders[key]); - } - } - - let replacer = (match, name) => { - let replacement = placeholders.get(name.toLowerCase()); - if (instanceOf(replacement, "Object") && "content" in replacement) { - return replacement.content; - } - return ""; - }; - - let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer); - - // Message names are also case-insensitive, so normalize them to lower-case. - result.set(key.toLowerCase(), value); - } - - this.messages.set(locale, result); - return result; - }, - - get acceptLanguages() { - let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString); - return result.split(/\s*,\s*/g); - }, - - - get uiLocale() { - // Return the browser locale, but convert it to a Chrome-style - // locale code. - return Locale.getLocale().replace(/-/g, "_"); - }, -}; - -// This is a generic class for managing event listeners. Example usage: -// -// new EventManager(context, "api.subAPI", fire => { -// let listener = (...) => { -// // Fire any listeners registered with addListener. -// fire(arg1, arg2); -// }; -// // Register the listener. -// SomehowRegisterListener(listener); -// return () => { -// // Return a way to unregister the listener. -// SomehowUnregisterListener(listener); -// }; -// }).api() -// -// The result is an object with addListener, removeListener, and -// hasListener methods. |context| is an add-on scope (either an -// ExtensionContext in the chrome process or ExtensionContext in a -// content process). |name| is for debugging. |register| is a function -// to register the listener. |register| is only called once, even if -// multiple listeners are registered. |register| should return an -// unregister function that will unregister the listener. -function EventManager(context, name, register) { - this.context = context; - this.name = name; - this.register = register; - this.unregister = null; - this.callbacks = new Set(); -} - -EventManager.prototype = { - addListener(callback) { - if (typeof(callback) != "function") { - dump(`Expected function\n${Error().stack}`); - return; - } - if (this.context.unloaded) { - dump(`Cannot add listener to ${this.name} after context unloaded`); - return; - } - - if (!this.callbacks.size) { - this.context.callOnClose(this); - - let fireFunc = this.fire.bind(this); - let fireWithoutClone = this.fireWithoutClone.bind(this); - fireFunc.withoutClone = fireWithoutClone; - this.unregister = this.register(fireFunc); - } - this.callbacks.add(callback); - }, - - removeListener(callback) { - if (!this.callbacks.size) { - return; - } - - this.callbacks.delete(callback); - if (this.callbacks.size == 0) { - this.unregister(); - this.unregister = null; - - this.context.forgetOnClose(this); - } - }, - - hasListener(callback) { - return this.callbacks.has(callback); - }, - - fire(...args) { - this._fireCommon("runSafe", args); - }, - - fireWithoutClone(...args) { - this._fireCommon("runSafeWithoutClone", args); - }, - - _fireCommon(runSafeMethod, args) { - for (let callback of this.callbacks) { - Promise.resolve(callback).then(callback => { - if (this.context.unloaded) { - dump(`${this.name} event fired after context unloaded.\n`); - } else if (!this.context.active) { - dump(`${this.name} event fired while context is inactive.\n`); - } else if (this.callbacks.has(callback)) { - this.context[runSafeMethod](callback, ...args); - } - }); - } - }, - - close() { - if (this.callbacks.size) { - this.unregister(); - } - this.callbacks.clear(); - this.register = null; - this.unregister = null; - }, - - api() { - return { - addListener: callback => this.addListener(callback), - removeListener: callback => this.removeListener(callback), - hasListener: callback => this.hasListener(callback), - }; - }, -}; - -// Similar to EventManager, but it doesn't try to consolidate event -// notifications. Each addListener call causes us to register once. It -// allows extra arguments to be passed to addListener. -function SingletonEventManager(context, name, register) { - this.context = context; - this.name = name; - this.register = register; - this.unregister = new Map(); -} - -SingletonEventManager.prototype = { - addListener(callback, ...args) { - let wrappedCallback = (...args) => { - if (this.context.unloaded) { - dump(`${this.name} event fired after context unloaded.\n`); - } else if (this.unregister.has(callback)) { - return callback(...args); - } - }; - - let unregister = this.register(wrappedCallback, ...args); - this.unregister.set(callback, unregister); - this.context.callOnClose(this); - }, - - removeListener(callback) { - if (!this.unregister.has(callback)) { - return; - } - - let unregister = this.unregister.get(callback); - this.unregister.delete(callback); - unregister(); - }, - - hasListener(callback) { - return this.unregister.has(callback); - }, - - close() { - for (let unregister of this.unregister.values()) { - unregister(); - } - }, - - api() { - return { - addListener: (...args) => this.addListener(...args), - removeListener: (...args) => this.removeListener(...args), - hasListener: (...args) => this.hasListener(...args), - }; - }, -}; - -// Simple API for event listeners where events never fire. -function ignoreEvent(context, name) { - return { - addListener: function(callback) { - let id = context.extension.id; - let frame = Components.stack.caller; - let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`; - let scriptError = Cc["@mozilla.org/scripterror;1"] - .createInstance(Ci.nsIScriptError); - scriptError.init(msg, frame.filename, null, frame.lineNumber, - frame.columnNumber, Ci.nsIScriptError.warningFlag, - "content javascript"); - let consoleService = Cc["@mozilla.org/consoleservice;1"] - .getService(Ci.nsIConsoleService); - consoleService.logMessage(scriptError); - }, - removeListener: function(callback) {}, - hasListener: function(callback) {}, - }; -} - -// Copy an API object from |source| into the scope |dest|. -function injectAPI(source, dest) { - for (let prop in source) { - // Skip names prefixed with '_'. - if (prop[0] == "_") { - continue; - } - - let desc = Object.getOwnPropertyDescriptor(source, prop); - if (typeof(desc.value) == "function") { - Cu.exportFunction(desc.value, dest, {defineAs: prop}); - } else if (typeof(desc.value) == "object") { - let obj = Cu.createObjectIn(dest, {defineAs: prop}); - injectAPI(desc.value, obj); - } else { - Object.defineProperty(dest, prop, desc); - } - } -} - -/** - * Returns a Promise which resolves when the given document's DOM has - * fully loaded. - * - * @param {Document} doc The document to await the load of. - * @returns {Promise<Document>} - */ -function promiseDocumentReady(doc) { - if (doc.readyState == "interactive" || doc.readyState == "complete") { - return Promise.resolve(doc); - } - - return new Promise(resolve => { - doc.addEventListener("DOMContentLoaded", function onReady(event) { - if (event.target === event.currentTarget) { - doc.removeEventListener("DOMContentLoaded", onReady, true); - resolve(doc); - } - }, true); - }); -} - -/** - * Returns a Promise which resolves when the given document is fully - * loaded. - * - * @param {Document} doc The document to await the load of. - * @returns {Promise<Document>} - */ -function promiseDocumentLoaded(doc) { - if (doc.readyState == "complete") { - return Promise.resolve(doc); - } - - return new Promise(resolve => { - doc.defaultView.addEventListener("load", function onReady(event) { - doc.defaultView.removeEventListener("load", onReady); - resolve(doc); - }); - }); -} - -/** - * Returns a Promise which resolves when the given event is dispatched to the - * given element. - * - * @param {Element} element - * The element on which to listen. - * @param {string} eventName - * The event to listen for. - * @param {boolean} [useCapture = true] - * If true, listen for the even in the capturing rather than - * bubbling phase. - * @param {Event} [test] - * An optional test function which, when called with the - * observer's subject and data, should return true if this is the - * expected event, false otherwise. - * @returns {Promise<Event>} - */ -function promiseEvent(element, eventName, useCapture = true, test = event => true) { - return new Promise(resolve => { - function listener(event) { - if (test(event)) { - element.removeEventListener(eventName, listener, useCapture); - resolve(event); - } - } - element.addEventListener(eventName, listener, useCapture); - }); -} - -/** - * Returns a Promise which resolves the given observer topic has been - * observed. - * - * @param {string} topic - * The topic to observe. - * @param {function(nsISupports, string)} [test] - * An optional test function which, when called with the - * observer's subject and data, should return true if this is the - * expected notification, false otherwise. - * @returns {Promise<object>} - */ -function promiseObserved(topic, test = () => true) { - return new Promise(resolve => { - let observer = (subject, topic, data) => { - if (test(subject, data)) { - Services.obs.removeObserver(observer, topic); - resolve({subject, data}); - } - }; - Services.obs.addObserver(observer, topic, false); - }); -} - -function getMessageManager(target) { - if (target instanceof Ci.nsIFrameLoaderOwner) { - return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; - } - return target.QueryInterface(Ci.nsIMessageSender); -} - -function flushJarCache(jarFile) { - Services.obs.notifyObservers(jarFile, "flush-cache-entry", null); -} - -const PlatformInfo = Object.freeze({ - os: (function() { - let os = AppConstants.platform; - if (os == "macosx") { - os = "mac"; - } - return os; - })(), - arch: (function() { - let abi = Services.appinfo.XPCOMABI; - let [arch] = abi.split("-"); - if (arch == "x86") { - arch = "x86-32"; - } else if (arch == "x86_64") { - arch = "x86-64"; - } - return arch; - })(), -}); - -function detectLanguage(text) { - return LanguageDetector.detectLanguage(text).then(result => ({ - isReliable: result.confident, - languages: result.languages.map(lang => { - return { - language: lang.languageCode, - percentage: lang.percent, - }; - }), - })); -} - -/** - * Convert any of several different representations of a date/time to a Date object. - * Accepts several formats: - * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as - * either a number or a string. - * - * @param {Date|string|number} date - * The date to convert. - * @returns {Date} - * A Date object - */ -function normalizeTime(date) { - // Of all the formats we accept the "number of milliseconds since the epoch as a string" - // is an outlier, everything else can just be passed directly to the Date constructor. - return new Date((typeof date == "string" && /^\d+$/.test(date)) - ? parseInt(date, 10) : date); -} - -const stylesheetMap = new DefaultMap(url => { - let uri = NetUtil.newURI(url); - return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET); -}); - -/** - * Defines a lazy getter for the given property on the given object. The - * first time the property is accessed, the return value of the getter - * is defined on the current `this` object with the given property name. - * Importantly, this means that a lazy getter defined on an object - * prototype will be invoked separately for each object instance that - * it's accessed on. - * - * @param {object} object - * The prototype object on which to define the getter. - * @param {string|Symbol} prop - * The property name for which to define the getter. - * @param {function} getter - * The function to call in order to generate the final property - * value. - */ -function defineLazyGetter(object, prop, getter) { - let redefine = (obj, value) => { - Object.defineProperty(obj, prop, { - enumerable: true, - configurable: true, - writable: true, - value, - }); - return value; - }; - - Object.defineProperty(object, prop, { - enumerable: true, - configurable: true, - - get() { - return redefine(this, getter.call(this)); - }, - - set(value) { - redefine(this, value); - }, - }); -} - -function findPathInObject(obj, path, printErrors = true) { - let parent; - for (let elt of path.split(".")) { - if (!obj || !(elt in obj)) { - if (printErrors) { - Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`); - } - return null; - } - - parent = obj; - obj = obj[elt]; - } - - if (typeof obj === "function") { - return obj.bind(parent); - } - return obj; -} - -/** - * Acts as a proxy for a message manager or message manager owner, and - * tracks docShell swaps so that messages are always sent to the same - * receiver, even if it is moved to a different <browser>. - * - * @param {nsIMessageSender|Element} target - * The target message manager on which to send messages, or the - * <browser> element which owns it. - */ -class MessageManagerProxy { - constructor(target) { - this.listeners = new DefaultMap(() => new Map()); - - if (target instanceof Ci.nsIMessageSender) { - Object.defineProperty(this, "messageManager", { - value: target, - configurable: true, - writable: true, - }); - } else { - this.addListeners(target); - } - } - - /** - * Disposes of the proxy object, removes event listeners, and drops - * all references to the underlying message manager. - * - * Must be called before the last reference to the proxy is dropped, - * unless the underlying message manager or <browser> is also being - * destroyed. - */ - dispose() { - if (this.eventTarget) { - this.removeListeners(this.eventTarget); - this.eventTarget = null; - } else { - this.messageManager = null; - } - } - - /** - * Returns true if the given target is the same as, or owns, the given - * message manager. - * - * @param {nsIMessageSender|MessageManagerProxy|Element} target - * The message manager, MessageManagerProxy, or <browser> - * element agaisnt which to match. - * @param {nsIMessageSender} messageManager - * The message manager against which to match `target`. - * - * @returns {boolean} - * True if `messageManager` is the same object as `target`, or - * `target` is a MessageManagerProxy or <browser> element that - * is tied to it. - */ - static matches(target, messageManager) { - return target === messageManager || target.messageManager === messageManager; - } - - /** - * @property {nsIMessageSender|null} messageManager - * The message manager that is currently being proxied. This - * may change during the life of the proxy object, so should - * not be stored elsewhere. - */ - get messageManager() { - return this.eventTarget && this.eventTarget.messageManager; - } - - /** - * Sends a message on the proxied message manager. - * - * @param {array} args - * Arguments to be passed verbatim to the underlying - * sendAsyncMessage method. - * @returns {undefined} - */ - sendAsyncMessage(...args) { - if (this.messageManager) { - return this.messageManager.sendAsyncMessage(...args); - } - /* globals uneval */ - Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`); - } - - /** - * Adds a message listener to the current message manager, and - * transfers it to the new message manager after a docShell swap. - * - * @param {string} message - * The name of the message to listen for. - * @param {nsIMessageListener} listener - * The listener to add. - * @param {boolean} [listenWhenClosed = false] - * If true, the listener will receive messages which were sent - * after the remote side of the listener began closing. - */ - addMessageListener(message, listener, listenWhenClosed = false) { - this.messageManager.addMessageListener(message, listener, listenWhenClosed); - this.listeners.get(message).set(listener, listenWhenClosed); - } - - /** - * Adds a message listener from the current message manager. - * - * @param {string} message - * The name of the message to stop listening for. - * @param {nsIMessageListener} listener - * The listener to remove. - */ - removeMessageListener(message, listener) { - this.messageManager.removeMessageListener(message, listener); - - let listeners = this.listeners.get(message); - listeners.delete(listener); - if (!listeners.size) { - this.listeners.delete(message); - } - } - - /** - * @private - * Iterates over all of the currently registered message listeners. - */ - * iterListeners() { - for (let [message, listeners] of this.listeners) { - for (let [listener, listenWhenClosed] of listeners) { - yield {message, listener, listenWhenClosed}; - } - } - } - - /** - * @private - * Adds docShell swap listeners to the message manager owner. - * - * @param {Element} target - * The target element. - */ - addListeners(target) { - target.addEventListener("SwapDocShells", this); - - for (let {message, listener, listenWhenClosed} of this.iterListeners()) { - target.addMessageListener(message, listener, listenWhenClosed); - } - - this.eventTarget = target; - } - - /** - * @private - * Removes docShell swap listeners to the message manager owner. - * - * @param {Element} target - * The target element. - */ - removeListeners(target) { - target.removeEventListener("SwapDocShells", this); - - for (let {message, listener} of this.iterListeners()) { - target.removeMessageListener(message, listener); - } - } - - handleEvent(event) { - if (event.type == "SwapDocShells") { - this.removeListeners(this.eventTarget); - this.addListeners(event.detail); - } - } -} - -this.ExtensionUtils = { - defineLazyGetter, - detectLanguage, - extend, - findPathInObject, - flushJarCache, - getConsole, - getInnerWindowID, - getMessageManager, - getUniqueId, - ignoreEvent, - injectAPI, - instanceOf, - normalizeTime, - promiseDocumentLoaded, - promiseDocumentReady, - promiseEvent, - promiseObserved, - runSafe, - runSafeSync, - runSafeSyncWithoutClone, - runSafeWithoutClone, - stylesheetMap, - DefaultMap, - DefaultWeakMap, - EventEmitter, - EventManager, - ExtensionError, - IconDetails, - LocaleData, - MessageManagerProxy, - PlatformInfo, - SingletonEventManager, - SpreadArgs, -}; diff --git a/toolkit/components/webextensions/ExtensionXPCShellUtils.jsm b/toolkit/components/webextensions/ExtensionXPCShellUtils.jsm deleted file mode 100644 index 339709a19..000000000 --- a/toolkit/components/webextensions/ExtensionXPCShellUtils.jsm +++ /dev/null @@ -1,306 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionTestUtils"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Components.utils.import("resource://gre/modules/Task.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Extension", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyGetter(this, "Management", () => { - const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); - return Management; -}); - -/* exported ExtensionTestUtils */ - -let BASE_MANIFEST = Object.freeze({ - "applications": Object.freeze({ - "gecko": Object.freeze({ - "id": "test@web.ext", - }), - }), - - "manifest_version": 2, - - "name": "name", - "version": "0", -}); - -class ExtensionWrapper { - constructor(extension, testScope) { - this.extension = extension; - this.testScope = testScope; - - this.state = "uninitialized"; - - this.testResolve = null; - this.testDone = new Promise(resolve => { this.testResolve = resolve; }); - - this.messageHandler = new Map(); - this.messageAwaiter = new Map(); - - this.messageQueue = new Set(); - - this.attachListeners(); - - this.testScope.do_register_cleanup(() => { - if (this.messageQueue.size) { - let names = Array.from(this.messageQueue, ([msg]) => msg); - this.testScope.equal(JSON.stringify(names), "[]", "message queue is empty"); - } - if (this.messageAwaiter.size) { - let names = Array.from(this.messageAwaiter.keys()); - this.testScope.equal(JSON.stringify(names), "[]", "no tasks awaiting on messages"); - } - }); - - this.testScope.do_register_cleanup(() => { - if (this.state == "pending" || this.state == "running") { - this.testScope.equal(this.state, "unloaded", "Extension left running at test shutdown"); - return this.unload(); - } else if (extension.state == "unloading") { - this.testScope.equal(this.state, "unloaded", "Extension not fully unloaded at test shutdown"); - } - }); - - this.testScope.do_print(`Extension loaded`); - } - - attachListeners() { - /* eslint-disable mozilla/balanced-listeners */ - this.extension.on("test-eq", (kind, pass, msg, expected, actual) => { - this.testScope.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`); - }); - this.extension.on("test-log", (kind, pass, msg) => { - this.testScope.do_print(msg); - }); - this.extension.on("test-result", (kind, pass, msg) => { - this.testScope.ok(pass, msg); - }); - this.extension.on("test-done", (kind, pass, msg, expected, actual) => { - this.testScope.ok(pass, msg); - this.testResolve(msg); - }); - - this.extension.on("test-message", (kind, msg, ...args) => { - let handler = this.messageHandler.get(msg); - if (handler) { - handler(...args); - } else { - this.messageQueue.add([msg, ...args]); - this.checkMessages(); - } - }); - /* eslint-enable mozilla/balanced-listeners */ - } - - startup() { - if (this.state != "uninitialized") { - throw new Error("Extension already started"); - } - this.state = "pending"; - - return this.extension.startup().then( - result => { - this.state = "running"; - - return result; - }, - error => { - this.state = "failed"; - - return Promise.reject(error); - }); - } - - unload() { - if (this.state != "running") { - throw new Error("Extension not running"); - } - this.state = "unloading"; - - this.extension.shutdown(); - - this.state = "unloaded"; - - return Promise.resolve(); - } - - /* - * This method marks the extension unloading without actually calling - * shutdown, since shutting down a MockExtension causes it to be uninstalled. - * - * Normally you shouldn't need to use this unless you need to test something - * that requires a restart, such as updates. - */ - markUnloaded() { - if (this.state != "running") { - throw new Error("Extension not running"); - } - this.state = "unloaded"; - - return Promise.resolve(); - } - - sendMessage(...args) { - this.extension.testMessage(...args); - } - - awaitFinish(msg) { - return this.testDone.then(actual => { - if (msg) { - this.testScope.equal(actual, msg, "test result correct"); - } - return actual; - }); - } - - checkMessages() { - for (let message of this.messageQueue) { - let [msg, ...args] = message; - - let listener = this.messageAwaiter.get(msg); - if (listener) { - this.messageQueue.delete(message); - this.messageAwaiter.delete(msg); - - listener.resolve(...args); - return; - } - } - } - - checkDuplicateListeners(msg) { - if (this.messageHandler.has(msg) || this.messageAwaiter.has(msg)) { - throw new Error("only one message handler allowed"); - } - } - - awaitMessage(msg) { - return new Promise(resolve => { - this.checkDuplicateListeners(msg); - - this.messageAwaiter.set(msg, {resolve}); - this.checkMessages(); - }); - } - - onMessage(msg, callback) { - this.checkDuplicateListeners(msg); - this.messageHandler.set(msg, callback); - } -} - -var ExtensionTestUtils = { - BASE_MANIFEST, - - normalizeManifest: Task.async(function* (manifest, baseManifest = BASE_MANIFEST) { - yield Management.lazyInit(); - - let errors = []; - let context = { - url: null, - - logError: error => { - errors.push(error); - }, - - preprocessors: {}, - }; - - manifest = Object.assign({}, baseManifest, manifest); - - let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context); - normalized.errors = errors; - - return normalized; - }), - - currentScope: null, - - profileDir: null, - - init(scope) { - this.currentScope = scope; - - this.profileDir = scope.do_get_profile(); - - // We need to load at least one frame script into every message - // manager to ensure that the scriptable wrapper for its global gets - // created before we try to access it externally. If we don't, we - // fail sanity checks on debug builds the first time we try to - // create a wrapper, because we should never have a global without a - // cached wrapper. - Services.mm.loadFrameScript("data:text/javascript,//", true); - - - let tmpD = this.profileDir.clone(); - tmpD.append("tmp"); - tmpD.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - - let dirProvider = { - getFile(prop, persistent) { - persistent.value = false; - if (prop == "TmpD") { - return tmpD.clone(); - } - return null; - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]), - }; - Services.dirsvc.registerProvider(dirProvider); - - - scope.do_register_cleanup(() => { - tmpD.remove(true); - Services.dirsvc.unregisterProvider(dirProvider); - - this.currentScope = null; - }); - }, - - addonManagerStarted: false, - - mockAppInfo() { - const {updateAppInfo} = Cu.import("resource://testing-common/AppInfo.jsm", {}); - updateAppInfo({ - ID: "xpcshell@tests.mozilla.org", - name: "XPCShell", - version: "48", - platformVersion: "48", - }); - }, - - startAddonManager() { - if (this.addonManagerStarted) { - return; - } - this.addonManagerStarted = true; - this.mockAppInfo(); - - let manager = Cc["@mozilla.org/addons/integration;1"].getService(Ci.nsIObserver) - .QueryInterface(Ci.nsITimerCallback); - manager.observe(null, "addons-startup", null); - }, - - loadExtension(data) { - let extension = Extension.generate(data); - - return new ExtensionWrapper(extension, this.currentScope); - }, -}; diff --git a/toolkit/components/webextensions/LegacyExtensionsUtils.jsm b/toolkit/components/webextensions/LegacyExtensionsUtils.jsm deleted file mode 100644 index e8d276fe9..000000000 --- a/toolkit/components/webextensions/LegacyExtensionsUtils.jsm +++ /dev/null @@ -1,250 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["LegacyExtensionsUtils"]; - -/* exported LegacyExtensionsUtils, LegacyExtensionContext */ - -/** - * This file exports helpers for Legacy Extensions that want to embed a webextensions - * and exchange messages with the embedded WebExtension. - */ - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Extension", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -Cu.import("resource://gre/modules/ExtensionChild.jsm"); -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); - -var { - BaseContext, -} = ExtensionCommon; - -var { - Messenger, -} = ExtensionChild; - -/** - * Instances created from this class provide to a legacy extension - * a simple API to exchange messages with a webextension. - */ -var LegacyExtensionContext = class extends BaseContext { - /** - * Create a new LegacyExtensionContext given a target Extension instance. - * - * @param {Extension} targetExtension - * The webextension instance associated with this context. This will be the - * instance of the newly created embedded webextension when this class is - * used through the EmbeddedWebExtensionsUtils. - */ - constructor(targetExtension) { - super("legacy_extension", targetExtension); - - // Legacy Extensions (xul overlays, bootstrap restartless and Addon SDK) - // runs with a systemPrincipal. - let addonPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); - Object.defineProperty( - this, "principal", - {value: addonPrincipal, enumerable: true, configurable: true} - ); - - let cloneScope = Cu.Sandbox(this.principal, {}); - Cu.setSandboxMetadata(cloneScope, {addonId: targetExtension.id}); - Object.defineProperty( - this, "cloneScope", - {value: cloneScope, enumerable: true, configurable: true, writable: true} - ); - - let sender = {id: targetExtension.id}; - let filter = {extensionId: targetExtension.id}; - // Legacy addons live in the main process. Messages from other addons are - // Messages from WebExtensions are sent to the main process and forwarded via - // the parent process manager to the legacy extension. - this.messenger = new Messenger(this, [Services.cpmm], sender, filter); - - this.api = { - browser: { - runtime: { - onConnect: this.messenger.onConnect("runtime.onConnect"), - onMessage: this.messenger.onMessage("runtime.onMessage"), - }, - }, - }; - } - - /** - * This method is called when the extension shuts down or is unloaded, - * and it nukes the cloneScope sandbox, if any. - */ - unload() { - if (this.unloaded) { - throw new Error("Error trying to unload LegacyExtensionContext twice."); - } - super.unload(); - Cu.nukeSandbox(this.cloneScope); - this.cloneScope = null; - } -}; - -var EmbeddedExtensionManager; - -/** - * Instances of this class are used internally by the exported EmbeddedWebExtensionsUtils - * to manage the embedded webextension instance and the related LegacyExtensionContext - * instance used to exchange messages with it. - */ -class EmbeddedExtension { - /** - * Create a new EmbeddedExtension given the add-on id and the base resource URI of the - * container add-on (the webextension resources will be loaded from the "webextension/" - * subdir of the base resource URI for the legacy extension add-on). - * - * @param {Object} containerAddonParams - * An object with the following properties: - * @param {string} containerAddonParams.id - * The Add-on id of the Legacy Extension which will contain the embedded webextension. - * @param {nsIURI} containerAddonParams.resourceURI - * The nsIURI of the Legacy Extension container add-on. - */ - constructor({id, resourceURI}) { - this.addonId = id; - this.resourceURI = resourceURI; - - // Setup status flag. - this.started = false; - } - - /** - * Start the embedded webextension. - * - * @returns {Promise<LegacyContextAPI>} A promise which resolve to the API exposed to the - * legacy context. - */ - startup() { - if (this.started) { - return Promise.reject(new Error("This embedded extension has already been started")); - } - - // Setup the startup promise. - this.startupPromise = new Promise((resolve, reject) => { - let embeddedExtensionURI = Services.io.newURI("webextension/", null, this.resourceURI); - - // This is the instance of the WebExtension embedded in the hybrid add-on. - this.extension = new Extension({ - id: this.addonId, - resourceURI: embeddedExtensionURI, - }); - - // This callback is register to the "startup" event, emitted by the Extension instance - // after the extension manifest.json has been loaded without any errors, but before - // starting any of the defined contexts (which give the legacy part a chance to subscribe - // runtime.onMessage/onConnect listener before the background page has been loaded). - const onBeforeStarted = () => { - this.extension.off("startup", onBeforeStarted); - - // Resolve the startup promise and reset the startupError. - this.started = true; - this.startupPromise = null; - - // Create the legacy extension context, the legacy container addon - // needs to use it before the embedded webextension startup, - // because it is supposed to be used during the legacy container startup - // to subscribe its message listeners (which are supposed to be able to - // receive any message that the embedded part can try to send to it - // during its startup). - this.context = new LegacyExtensionContext(this.extension); - - // Destroy the LegacyExtensionContext cloneScope when - // the embedded webextensions is unloaded. - this.extension.callOnClose({ - close: () => { - this.context.unload(); - }, - }); - - // resolve startupPromise to execute any pending shutdown that has been - // chained to it. - resolve(this.context.api); - }; - - this.extension.on("startup", onBeforeStarted); - - // Run ambedded extension startup and catch any error during embedded extension - // startup. - this.extension.startup().catch((err) => { - this.started = false; - this.startupPromise = null; - this.extension.off("startup", onBeforeStarted); - - reject(err); - }); - }); - - return this.startupPromise; - } - - /** - * Shuts down the embedded webextension. - * - * @returns {Promise<void>} a promise that is resolved when the shutdown has been done - */ - shutdown() { - EmbeddedExtensionManager.untrackEmbeddedExtension(this); - - // If there is a pending startup, wait to be completed and then shutdown. - if (this.startupPromise) { - return this.startupPromise.then(() => { - this.extension.shutdown(); - }); - } - - // Run shutdown now if the embedded webextension has been correctly started - if (this.extension && this.started && !this.extension.hasShutdown) { - this.extension.shutdown(); - } - - return Promise.resolve(); - } -} - -// Keep track on the created EmbeddedExtension instances and destroy -// them when their container addon is going to be disabled or uninstalled. -EmbeddedExtensionManager = { - // Map of the existent EmbeddedExtensions instances by addon id. - embeddedExtensionsByAddonId: new Map(), - - untrackEmbeddedExtension(embeddedExtensionInstance) { - // Remove this instance from the tracked embedded extensions - let id = embeddedExtensionInstance.addonId; - if (this.embeddedExtensionsByAddonId.get(id) == embeddedExtensionInstance) { - this.embeddedExtensionsByAddonId.delete(id); - } - }, - - getEmbeddedExtensionFor({id, resourceURI}) { - let embeddedExtension = this.embeddedExtensionsByAddonId.get(id); - - if (!embeddedExtension) { - embeddedExtension = new EmbeddedExtension({id, resourceURI}); - // Keep track of the embedded extension instance. - this.embeddedExtensionsByAddonId.set(id, embeddedExtension); - } - - return embeddedExtension; - }, -}; - -this.LegacyExtensionsUtils = { - getEmbeddedExtensionFor: (addon) => { - return EmbeddedExtensionManager.getEmbeddedExtensionFor(addon); - }, -}; diff --git a/toolkit/components/webextensions/MessageChannel.jsm b/toolkit/components/webextensions/MessageChannel.jsm deleted file mode 100644 index c5b326405..000000000 --- a/toolkit/components/webextensions/MessageChannel.jsm +++ /dev/null @@ -1,797 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -/** - * This module provides wrappers around standard message managers to - * simplify bidirectional communication. It currently allows a caller to - * send a message to a single listener, and receive a reply. If there - * are no matching listeners, or the message manager disconnects before - * a reply is received, the caller is returned an error. - * - * The listener end may specify filters for the messages it wishes to - * receive, and the sender end likewise may specify recipient tags to - * match the filters. - * - * The message handler on the listener side may return its response - * value directly, or may return a promise, the resolution or rejection - * of which will be returned instead. The sender end likewise receives a - * promise which resolves or rejects to the listener's response. - * - * - * A basic setup works something like this: - * - * A content script adds a message listener to its global - * nsIContentFrameMessageManager, with an appropriate set of filters: - * - * { - * init(messageManager, window, extensionID) { - * this.window = window; - * - * MessageChannel.addListener( - * messageManager, "ContentScript:TouchContent", - * this); - * - * this.messageFilterStrict = { - * innerWindowID: getInnerWindowID(window), - * extensionID: extensionID, - * }; - * - * this.messageFilterPermissive = { - * outerWindowID: getOuterWindowID(window), - * }; - * }, - * - * receiveMessage({ target, messageName, sender, recipient, data }) { - * if (messageName == "ContentScript:TouchContent") { - * return new Promise(resolve => { - * this.touchWindow(data.touchWith, result => { - * resolve({ touchResult: result }); - * }); - * }); - * } - * }, - * }; - * - * A script in the parent process sends a message to the content process - * via a tab message manager, including recipient tags to match its - * filter, and an optional sender tag to identify itself: - * - * let data = { touchWith: "pencil" }; - * let sender = { extensionID, contextID }; - * let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID }; - * - * MessageChannel.sendMessage( - * tab.linkedBrowser.messageManager, "ContentScript:TouchContent", - * data, {recipient, sender} - * ).then(result => { - * alert(result.touchResult); - * }); - * - * Since the lifetimes of message senders and receivers may not always - * match, either side of the message channel may cancel pending - * responses which match its sender or recipient tags. - * - * For the above client, this might be done from an - * inner-window-destroyed observer, when its target scope is destroyed: - * - * observe(subject, topic, data) { - * if (topic == "inner-window-destroyed") { - * let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - * - * MessageChannel.abortResponses({ innerWindowID }); - * } - * }, - * - * From the parent, it may be done when its context is being destroyed: - * - * onDestroy() { - * MessageChannel.abortResponses({ - * extensionID: this.extensionID, - * contextID: this.contextID, - * }); - * }, - * - */ - -this.EXPORTED_SYMBOLS = ["MessageChannel"]; - -/* globals MessageChannel */ - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", - "resource://gre/modules/ExtensionUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", - "resource://gre/modules/PromiseUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); - -XPCOMUtils.defineLazyGetter(this, "MessageManagerProxy", - () => ExtensionUtils.MessageManagerProxy); - -/** - * Handles the mapping and dispatching of messages to their registered - * handlers. There is one broker per message manager and class of - * messages. Each class of messages is mapped to one native message - * name, e.g., "MessageChannel:Message", and is dispatched to handlers - * based on an internal message name, e.g., "Extension:ExecuteScript". - */ -class FilteringMessageManager { - /** - * @param {string} messageName - * The name of the native message this broker listens for. - * @param {function} callback - * A function which is called for each message after it has been - * mapped to its handler. The function receives two arguments: - * - * result: - * An object containing either a `handler` or an `error` property. - * If no error occurs, `handler` will be a matching handler that - * was registered by `addHandler`. Otherwise, the `error` property - * will contain an object describing the error. - * - * data: - * An object describing the message, as defined in - * `MessageChannel.addListener`. - * @param {nsIMessageListenerManager} messageManager - */ - constructor(messageName, callback, messageManager) { - this.messageName = messageName; - this.callback = callback; - this.messageManager = messageManager; - - this.messageManager.addMessageListener(this.messageName, this, true); - - this.handlers = new Map(); - } - - /** - * Receives a message from our message manager, maps it to a handler, and - * passes the result to our message callback. - */ - receiveMessage({data, target}) { - let handlers = Array.from(this.getHandlers(data.messageName, data.sender, data.recipient)); - - data.target = target; - this.callback(handlers, data); - } - - /** - * Iterates over all handlers for the given message name. If `recipient` - * is provided, only iterates over handlers whose filters match it. - * - * @param {string|number} messageName - * The message for which to return handlers. - * @param {object} sender - * The sender data on which to filter handlers. - * @param {object} recipient - * The recipient data on which to filter handlers. - */ - * getHandlers(messageName, sender, recipient) { - let handlers = this.handlers.get(messageName) || new Set(); - for (let handler of handlers) { - if (MessageChannel.matchesFilter(handler.messageFilterStrict || {}, recipient) && - MessageChannel.matchesFilter(handler.messageFilterPermissive || {}, recipient, false) && - (!handler.filterMessage || handler.filterMessage(sender, recipient))) { - yield handler; - } - } - } - - /** - * Registers a handler for the given message. - * - * @param {string} messageName - * The internal message name for which to register the handler. - * @param {object} handler - * An opaque handler object. The object may have a - * `messageFilterStrict` and/or a `messageFilterPermissive` - * property and/or a `filterMessage` method on which to filter messages. - * - * Final dispatching is handled by the message callback passed to - * the constructor. - */ - addHandler(messageName, handler) { - if (!this.handlers.has(messageName)) { - this.handlers.set(messageName, new Set()); - } - - this.handlers.get(messageName).add(handler); - } - - /** - * Unregisters a handler for the given message. - * - * @param {string} messageName - * The internal message name for which to unregister the handler. - * @param {object} handler - * The handler object to unregister. - */ - removeHandler(messageName, handler) { - this.handlers.get(messageName).delete(handler); - } -} - -/** - * Manages mappings of message managers to their corresponding message - * brokers. Brokers are lazily created for each message manager the - * first time they are accessed. In the case of content frame message - * managers, they are also automatically destroyed when the frame - * unload event fires. - */ -class FilteringMessageManagerMap extends Map { - // Unfortunately, we can't use a WeakMap for this, because message - // managers do not support preserved wrappers. - - /** - * @param {string} messageName - * The native message name passed to `FilteringMessageManager` constructors. - * @param {function} callback - * The message callback function passed to - * `FilteringMessageManager` constructors. - */ - constructor(messageName, callback) { - super(); - - this.messageName = messageName; - this.callback = callback; - } - - /** - * Returns, and possibly creates, a message broker for the given - * message manager. - * - * @param {nsIMessageListenerManager} target - * The message manager for which to return a broker. - * - * @returns {FilteringMessageManager} - */ - get(target) { - if (this.has(target)) { - return super.get(target); - } - - let broker = new FilteringMessageManager(this.messageName, this.callback, target); - this.set(target, broker); - - if (target instanceof Ci.nsIDOMEventTarget) { - let onUnload = event => { - target.removeEventListener("unload", onUnload); - this.delete(target); - }; - target.addEventListener("unload", onUnload); - } - - return broker; - } -} - -const MESSAGE_MESSAGE = "MessageChannel:Message"; -const MESSAGE_RESPONSE = "MessageChannel:Response"; - -this.MessageChannel = { - init() { - Services.obs.addObserver(this, "message-manager-close", false); - Services.obs.addObserver(this, "message-manager-disconnect", false); - - this.messageManagers = new FilteringMessageManagerMap( - MESSAGE_MESSAGE, this._handleMessage.bind(this)); - - this.responseManagers = new FilteringMessageManagerMap( - MESSAGE_RESPONSE, this._handleResponse.bind(this)); - - /** - * Contains a list of pending responses, either waiting to be - * received or waiting to be sent. @see _addPendingResponse - */ - this.pendingResponses = new Set(); - }, - - RESULT_SUCCESS: 0, - RESULT_DISCONNECTED: 1, - RESULT_NO_HANDLER: 2, - RESULT_MULTIPLE_HANDLERS: 3, - RESULT_ERROR: 4, - RESULT_NO_RESPONSE: 5, - - REASON_DISCONNECTED: { - result: this.RESULT_DISCONNECTED, - message: "Message manager disconnected", - }, - - /** - * Specifies that only a single listener matching the specified - * recipient tag may be listening for the given message, at the other - * end of the target message manager. - * - * If no matching listeners exist, a RESULT_NO_HANDLER error will be - * returned. If multiple matching listeners exist, a - * RESULT_MULTIPLE_HANDLERS error will be returned. - */ - RESPONSE_SINGLE: 0, - - /** - * If multiple message managers matching the specified recipient tag - * are listening for a message, all listeners are notified, but only - * the first response or error is returned. - * - * Only handlers which return a value other than `undefined` are - * considered to have responded. Returning a Promise which evaluates - * to `undefined` is interpreted as an explicit response. - * - * If no matching listeners exist, a RESULT_NO_HANDLER error will be - * returned. If no listeners return a response, a RESULT_NO_RESPONSE - * error will be returned. - */ - RESPONSE_FIRST: 1, - - /** - * If multiple message managers matching the specified recipient tag - * are listening for a message, all listeners are notified, and all - * responses are returned as an array, once all listeners have - * replied. - */ - RESPONSE_ALL: 2, - - /** - * Fire-and-forget: The sender of this message does not expect a reply. - */ - RESPONSE_NONE: 3, - - /** - * Initializes message handlers for the given message managers if needed. - * - * @param {Array<nsIMessageListenerManager>} messageManagers - */ - setupMessageManagers(messageManagers) { - for (let mm of messageManagers) { - // This call initializes a FilteringMessageManager for |mm| if needed. - // The FilteringMessageManager must be created to make sure that senders - // of messages that expect a reply, such as MessageChannel:Message, do - // actually receive a default reply even if there are no explicit message - // handlers. - this.messageManagers.get(mm); - } - }, - - /** - * Returns true if the properties of the `data` object match those in - * the `filter` object. Matching is done on a strict equality basis, - * and the behavior varies depending on the value of the `strict` - * parameter. - * - * @param {object} filter - * The filter object to match against. - * @param {object} data - * The data object being matched. - * @param {boolean} [strict=false] - * If true, all properties in the `filter` object have a - * corresponding property in `data` with the same value. If - * false, properties present in both objects must have the same - * value. - * @returns {boolean} True if the objects match. - */ - matchesFilter(filter, data, strict = true) { - if (strict) { - return Object.keys(filter).every(key => { - return key in data && data[key] === filter[key]; - }); - } - return Object.keys(filter).every(key => { - return !(key in data) || data[key] === filter[key]; - }); - }, - - /** - * Adds a message listener to the given message manager. - * - * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets - * The message managers on which to listen. - * @param {string|number} messageName - * The name of the message to listen for. - * @param {MessageReceiver} handler - * The handler to dispatch to. Must be an object with the following - * properties: - * - * receiveMessage: - * A method which is called for each message received by the - * listener. The method takes one argument, an object, with the - * following properties: - * - * messageName: - * The internal message name, as passed to `sendMessage`. - * - * target: - * The message manager which received this message. - * - * channelId: - * The internal ID of the transaction, used to map responses to - * the original sender. - * - * sender: - * An object describing the sender, as passed to `sendMessage`. - * - * recipient: - * An object describing the recipient, as passed to - * `sendMessage`. - * - * data: - * The contents of the message, as passed to `sendMessage`. - * - * The method may return any structured-clone-compatible - * object, which will be returned as a response to the message - * sender. It may also instead return a `Promise`, the - * resolution or rejection value of which will likewise be - * returned to the message sender. - * - * messageFilterStrict: - * An object containing arbitrary properties on which to filter - * received messages. Messages will only be dispatched to this - * object if the `recipient` object passed to `sendMessage` - * matches this filter, as determined by `matchesFilter` with - * `strict=true`. - * - * messageFilterPermissive: - * An object containing arbitrary properties on which to filter - * received messages. Messages will only be dispatched to this - * object if the `recipient` object passed to `sendMessage` - * matches this filter, as determined by `matchesFilter` with - * `strict=false`. - * - * filterMessage: - * An optional function that prevents the handler from handling a - * message by returning `false`. See `getHandlers` for the parameters. - */ - addListener(targets, messageName, handler) { - for (let target of [].concat(targets)) { - this.messageManagers.get(target).addHandler(messageName, handler); - } - }, - - /** - * Removes a message listener from the given message manager. - * - * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets - * The message managers on which to stop listening. - * @param {string|number} messageName - * The name of the message to stop listening for. - * @param {MessageReceiver} handler - * The handler to stop dispatching to. - */ - removeListener(targets, messageName, handler) { - for (let target of [].concat(targets)) { - if (this.messageManagers.has(target)) { - this.messageManagers.get(target).removeHandler(messageName, handler); - } - } - }, - - /** - * Sends a message via the given message manager. Returns a promise which - * resolves or rejects with the return value of the message receiver. - * - * The promise also rejects if there is no matching listener, or the other - * side of the message manager disconnects before the response is received. - * - * @param {nsIMessageSender} target - * The message manager on which to send the message. - * @param {string} messageName - * The name of the message to send, as passed to `addListener`. - * @param {object} data - * A structured-clone-compatible object to send to the message - * recipient. - * @param {object} [options] - * An object containing any of the following properties: - * @param {object} [options.recipient] - * A structured-clone-compatible object to identify the message - * recipient. The object must match the `messageFilterStrict` and - * `messageFilterPermissive` filters defined by recipients in order - * for the message to be received. - * @param {object} [options.sender] - * A structured-clone-compatible object to identify the message - * sender. This object may also be used to avoid delivering the - * message to the sender, and as a filter to prematurely - * abort responses when the sender is being destroyed. - * @see `abortResponses`. - * @param {integer} [options.responseType=RESPONSE_SINGLE] - * Specifies the type of response expected. See the `RESPONSE_*` - * contents for details. - * @returns {Promise} - */ - sendMessage(target, messageName, data, options = {}) { - let sender = options.sender || {}; - let recipient = options.recipient || {}; - let responseType = options.responseType || this.RESPONSE_SINGLE; - - let channelId = ExtensionUtils.getUniqueId(); - let message = {messageName, channelId, sender, recipient, data, responseType}; - - if (responseType == this.RESPONSE_NONE) { - try { - target.sendAsyncMessage(MESSAGE_MESSAGE, message); - } catch (e) { - // Caller is not expecting a reply, so dump the error to the console. - Cu.reportError(e); - return Promise.reject(e); - } - return Promise.resolve(); // Not expecting any reply. - } - - let deferred = PromiseUtils.defer(); - deferred.sender = recipient; - deferred.messageManager = target; - - this._addPendingResponse(deferred); - - // The channel ID is used as the message name when routing responses. - // Add a message listener to the response broker, and remove it once - // we've gotten (or canceled) a response. - let broker = this.responseManagers.get(target); - broker.addHandler(channelId, deferred); - - let cleanup = () => { - broker.removeHandler(channelId, deferred); - }; - deferred.promise.then(cleanup, cleanup); - - try { - target.sendAsyncMessage(MESSAGE_MESSAGE, message); - } catch (e) { - deferred.reject(e); - } - return deferred.promise; - }, - - _callHandlers(handlers, data) { - let responseType = data.responseType; - - // At least one handler is required for all response types but - // RESPONSE_ALL. - if (handlers.length == 0 && responseType != this.RESPONSE_ALL) { - return Promise.reject({result: MessageChannel.RESULT_NO_HANDLER, - message: "No matching message handler"}); - } - - if (responseType == this.RESPONSE_SINGLE) { - if (handlers.length > 1) { - return Promise.reject({result: MessageChannel.RESULT_MULTIPLE_HANDLERS, - message: `Multiple matching handlers for ${data.messageName}`}); - } - - // Note: We use `new Promise` rather than `Promise.resolve` here - // so that errors from the handler are trapped and converted into - // rejected promises. - return new Promise(resolve => { - resolve(handlers[0].receiveMessage(data)); - }); - } - - let responses = handlers.map(handler => { - try { - return handler.receiveMessage(data); - } catch (e) { - return Promise.reject(e); - } - }); - responses = responses.filter(response => response !== undefined); - - switch (responseType) { - case this.RESPONSE_FIRST: - if (responses.length == 0) { - return Promise.reject({result: MessageChannel.RESULT_NO_RESPONSE, - message: "No handler returned a response"}); - } - - return Promise.race(responses); - - case this.RESPONSE_ALL: - return Promise.all(responses); - } - return Promise.reject({message: "Invalid response type"}); - }, - - /** - * Handles dispatching message callbacks from the message brokers to their - * appropriate `MessageReceivers`, and routing the responses back to the - * original senders. - * - * Each handler object is a `MessageReceiver` object as passed to - * `addListener`. - * - * @param {Array<MessageHandler>} handlers - * @param {object} data - * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target - */ - _handleMessage(handlers, data) { - if (data.responseType == this.RESPONSE_NONE) { - handlers.forEach(handler => { - // The sender expects no reply, so dump any errors to the console. - new Promise(resolve => { - resolve(handler.receiveMessage(data)); - }).catch(e => { - Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e); - }); - }); - // Note: Unhandled messages are silently dropped. - return; - } - - let target = new MessageManagerProxy(data.target); - - let deferred = { - sender: data.sender, - messageManager: target, - }; - deferred.promise = new Promise((resolve, reject) => { - deferred.reject = reject; - - this._callHandlers(handlers, data).then(resolve, reject); - }).then( - value => { - let response = { - result: this.RESULT_SUCCESS, - messageName: data.channelId, - recipient: {}, - value, - }; - - target.sendAsyncMessage(MESSAGE_RESPONSE, response); - }, - error => { - let response = { - result: this.RESULT_ERROR, - messageName: data.channelId, - recipient: {}, - error: {}, - }; - - if (error && typeof(error) == "object") { - if (error.result) { - response.result = error.result; - } - // Error objects are not structured-clonable, so just copy - // over the important properties. - for (let key of ["fileName", "filename", "lineNumber", - "columnNumber", "message", "stack", "result"]) { - if (key in error) { - response.error[key] = error[key]; - } - } - } - - target.sendAsyncMessage(MESSAGE_RESPONSE, response); - }).catch(e => { - Cu.reportError(e); - }).then(() => { - target.dispose(); - }); - - this._addPendingResponse(deferred); - }, - - /** - * Handles message callbacks from the response brokers. - * - * Each handler object is a deferred object created by `sendMessage`, and - * should be resolved or rejected based on the contents of the response. - * - * @param {Array<MessageHandler>} handlers - * @param {object} data - * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target - */ - _handleResponse(handlers, data) { - // If we have an error at this point, we have handler to report it to, - // so just log it. - if (handlers.length == 0) { - Cu.reportError(`No matching message response handler for ${data.messageName}`); - } else if (handlers.length > 1) { - Cu.reportError(`Multiple matching response handlers for ${data.messageName}`); - } else if (data.result === this.RESULT_SUCCESS) { - handlers[0].resolve(data.value); - } else { - handlers[0].reject(data.error); - } - }, - - /** - * Adds a pending response to the the `pendingResponses` list. - * - * The response object must be a deferred promise with the following - * properties: - * - * promise: - * The promise object which resolves or rejects when the response - * is no longer pending. - * - * reject: - * A function which, when called, causes the `promise` object to be - * rejected. - * - * sender: - * A sender object, as passed to `sendMessage. - * - * messageManager: - * The message manager the response will be sent or received on. - * - * When the promise resolves or rejects, it will be removed from the - * list. - * - * These values are used to clear pending responses when execution - * contexts are destroyed. - * - * @param {Deferred} deferred - */ - _addPendingResponse(deferred) { - let cleanup = () => { - this.pendingResponses.delete(deferred); - }; - this.pendingResponses.add(deferred); - deferred.promise.then(cleanup, cleanup); - }, - - /** - * Aborts any pending message responses to senders matching the given - * filter. - * - * @param {object} sender - * The object on which to filter senders, as determined by - * `matchesFilter`. - * @param {object} [reason] - * An optional object describing the reason the response was aborted. - * Will be passed to the promise rejection handler of all aborted - * responses. - */ - abortResponses(sender, reason = this.REASON_DISCONNECTED) { - for (let response of this.pendingResponses) { - if (this.matchesFilter(sender, response.sender)) { - response.reject(reason); - } - } - }, - - /** - * Aborts any pending message responses to the broker for the given - * message manager. - * - * @param {nsIMessageListenerManager} target - * The message manager for which to abort brokers. - * @param {object} reason - * An object describing the reason the responses were aborted. - * Will be passed to the promise rejection handler of all aborted - * responses. - */ - abortMessageManager(target, reason) { - for (let response of this.pendingResponses) { - if (MessageManagerProxy.matches(response.messageManager, target)) { - response.reject(reason); - } - } - }, - - observe(subject, topic, data) { - switch (topic) { - case "message-manager-close": - case "message-manager-disconnect": - try { - if (this.responseManagers.has(subject)) { - this.abortMessageManager(subject, this.REASON_DISCONNECTED); - } - } finally { - this.responseManagers.delete(subject); - this.messageManagers.delete(subject); - } - break; - } - }, -}; - -MessageChannel.init(); diff --git a/toolkit/components/webextensions/NativeMessaging.jsm b/toolkit/components/webextensions/NativeMessaging.jsm deleted file mode 100644 index 3d8658a3f..000000000 --- a/toolkit/components/webextensions/NativeMessaging.jsm +++ /dev/null @@ -1,443 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["HostManifestManager", "NativeApp"]; -/* globals NativeApp */ - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {}); - -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", - "resource://gre/modules/AsyncShutdown.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild", - "resource://gre/modules/ExtensionChild.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Subprocess", - "resource://gre/modules/Subprocess.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", - "resource://gre/modules/WindowsRegistry.jsm"); - -const HOST_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_host_manifest.json"; -const VALID_APPLICATION = /^\w+(\.\w+)*$/; - -// For a graceful shutdown (i.e., when the extension is unloaded or when it -// explicitly calls disconnect() on a native port), how long we give the native -// application to exit before we start trying to kill it. (in milliseconds) -const GRACEFUL_SHUTDOWN_TIME = 3000; - -// Hard limits on maximum message size that can be read/written -// These are defined in the native messaging documentation, note that -// the write limit is imposed by the "wire protocol" in which message -// boundaries are defined by preceding each message with its length as -// 4-byte unsigned integer so this is the largest value that can be -// represented. Good luck generating a serialized message that large, -// the practical write limit is likely to be dictated by available memory. -const MAX_READ = 1024 * 1024; -const MAX_WRITE = 0xffffffff; - -// Preferences that can lower the message size limits above, -// used for testing the limits. -const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes"; -const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes"; - -const REGPATH = "Software\\Mozilla\\NativeMessagingHosts"; - -this.HostManifestManager = { - _initializePromise: null, - _lookup: null, - - init() { - if (!this._initializePromise) { - let platform = AppConstants.platform; - if (platform == "win") { - this._lookup = this._winLookup; - } else if (platform == "macosx" || platform == "linux") { - let dirs = [ - Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path, - Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path, - ]; - this._lookup = (application, context) => this._tryPaths(application, dirs, context); - } else { - throw new Error(`Native messaging is not supported on ${AppConstants.platform}`); - } - this._initializePromise = Schemas.load(HOST_MANIFEST_SCHEMA); - } - return this._initializePromise; - }, - - _winLookup(application, context) { - const REGISTRY = Ci.nsIWindowsRegKey; - let regPath = `${REGPATH}\\${application}`; - let path = WindowsRegistry.readRegKey(REGISTRY.ROOT_KEY_CURRENT_USER, - regPath, "", REGISTRY.WOW64_64); - if (!path) { - path = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, - regPath, "", REGISTRY.WOW64_64); - } - if (!path) { - return null; - } - return this._tryPath(path, application, context) - .then(manifest => manifest ? {path, manifest} : null); - }, - - _tryPath(path, application, context) { - return Promise.resolve() - .then(() => OS.File.read(path, {encoding: "utf-8"})) - .then(data => { - let manifest; - try { - manifest = JSON.parse(data); - } catch (ex) { - let msg = `Error parsing native host manifest ${path}: ${ex.message}`; - Cu.reportError(msg); - return null; - } - - let normalized = Schemas.normalize(manifest, "manifest.NativeHostManifest", context); - if (normalized.error) { - Cu.reportError(normalized.error); - return null; - } - manifest = normalized.value; - if (manifest.name != application) { - let msg = `Native host manifest ${path} has name property ${manifest.name} (expected ${application})`; - Cu.reportError(msg); - return null; - } - return normalized.value; - }).catch(ex => { - if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) { - return null; - } - throw ex; - }); - }, - - _tryPaths: Task.async(function* (application, dirs, context) { - for (let dir of dirs) { - let path = OS.Path.join(dir, `${application}.json`); - let manifest = yield this._tryPath(path, application, context); - if (manifest) { - return {path, manifest}; - } - } - return null; - }), - - /** - * Search for a valid native host manifest for the given application name. - * The directories searched and rules for manifest validation are all - * detailed in the native messaging documentation. - * - * @param {string} application The name of the applciation to search for. - * @param {object} context A context object as expected by Schemas.normalize. - * @returns {object} The contents of the validated manifest, or null if - * no valid manifest can be found for this application. - */ - lookupApplication(application, context) { - if (!VALID_APPLICATION.test(application)) { - throw new Error(`Invalid application "${application}"`); - } - return this.init().then(() => this._lookup(application, context)); - }, -}; - -this.NativeApp = class extends EventEmitter { - /** - * @param {BaseContext} context The context that initiated the native app. - * @param {string} application The identifier of the native app. - */ - constructor(context, application) { - super(); - - this.context = context; - this.name = application; - - // We want a close() notification when the window is destroyed. - this.context.callOnClose(this); - - this.proc = null; - this.readPromise = null; - this.sendQueue = []; - this.writePromise = null; - this.sentDisconnect = false; - - this.startupPromise = HostManifestManager.lookupApplication(application, context) - .then(hostInfo => { - // Put the two errors together to not leak information about whether a native - // application is installed to addons that do not have the right permission. - if (!hostInfo || !hostInfo.manifest.allowed_extensions.includes(context.extension.id)) { - throw new context.cloneScope.Error(`This extension does not have permission to use native application ${application} (or the application is not installed)`); - } - - let command = hostInfo.manifest.path; - if (AppConstants.platform == "win") { - // OS.Path.join() ignores anything before the last absolute path - // it sees, so if command is already absolute, it remains unchanged - // here. If it is relative, we get the proper absolute path here. - command = OS.Path.join(OS.Path.dirname(hostInfo.path), command); - } - - let subprocessOpts = { - command: command, - arguments: [hostInfo.path], - workdir: OS.Path.dirname(command), - stderr: "pipe", - }; - return Subprocess.call(subprocessOpts); - }).then(proc => { - this.startupPromise = null; - this.proc = proc; - this._startRead(); - this._startWrite(); - this._startStderrRead(); - }).catch(err => { - this.startupPromise = null; - Cu.reportError(err instanceof Error ? err : err.message); - this._cleanup(err); - }); - } - - /** - * Open a connection to a native messaging host. - * - * @param {BaseContext} context The context associated with the port. - * @param {nsIMessageSender} messageManager The message manager used to send - * and receive messages from the port's creator. - * @param {string} portId A unique internal ID that identifies the port. - * @param {object} sender The object describing the creator of the connection - * request. - * @param {string} application The name of the native messaging host. - */ - static onConnectNative(context, messageManager, portId, sender, application) { - let app = new NativeApp(context, application); - let port = new ExtensionChild.Port(context, messageManager, [Services.mm], "", portId, sender, sender); - app.once("disconnect", (what, err) => port.disconnect(err)); - - /* eslint-disable mozilla/balanced-listeners */ - app.on("message", (what, msg) => port.postMessage(msg)); - /* eslint-enable mozilla/balanced-listeners */ - - port.registerOnMessage(msg => app.send(msg)); - port.registerOnDisconnect(msg => app.close()); - } - - /** - * @param {BaseContext} context The scope from where `message` originates. - * @param {*} message A message from the extension, meant for a native app. - * @returns {ArrayBuffer} An ArrayBuffer that can be sent to the native app. - */ - static encodeMessage(context, message) { - message = context.jsonStringify(message); - let buffer = new TextEncoder().encode(message).buffer; - if (buffer.byteLength > NativeApp.maxWrite) { - throw new context.cloneScope.Error("Write too big"); - } - return buffer; - } - - // A port is definitely "alive" if this.proc is non-null. But we have - // to provide a live port object immediately when connecting so we also - // need to consider a port alive if proc is null but the startupPromise - // is still pending. - get _isDisconnected() { - return (!this.proc && !this.startupPromise); - } - - _startRead() { - if (this.readPromise) { - throw new Error("Entered _startRead() while readPromise is non-null"); - } - this.readPromise = this.proc.stdout.readUint32() - .then(len => { - if (len > NativeApp.maxRead) { - throw new this.context.cloneScope.Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.`); - } - return this.proc.stdout.readJSON(len); - }).then(msg => { - this.emit("message", msg); - this.readPromise = null; - this._startRead(); - }).catch(err => { - if (err.errorCode != Subprocess.ERROR_END_OF_FILE) { - Cu.reportError(err instanceof Error ? err : err.message); - } - this._cleanup(err); - }); - } - - _startWrite() { - if (this.sendQueue.length == 0) { - return; - } - - if (this.writePromise) { - throw new Error("Entered _startWrite() while writePromise is non-null"); - } - - let buffer = this.sendQueue.shift(); - let uintArray = Uint32Array.of(buffer.byteLength); - - this.writePromise = Promise.all([ - this.proc.stdin.write(uintArray.buffer), - this.proc.stdin.write(buffer), - ]).then(() => { - this.writePromise = null; - this._startWrite(); - }).catch(err => { - Cu.reportError(err.message); - this._cleanup(err); - }); - } - - _startStderrRead() { - let proc = this.proc; - let app = this.name; - Task.spawn(function* () { - let partial = ""; - while (true) { - let data = yield proc.stderr.readString(); - if (data.length == 0) { - // We have hit EOF, just stop reading - if (partial) { - Services.console.logStringMessage(`stderr output from native app ${app}: ${partial}`); - } - break; - } - - let lines = data.split(/\r?\n/); - lines[0] = partial + lines[0]; - partial = lines.pop(); - - for (let line of lines) { - Services.console.logStringMessage(`stderr output from native app ${app}: ${line}`); - } - } - }); - } - - send(msg) { - if (this._isDisconnected) { - throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port"); - } - if (Cu.getClassName(msg, true) != "ArrayBuffer") { - // This error cannot be triggered by extensions; it indicates an error in - // our implementation. - throw new Error("The message to the native messaging host is not an ArrayBuffer"); - } - - let buffer = msg; - - if (buffer.byteLength > NativeApp.maxWrite) { - throw new this.context.cloneScope.Error("Write too big"); - } - - this.sendQueue.push(buffer); - if (!this.startupPromise && !this.writePromise) { - this._startWrite(); - } - } - - // Shut down the native application and also signal to the extension - // that the connect has been disconnected. - _cleanup(err) { - this.context.forgetOnClose(this); - - let doCleanup = () => { - // Set a timer to kill the process gracefully after one timeout - // interval and kill it forcefully after two intervals. - let timer = setTimeout(() => { - this.proc.kill(GRACEFUL_SHUTDOWN_TIME); - }, GRACEFUL_SHUTDOWN_TIME); - - let promise = Promise.all([ - this.proc.stdin.close() - .catch(err => { - if (err.errorCode != Subprocess.ERROR_END_OF_FILE) { - throw err; - } - }), - this.proc.wait(), - ]).then(() => { - this.proc = null; - clearTimeout(timer); - }); - - AsyncShutdown.profileBeforeChange.addBlocker( - `Native Messaging: Wait for application ${this.name} to exit`, - promise); - - promise.then(() => { - AsyncShutdown.profileBeforeChange.removeBlocker(promise); - }); - - return promise; - }; - - if (this.proc) { - doCleanup(); - } else if (this.startupPromise) { - this.startupPromise.then(doCleanup); - } - - if (!this.sentDisconnect) { - this.sentDisconnect = true; - if (err && err.errorCode == Subprocess.ERROR_END_OF_FILE) { - err = null; - } - this.emit("disconnect", err); - } - } - - // Called from Context when the extension is shut down. - close() { - this._cleanup(); - } - - sendMessage(msg) { - let responsePromise = new Promise((resolve, reject) => { - this.once("message", (what, msg) => { resolve(msg); }); - this.once("disconnect", (what, err) => { reject(err); }); - }); - - let result = this.startupPromise.then(() => { - this.send(msg); - return responsePromise; - }); - - result.then(() => { - this._cleanup(); - }, () => { - // Prevent the response promise from being reported as an - // unchecked rejection if the startup promise fails. - responsePromise.catch(() => {}); - - this._cleanup(); - }); - - return result; - } -}; - -XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxRead", PREF_MAX_READ, MAX_READ); -XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxWrite", PREF_MAX_WRITE, MAX_WRITE); diff --git a/toolkit/components/webextensions/Schemas.jsm b/toolkit/components/webextensions/Schemas.jsm deleted file mode 100644 index 159211c79..000000000 --- a/toolkit/components/webextensions/Schemas.jsm +++ /dev/null @@ -1,2143 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -const global = this; - -Cu.importGlobalProperties(["URL"]); - -Cu.import("resource://gre/modules/NetUtil.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -var { - DefaultMap, - instanceOf, -} = ExtensionUtils; - -class DeepMap extends DefaultMap { - constructor() { - super(() => new DeepMap()); - } - - getPath(...keys) { - return keys.reduce((map, prop) => map.get(prop), this); - } -} - -XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService", - "@mozilla.org/addons/content-policy;1", - "nsIAddonContentPolicy"); - -this.EXPORTED_SYMBOLS = ["Schemas"]; - -/* globals Schemas, URL */ - -function readJSON(url) { - return new Promise((resolve, reject) => { - NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => { - if (!Components.isSuccessCode(status)) { - // Convert status code to a string - let e = Components.Exception("", status); - reject(new Error(`Error while loading '${url}' (${e.name})`)); - return; - } - try { - let text = NetUtil.readInputStreamToString(inputStream, inputStream.available()); - - // Chrome JSON files include a license comment that we need to - // strip off for this to be valid JSON. As a hack, we just - // look for the first '[' character, which signals the start - // of the JSON content. - let index = text.indexOf("["); - text = text.slice(index); - - resolve(JSON.parse(text)); - } catch (e) { - reject(e); - } - }); - }); -} - -/** - * Defines a lazy getter for the given property on the given object. Any - * security wrappers are waived on the object before the property is - * defined, and the getter and setter methods are wrapped for the target - * scope. - * - * The given getter function is guaranteed to be called only once, even - * if the target scope retrieves the wrapped getter from the property - * descriptor and calls it directly. - * - * @param {object} object - * The object on which to define the getter. - * @param {string|Symbol} prop - * The property name for which to define the getter. - * @param {function} getter - * The function to call in order to generate the final property - * value. - */ -function exportLazyGetter(object, prop, getter) { - object = Cu.waiveXrays(object); - - let redefine = value => { - if (value === undefined) { - delete object[prop]; - } else { - Object.defineProperty(object, prop, { - enumerable: true, - configurable: true, - writable: true, - value, - }); - } - - getter = null; - - return value; - }; - - Object.defineProperty(object, prop, { - enumerable: true, - configurable: true, - - get: Cu.exportFunction(function() { - return redefine(getter.call(this)); - }, object), - - set: Cu.exportFunction(value => { - redefine(value); - }, object), - }); -} - -const POSTPROCESSORS = { - convertImageDataToURL(imageData, context) { - let document = context.cloneScope.document; - let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); - canvas.width = imageData.width; - canvas.height = imageData.height; - canvas.getContext("2d").putImageData(imageData, 0, 0); - - return canvas.toDataURL("image/png"); - }, -}; - -// Parses a regular expression, with support for the Python extended -// syntax that allows setting flags by including the string (?im) -function parsePattern(pattern) { - let flags = ""; - let match = /^\(\?([im]*)\)(.*)/.exec(pattern); - if (match) { - [, flags, pattern] = match; - } - return new RegExp(pattern, flags); -} - -function getValueBaseType(value) { - let t = typeof(value); - if (t == "object") { - if (value === null) { - return "null"; - } else if (Array.isArray(value)) { - return "array"; - } else if (Object.prototype.toString.call(value) == "[object ArrayBuffer]") { - return "binary"; - } - } else if (t == "number") { - if (value % 1 == 0) { - return "integer"; - } - } - return t; -} - -// Methods of Context that are used by Schemas.normalize. These methods can be -// overridden at the construction of Context. -const CONTEXT_FOR_VALIDATION = [ - "checkLoadURL", - "hasPermission", - "logError", -]; - -// Methods of Context that are used by Schemas.inject. -// Callers of Schemas.inject should implement all of these methods. -const CONTEXT_FOR_INJECTION = [ - ...CONTEXT_FOR_VALIDATION, - "shouldInject", - "getImplementation", -]; - -/** - * A context for schema validation and error reporting. This class is only used - * internally within Schemas. - */ -class Context { - /** - * @param {object} params Provides the implementation of this class. - * @param {Array<string>} overridableMethods - */ - constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) { - this.params = params; - - this.path = []; - this.preprocessors = { - localize(value, context) { - return value; - }, - }; - this.postprocessors = POSTPROCESSORS; - this.isChromeCompat = false; - - this.currentChoices = new Set(); - this.choicePathIndex = 0; - - for (let method of overridableMethods) { - if (method in params) { - this[method] = params[method].bind(params); - } - } - - let props = ["preprocessors", "isChromeCompat"]; - for (let prop of props) { - if (prop in params) { - if (prop in this && typeof this[prop] == "object") { - Object.assign(this[prop], params[prop]); - } else { - this[prop] = params[prop]; - } - } - } - } - - get choicePath() { - let path = this.path.slice(this.choicePathIndex); - return path.join("."); - } - - get cloneScope() { - return this.params.cloneScope; - } - - get url() { - return this.params.url; - } - - get principal() { - return this.params.principal || Services.scriptSecurityManager.createNullPrincipal({}); - } - - /** - * Checks whether `url` may be loaded by the extension in this context. - * - * @param {string} url The URL that the extension wished to load. - * @returns {boolean} Whether the context may load `url`. - */ - checkLoadURL(url) { - let ssm = Services.scriptSecurityManager; - try { - ssm.checkLoadURIStrWithPrincipal(this.principal, url, - ssm.DISALLOW_INHERIT_PRINCIPAL); - } catch (e) { - return false; - } - return true; - } - - /** - * Checks whether this context has the given permission. - * - * @param {string} permission - * The name of the permission to check. - * - * @returns {boolean} True if the context has the given permission. - */ - hasPermission(permission) { - return false; - } - - /** - * Returns an error result object with the given message, for return - * by Type normalization functions. - * - * If the context has a `currentTarget` value, this is prepended to - * the message to indicate the location of the error. - * - * @param {string} errorMessage - * The error message which will be displayed when this is the - * only possible matching schema. - * @param {string} choicesMessage - * The message describing the valid what constitutes a valid - * value for this schema, which will be displayed when multiple - * schema choices are available and none match. - * - * A caller may pass `null` to prevent a choice from being - * added, but this should *only* be done from code processing a - * choices type. - * @returns {object} - */ - error(errorMessage, choicesMessage = undefined) { - if (choicesMessage !== null) { - let {choicePath} = this; - if (choicePath) { - choicesMessage = `.${choicePath} must ${choicesMessage}`; - } - - this.currentChoices.add(choicesMessage); - } - - if (this.currentTarget) { - return {error: `Error processing ${this.currentTarget}: ${errorMessage}`}; - } - return {error: errorMessage}; - } - - /** - * Creates an `Error` object belonging to the current unprivileged - * scope. If there is no unprivileged scope associated with this - * context, the message is returned as a string. - * - * If the context has a `currentTarget` value, this is prepended to - * the message, in the same way as for the `error` method. - * - * @param {string} message - * @returns {Error} - */ - makeError(message) { - let {error} = this.error(message); - if (this.cloneScope) { - return new this.cloneScope.Error(error); - } - return error; - } - - /** - * Logs the given error to the console. May be overridden to enable - * custom logging. - * - * @param {Error|string} error - */ - logError(error) { - Cu.reportError(error); - } - - /** - * Returns the name of the value currently being normalized. For a - * nested object, this is usually approximately equivalent to the - * JavaScript property accessor for that property. Given: - * - * { foo: { bar: [{ baz: x }] } } - * - * When processing the value for `x`, the currentTarget is - * 'foo.bar.0.baz' - */ - get currentTarget() { - return this.path.join("."); - } - - /** - * Executes the given callback, and returns an array of choice strings - * passed to {@see #error} during its execution. - * - * @param {function} callback - * @returns {object} - * An object with a `result` property containing the return - * value of the callback, and a `choice` property containing - * an array of choices. - */ - withChoices(callback) { - let {currentChoices, choicePathIndex} = this; - - let choices = new Set(); - this.currentChoices = choices; - this.choicePathIndex = this.path.length; - - try { - let result = callback(); - - return {result, choices: Array.from(choices)}; - } finally { - this.currentChoices = currentChoices; - this.choicePathIndex = choicePathIndex; - - choices = Array.from(choices); - if (choices.length == 1) { - currentChoices.add(choices[0]); - } else if (choices.length) { - let n = choices.length - 1; - choices[n] = `or ${choices[n]}`; - - this.error(null, `must either [${choices.join(", ")}]`); - } - } - } - - /** - * Appends the given component to the `currentTarget` path to indicate - * that it is being processed, calls the given callback function, and - * then restores the original path. - * - * This is used to identify the path of the property being processed - * when reporting type errors. - * - * @param {string} component - * @param {function} callback - * @returns {*} - */ - withPath(component, callback) { - this.path.push(component); - try { - return callback(); - } finally { - this.path.pop(); - } - } -} - -/** - * Holds methods that run the actual implementation of the extension APIs. These - * methods are only called if the extension API invocation matches the signature - * as defined in the schema. Otherwise an error is reported to the context. - */ -class InjectionContext extends Context { - constructor(params) { - super(params, CONTEXT_FOR_INJECTION); - } - - /** - * Check whether the API should be injected. - * - * @abstract - * @param {string} namespace The namespace of the API. This may contain dots, - * e.g. in the case of "devtools.inspectedWindow". - * @param {string} [name] The name of the property in the namespace. - * `null` if we are checking whether the namespace should be injected. - * @param {Array<string>} allowedContexts A list of additional contexts in which - * this API should be available. May include any of: - * "main" - The main chrome browser process. - * "addon" - An addon process. - * "content" - A content process. - * @returns {boolean} Whether the API should be injected. - */ - shouldInject(namespace, name, allowedContexts) { - throw new Error("Not implemented"); - } - - /** - * Generate the implementation for `namespace`.`name`. - * - * @abstract - * @param {string} namespace The full path to the namespace of the API, minus - * the name of the method or property. E.g. "storage.local". - * @param {string} name The name of the method, property or event. - * @returns {SchemaAPIInterface} The implementation of the API. - */ - getImplementation(namespace, name) { - throw new Error("Not implemented"); - } -} - -/** - * The methods in this singleton represent the "format" specifier for - * JSON Schema string types. - * - * Each method either returns a normalized version of the original - * value, or throws an error if the value is not valid for the given - * format. - */ -const FORMATS = { - url(string, context) { - let url = new URL(string).href; - - if (!context.checkLoadURL(url)) { - throw new Error(`Access denied for URL ${url}`); - } - return url; - }, - - relativeUrl(string, context) { - if (!context.url) { - // If there's no context URL, return relative URLs unresolved, and - // skip security checks for them. - try { - new URL(string); - } catch (e) { - return string; - } - } - - let url = new URL(string, context.url).href; - - if (!context.checkLoadURL(url)) { - throw new Error(`Access denied for URL ${url}`); - } - return url; - }, - - strictRelativeUrl(string, context) { - // Do not accept a string which resolves as an absolute URL, or any - // protocol-relative URL. - if (!string.startsWith("//")) { - try { - new URL(string); - } catch (e) { - return FORMATS.relativeUrl(string, context); - } - } - - throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`); - }, - - contentSecurityPolicy(string, context) { - let error = contentPolicyService.validateAddonCSP(string); - if (error != null) { - throw new SyntaxError(error); - } - return string; - }, - - date(string, context) { - // A valid ISO 8601 timestamp. - const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/; - if (!PATTERN.test(string)) { - throw new Error(`Invalid date string ${string}`); - } - // Our pattern just checks the format, we could still have invalid - // values (e.g., month=99 or month=02 and day=31). Let the Date - // constructor do the dirty work of validating. - if (isNaN(new Date(string))) { - throw new Error(`Invalid date string ${string}`); - } - return string; - }, -}; - -// Schema files contain namespaces, and each namespace contains types, -// properties, functions, and events. An Entry is a base class for -// types, properties, functions, and events. -class Entry { - constructor(schema = {}) { - /** - * If set to any value which evaluates as true, this entry is - * deprecated, and any access to it will result in a deprecation - * warning being logged to the browser console. - * - * If the value is a string, it will be appended to the deprecation - * message. If it contains the substring "${value}", it will be - * replaced with a string representation of the value being - * processed. - * - * If the value is any other truthy value, a generic deprecation - * message will be emitted. - */ - this.deprecated = false; - if ("deprecated" in schema) { - this.deprecated = schema.deprecated; - } - - /** - * @property {string} [preprocessor] - * If set to a string value, and a preprocessor of the same is - * defined in the validation context, it will be applied to this - * value prior to any normalization. - */ - this.preprocessor = schema.preprocess || null; - - /** - * @property {string} [postprocessor] - * If set to a string value, and a postprocessor of the same is - * defined in the validation context, it will be applied to this - * value after any normalization. - */ - this.postprocessor = schema.postprocess || null; - - /** - * @property {Array<string>} allowedContexts A list of allowed contexts - * to consider before generating the API. - * These are not parsed by the schema, but passed to `shouldInject`. - */ - this.allowedContexts = schema.allowedContexts || []; - } - - /** - * Preprocess the given value with the preprocessor declared in - * `preprocessor`. - * - * @param {*} value - * @param {Context} context - * @returns {*} - */ - preprocess(value, context) { - if (this.preprocessor) { - return context.preprocessors[this.preprocessor](value, context); - } - return value; - } - - /** - * Postprocess the given result with the postprocessor declared in - * `postprocessor`. - * - * @param {object} result - * @param {Context} context - * @returns {object} - */ - postprocess(result, context) { - if (result.error || !this.postprocessor) { - return result; - } - - let value = context.postprocessors[this.postprocessor](result.value, context); - return {value}; - } - - /** - * Logs a deprecation warning for this entry, based on the value of - * its `deprecated` property. - * - * @param {Context} context - * @param {value} [value] - */ - logDeprecation(context, value = null) { - let message = "This property is deprecated"; - if (typeof(this.deprecated) == "string") { - message = this.deprecated; - if (message.includes("${value}")) { - try { - value = JSON.stringify(value); - } catch (e) { - value = String(value); - } - message = message.replace(/\$\{value\}/g, () => value); - } - } - - context.logError(context.makeError(message)); - } - - /** - * Checks whether the entry is deprecated and, if so, logs a - * deprecation message. - * - * @param {Context} context - * @param {value} [value] - */ - checkDeprecated(context, value = null) { - if (this.deprecated) { - this.logDeprecation(context, value); - } - } - - /** - * Injects JS values for the entry into the extension API - * namespace. The default implementation is to do nothing. - * `context` is used to call the actual implementation - * of a given function or event. - * - * @param {Array<string>} path The API path, e.g. `["storage", "local"]`. - * @param {string} name The method name, e.g. "get". - * @param {object} dest The object where `path`.`name` should be stored. - * @param {InjectionContext} context - */ - inject(path, name, dest, context) { - } -} - -// Corresponds either to a type declared in the "types" section of the -// schema or else to any type object used throughout the schema. -class Type extends Entry { - /** - * @property {Array<string>} EXTRA_PROPERTIES - * An array of extra properties which may be present for - * schemas of this type. - */ - static get EXTRA_PROPERTIES() { - return ["description", "deprecated", "preprocess", "postprocess", "allowedContexts"]; - } - - /** - * Parses the given schema object and returns an instance of this - * class which corresponds to its properties. - * - * @param {object} schema - * A JSON schema object which corresponds to a definition of - * this type. - * @param {Array<string>} path - * The path to this schema object from the root schema, - * corresponding to the property names and array indices - * traversed during parsing in order to arrive at this schema - * object. - * @param {Array<string>} [extraProperties] - * An array of extra property names which are valid for this - * schema in the current context. - * @returns {Type} - * An instance of this type which corresponds to the given - * schema object. - * @static - */ - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - return new this(schema); - } - - /** - * Checks that all of the properties present in the given schema - * object are valid properties for this type, and throws if invalid. - * - * @param {object} schema - * A JSON schema object. - * @param {Array<string>} path - * The path to this schema object from the root schema, - * corresponding to the property names and array indices - * traversed during parsing in order to arrive at this schema - * object. - * @param {Array<string>} [extra] - * An array of extra property names which are valid for this - * schema in the current context. - * @throws {Error} - * An error describing the first invalid property found in the - * schema object. - */ - static checkSchemaProperties(schema, path, extra = []) { - let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]); - - for (let prop of Object.keys(schema)) { - if (!allowedSet.has(prop)) { - throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${schema.id || JSON.stringify(schema)}"`); - } - } - } - - // Takes a value, checks that it has the correct type, and returns a - // "normalized" version of the value. The normalized version will - // include "nulls" in place of omitted optional properties. The - // result of this function is either {error: "Some type error"} or - // {value: <normalized-value>}. - normalize(value, context) { - return context.error("invalid type"); - } - - // Unlike normalize, this function does a shallow check to see if - // |baseType| (one of the possible getValueBaseType results) is - // valid for this type. It returns true or false. It's used to fill - // in optional arguments to functions before actually type checking - - checkBaseType(baseType) { - return false; - } - - // Helper method that simply relies on checkBaseType to implement - // normalize. Subclasses can choose to use it or not. - normalizeBase(type, value, context) { - if (this.checkBaseType(getValueBaseType(value))) { - this.checkDeprecated(context, value); - return {value: this.preprocess(value, context)}; - } - - let choice; - if (/^[aeiou]/.test(type)) { - choice = `be an ${type} value`; - } else { - choice = `be a ${type} value`; - } - - return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`, - choice); - } -} - -// Type that allows any value. -class AnyType extends Type { - normalize(value, context) { - this.checkDeprecated(context, value); - return this.postprocess({value}, context); - } - - checkBaseType(baseType) { - return true; - } -} - -// An untagged union type. -class ChoiceType extends Type { - static get EXTRA_PROPERTIES() { - return ["choices", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let choices = schema.choices.map(t => Schemas.parseSchema(t, path)); - return new this(schema, choices); - } - - constructor(schema, choices) { - super(schema); - this.choices = choices; - } - - extend(type) { - this.choices.push(...type.choices); - - return this; - } - - normalize(value, context) { - this.checkDeprecated(context, value); - - let error; - let {choices, result} = context.withChoices(() => { - for (let choice of this.choices) { - let r = choice.normalize(value, context); - if (!r.error) { - return r; - } - - error = r; - } - }); - - if (result) { - return result; - } - if (choices.length <= 1) { - return error; - } - - let n = choices.length - 1; - choices[n] = `or ${choices[n]}`; - - let message = `Value must either: ${choices.join(", ")}`; - - return context.error(message, null); - } - - checkBaseType(baseType) { - return this.choices.some(t => t.checkBaseType(baseType)); - } -} - -// This is a reference to another type--essentially a typedef. -class RefType extends Type { - static get EXTRA_PROPERTIES() { - return ["$ref", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let ref = schema.$ref; - let ns = path[0]; - if (ref.includes(".")) { - [ns, ref] = ref.split("."); - } - return new this(schema, ns, ref); - } - - // For a reference to a type named T declared in namespace NS, - // namespaceName will be NS and reference will be T. - constructor(schema, namespaceName, reference) { - super(schema); - this.namespaceName = namespaceName; - this.reference = reference; - } - - get targetType() { - let ns = Schemas.namespaces.get(this.namespaceName); - let type = ns.get(this.reference); - if (!type) { - throw new Error(`Internal error: Type ${this.reference} not found`); - } - return type; - } - - normalize(value, context) { - this.checkDeprecated(context, value); - return this.targetType.normalize(value, context); - } - - checkBaseType(baseType) { - return this.targetType.checkBaseType(baseType); - } -} - -class StringType extends Type { - static get EXTRA_PROPERTIES() { - return ["enum", "minLength", "maxLength", "pattern", "format", - ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let enumeration = schema.enum || null; - if (enumeration) { - // The "enum" property is either a list of strings that are - // valid values or else a list of {name, description} objects, - // where the .name values are the valid values. - enumeration = enumeration.map(e => { - if (typeof(e) == "object") { - return e.name; - } - return e; - }); - } - - let pattern = null; - if (schema.pattern) { - try { - pattern = parsePattern(schema.pattern); - } catch (e) { - throw new Error(`Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`); - } - } - - let format = null; - if (schema.format) { - if (!(schema.format in FORMATS)) { - throw new Error(`Internal error: Invalid string format ${schema.format}`); - } - format = FORMATS[schema.format]; - } - return new this(schema, enumeration, - schema.minLength || 0, - schema.maxLength || Infinity, - pattern, - format); - } - - constructor(schema, enumeration, minLength, maxLength, pattern, format) { - super(schema); - this.enumeration = enumeration; - this.minLength = minLength; - this.maxLength = maxLength; - this.pattern = pattern; - this.format = format; - } - - normalize(value, context) { - let r = this.normalizeBase("string", value, context); - if (r.error) { - return r; - } - value = r.value; - - if (this.enumeration) { - if (this.enumeration.includes(value)) { - return this.postprocess({value}, context); - } - - let choices = this.enumeration.map(JSON.stringify).join(", "); - - return context.error(`Invalid enumeration value ${JSON.stringify(value)}`, - `be one of [${choices}]`); - } - - if (value.length < this.minLength) { - return context.error(`String ${JSON.stringify(value)} is too short (must be ${this.minLength})`, - `be longer than ${this.minLength}`); - } - if (value.length > this.maxLength) { - return context.error(`String ${JSON.stringify(value)} is too long (must be ${this.maxLength})`, - `be shorter than ${this.maxLength}`); - } - - if (this.pattern && !this.pattern.test(value)) { - return context.error(`String ${JSON.stringify(value)} must match ${this.pattern}`, - `match the pattern ${this.pattern.toSource()}`); - } - - if (this.format) { - try { - r.value = this.format(r.value, context); - } catch (e) { - return context.error(String(e), `match the format "${this.format.name}"`); - } - } - - return r; - } - - checkBaseType(baseType) { - return baseType == "string"; - } - - inject(path, name, dest, context) { - if (this.enumeration) { - exportLazyGetter(dest, name, () => { - let obj = Cu.createObjectIn(dest); - for (let e of this.enumeration) { - obj[e.toUpperCase()] = e; - } - return obj; - }); - } - } -} - -let SubModuleType; -class ObjectType extends Type { - static get EXTRA_PROPERTIES() { - return ["properties", "patternProperties", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - if ("functions" in schema) { - return SubModuleType.parseSchema(schema, path, extraProperties); - } - - if (!("$extend" in schema)) { - // Only allow extending "properties" and "patternProperties". - extraProperties = ["additionalProperties", "isInstanceOf", ...extraProperties]; - } - this.checkSchemaProperties(schema, path, extraProperties); - - let parseProperty = (schema, extraProps = []) => { - return { - type: Schemas.parseSchema(schema, path, - ["unsupported", "onError", "permissions", ...extraProps]), - optional: schema.optional || false, - unsupported: schema.unsupported || false, - onError: schema.onError || null, - }; - }; - - // Parse explicit "properties" object. - let properties = Object.create(null); - for (let propName of Object.keys(schema.properties || {})) { - properties[propName] = parseProperty(schema.properties[propName], ["optional"]); - } - - // Parse regexp properties from "patternProperties" object. - let patternProperties = []; - for (let propName of Object.keys(schema.patternProperties || {})) { - let pattern; - try { - pattern = parsePattern(propName); - } catch (e) { - throw new Error(`Internal error: Invalid property pattern ${JSON.stringify(propName)}`); - } - - patternProperties.push({ - pattern, - type: parseProperty(schema.patternProperties[propName]), - }); - } - - // Parse "additionalProperties" schema. - let additionalProperties = null; - if (schema.additionalProperties) { - let type = schema.additionalProperties; - if (type === true) { - type = {"type": "any"}; - } - - additionalProperties = Schemas.parseSchema(type, path); - } - - return new this(schema, properties, additionalProperties, patternProperties, schema.isInstanceOf || null); - } - - constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) { - super(schema); - this.properties = properties; - this.additionalProperties = additionalProperties; - this.patternProperties = patternProperties; - this.isInstanceOf = isInstanceOf; - } - - extend(type) { - for (let key of Object.keys(type.properties)) { - if (key in this.properties) { - throw new Error(`InternalError: Attempt to extend an object with conflicting property "${key}"`); - } - this.properties[key] = type.properties[key]; - } - - this.patternProperties.push(...type.patternProperties); - - return this; - } - - checkBaseType(baseType) { - return baseType == "object"; - } - - /** - * Extracts the enumerable properties of the given object, including - * function properties which would normally be omitted by X-ray - * wrappers. - * - * @param {object} value - * @param {Context} context - * The current parse context. - * @returns {object} - * An object with an `error` or `value` property. - */ - extractProperties(value, context) { - // |value| should be a JS Xray wrapping an object in the - // extension compartment. This works well except when we need to - // access callable properties on |value| since JS Xrays don't - // support those. To work around the problem, we verify that - // |value| is a plain JS object (i.e., not anything scary like a - // Proxy). Then we copy the properties out of it into a normal - // object using a waiver wrapper. - - let klass = Cu.getClassName(value, true); - if (klass != "Object") { - throw context.error(`Expected a plain JavaScript object, got a ${klass}`, - `be a plain JavaScript object`); - } - - let properties = Object.create(null); - - let waived = Cu.waiveXrays(value); - for (let prop of Object.getOwnPropertyNames(waived)) { - let desc = Object.getOwnPropertyDescriptor(waived, prop); - if (desc.get || desc.set) { - throw context.error("Objects cannot have getters or setters on properties", - "contain no getter or setter properties"); - } - // Chrome ignores non-enumerable properties. - if (desc.enumerable) { - properties[prop] = Cu.unwaiveXrays(desc.value); - } - } - - return properties; - } - - checkProperty(context, prop, propType, result, properties, remainingProps) { - let {type, optional, unsupported, onError} = propType; - let error = null; - - if (unsupported) { - if (prop in properties) { - error = context.error(`Property "${prop}" is unsupported by Firefox`, - `not contain an unsupported "${prop}" property`); - } - } else if (prop in properties) { - if (optional && (properties[prop] === null || properties[prop] === undefined)) { - result[prop] = null; - } else { - let r = context.withPath(prop, () => type.normalize(properties[prop], context)); - if (r.error) { - error = r; - } else { - result[prop] = r.value; - properties[prop] = r.value; - } - } - remainingProps.delete(prop); - } else if (!optional) { - error = context.error(`Property "${prop}" is required`, - `contain the required "${prop}" property`); - } else if (optional !== "omit-key-if-missing") { - result[prop] = null; - } - - if (error) { - if (onError == "warn") { - context.logError(error.error); - } else if (onError != "ignore") { - throw error; - } - - result[prop] = null; - } - } - - normalize(value, context) { - try { - let v = this.normalizeBase("object", value, context); - if (v.error) { - return v; - } - value = v.value; - - if (this.isInstanceOf) { - if (Object.keys(this.properties).length || - this.patternProperties.length || - !(this.additionalProperties instanceof AnyType)) { - throw new Error("InternalError: isInstanceOf can only be used with objects that are otherwise unrestricted"); - } - - if (!instanceOf(value, this.isInstanceOf)) { - return context.error(`Object must be an instance of ${this.isInstanceOf}`, - `be an instance of ${this.isInstanceOf}`); - } - - // This is kind of a hack, but we can't normalize things that - // aren't JSON, so we just return them. - return this.postprocess({value}, context); - } - - let properties = this.extractProperties(value, context); - let remainingProps = new Set(Object.keys(properties)); - - let result = {}; - for (let prop of Object.keys(this.properties)) { - this.checkProperty(context, prop, this.properties[prop], result, - properties, remainingProps); - } - - for (let prop of Object.keys(properties)) { - for (let {pattern, type} of this.patternProperties) { - if (pattern.test(prop)) { - this.checkProperty(context, prop, type, result, - properties, remainingProps); - } - } - } - - if (this.additionalProperties) { - for (let prop of remainingProps) { - let type = this.additionalProperties; - let r = context.withPath(prop, () => type.normalize(properties[prop], context)); - if (r.error) { - return r; - } - result[prop] = r.value; - } - } else if (remainingProps.size == 1) { - return context.error(`Unexpected property "${[...remainingProps]}"`, - `not contain an unexpected "${[...remainingProps]}" property`); - } else if (remainingProps.size) { - let props = [...remainingProps].sort().join(", "); - return context.error(`Unexpected properties: ${props}`, - `not contain the unexpected properties [${props}]`); - } - - return this.postprocess({value: result}, context); - } catch (e) { - if (e.error) { - return e; - } - throw e; - } - } -} - -// This type is just a placeholder to be referred to by -// SubModuleProperty. No value is ever expected to have this type. -SubModuleType = class SubModuleType extends Type { - static get EXTRA_PROPERTIES() { - return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - // The path we pass in here is only used for error messages. - path = [...path, schema.id]; - let functions = schema.functions.map(fun => Schemas.parseFunction(path, fun)); - - return new this(functions); - } - - constructor(functions) { - super(); - this.functions = functions; - } -}; - -class NumberType extends Type { - normalize(value, context) { - let r = this.normalizeBase("number", value, context); - if (r.error) { - return r; - } - - if (isNaN(r.value) || !Number.isFinite(r.value)) { - return context.error("NaN and infinity are not valid", - "be a finite number"); - } - - return r; - } - - checkBaseType(baseType) { - return baseType == "number" || baseType == "integer"; - } -} - -class IntegerType extends Type { - static get EXTRA_PROPERTIES() { - return ["minimum", "maximum", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - return new this(schema, schema.minimum || -Infinity, schema.maximum || Infinity); - } - - constructor(schema, minimum, maximum) { - super(schema); - this.minimum = minimum; - this.maximum = maximum; - } - - normalize(value, context) { - let r = this.normalizeBase("integer", value, context); - if (r.error) { - return r; - } - value = r.value; - - // Ensure it's between -2**31 and 2**31-1 - if (!Number.isSafeInteger(value)) { - return context.error("Integer is out of range", - "be a valid 32 bit signed integer"); - } - - if (value < this.minimum) { - return context.error(`Integer ${value} is too small (must be at least ${this.minimum})`, - `be at least ${this.minimum}`); - } - if (value > this.maximum) { - return context.error(`Integer ${value} is too big (must be at most ${this.maximum})`, - `be no greater than ${this.maximum}`); - } - - return this.postprocess(r, context); - } - - checkBaseType(baseType) { - return baseType == "integer"; - } -} - -class BooleanType extends Type { - normalize(value, context) { - return this.normalizeBase("boolean", value, context); - } - - checkBaseType(baseType) { - return baseType == "boolean"; - } -} - -class ArrayType extends Type { - static get EXTRA_PROPERTIES() { - return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let items = Schemas.parseSchema(schema.items, path); - - return new this(schema, items, schema.minItems || 0, schema.maxItems || Infinity); - } - - constructor(schema, itemType, minItems, maxItems) { - super(schema); - this.itemType = itemType; - this.minItems = minItems; - this.maxItems = maxItems; - } - - normalize(value, context) { - let v = this.normalizeBase("array", value, context); - if (v.error) { - return v; - } - value = v.value; - - let result = []; - for (let [i, element] of value.entries()) { - element = context.withPath(String(i), () => this.itemType.normalize(element, context)); - if (element.error) { - return element; - } - result.push(element.value); - } - - if (result.length < this.minItems) { - return context.error(`Array requires at least ${this.minItems} items; you have ${result.length}`, - `have at least ${this.minItems} items`); - } - - if (result.length > this.maxItems) { - return context.error(`Array requires at most ${this.maxItems} items; you have ${result.length}`, - `have at most ${this.maxItems} items`); - } - - return this.postprocess({value: result}, context); - } - - checkBaseType(baseType) { - return baseType == "array"; - } -} - -class FunctionType extends Type { - static get EXTRA_PROPERTIES() { - return ["parameters", "async", "returns", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let isAsync = !!schema.async; - let isExpectingCallback = typeof schema.async === "string"; - let parameters = null; - if ("parameters" in schema) { - parameters = []; - for (let param of schema.parameters) { - // Callbacks default to optional for now, because of promise - // handling. - let isCallback = isAsync && param.name == schema.async; - if (isCallback) { - isExpectingCallback = false; - } - - parameters.push({ - type: Schemas.parseSchema(param, path, ["name", "optional", "default"]), - name: param.name, - optional: param.optional == null ? isCallback : param.optional, - default: param.default == undefined ? null : param.default, - }); - } - } - if (isExpectingCallback) { - throw new Error(`Internal error: Expected a callback parameter with name ${schema.async}`); - } - - let hasAsyncCallback = false; - if (isAsync) { - hasAsyncCallback = (parameters && - parameters.length && - parameters[parameters.length - 1].name == schema.async); - - if (schema.returns) { - throw new Error("Internal error: Async functions must not have return values."); - } - if (schema.allowAmbiguousOptionalArguments && !hasAsyncCallback) { - throw new Error("Internal error: Async functions with ambiguous arguments must declare the callback as the last parameter"); - } - } - - return new this(schema, parameters, isAsync, hasAsyncCallback); - } - - constructor(schema, parameters, isAsync, hasAsyncCallback) { - super(schema); - this.parameters = parameters; - this.isAsync = isAsync; - this.hasAsyncCallback = hasAsyncCallback; - } - - normalize(value, context) { - return this.normalizeBase("function", value, context); - } - - checkBaseType(baseType) { - return baseType == "function"; - } -} - -// Represents a "property" defined in a schema namespace with a -// particular value. Essentially this is a constant. -class ValueProperty extends Entry { - constructor(schema, name, value) { - super(schema); - this.name = name; - this.value = value; - } - - inject(path, name, dest, context) { - dest[name] = this.value; - } -} - -// Represents a "property" defined in a schema namespace that is not a -// constant. -class TypeProperty extends Entry { - constructor(schema, namespaceName, name, type, writable) { - super(schema); - this.namespaceName = namespaceName; - this.name = name; - this.type = type; - this.writable = writable; - } - - throwError(context, msg) { - throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`); - } - - inject(path, name, dest, context) { - if (this.unsupported) { - return; - } - - let apiImpl = context.getImplementation(path.join("."), name); - - let getStub = () => { - this.checkDeprecated(context); - return apiImpl.getProperty(); - }; - - let desc = { - configurable: false, - enumerable: true, - - get: Cu.exportFunction(getStub, dest), - }; - - if (this.writable) { - let setStub = (value) => { - let normalized = this.type.normalize(value, context); - if (normalized.error) { - this.throwError(context, normalized.error); - } - - apiImpl.setProperty(normalized.value); - }; - - desc.set = Cu.exportFunction(setStub, dest); - } - - Object.defineProperty(dest, name, desc); - } -} - -class SubModuleProperty extends Entry { - // A SubModuleProperty represents a tree of objects and properties - // to expose to an extension. Currently we support only a limited - // form of sub-module properties, where "$ref" points to a - // SubModuleType containing a list of functions and "properties" is - // a list of additional simple properties. - // - // name: Name of the property stuff is being added to. - // namespaceName: Namespace in which the property lives. - // reference: Name of the type defining the functions to add to the property. - // properties: Additional properties to add to the module (unsupported). - constructor(schema, name, namespaceName, reference, properties) { - super(schema); - this.name = name; - this.namespaceName = namespaceName; - this.reference = reference; - this.properties = properties; - } - - inject(path, name, dest, context) { - exportLazyGetter(dest, name, () => { - let obj = Cu.createObjectIn(dest); - - let ns = Schemas.namespaces.get(this.namespaceName); - let type = ns.get(this.reference); - if (!type && this.reference.includes(".")) { - let [namespaceName, ref] = this.reference.split("."); - ns = Schemas.namespaces.get(namespaceName); - type = ns.get(ref); - } - if (!type || !(type instanceof SubModuleType)) { - throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`); - } - - let functions = type.functions; - for (let fun of functions) { - let subpath = path.concat(name); - let namespace = subpath.join("."); - let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts; - if (context.shouldInject(namespace, fun.name, allowedContexts)) { - fun.inject(subpath, fun.name, obj, context); - } - } - - // TODO: Inject this.properties. - - return obj; - }); - } -} - -// This class is a base class for FunctionEntrys and Events. It takes -// care of validating parameter lists (i.e., handling of optional -// parameters and parameter type checking). -class CallEntry extends Entry { - constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) { - super(schema); - this.path = path; - this.name = name; - this.parameters = parameters; - this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments; - } - - throwError(context, msg) { - throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`); - } - - checkParameters(args, context) { - let fixedArgs = []; - - // First we create a new array, fixedArgs, that is the same as - // |args| but with default values in place of omitted optional parameters. - let check = (parameterIndex, argIndex) => { - if (parameterIndex == this.parameters.length) { - if (argIndex == args.length) { - return true; - } - return false; - } - - let parameter = this.parameters[parameterIndex]; - if (parameter.optional) { - // Try skipping it. - fixedArgs[parameterIndex] = parameter.default; - if (check(parameterIndex + 1, argIndex)) { - return true; - } - } - - if (argIndex == args.length) { - return false; - } - - let arg = args[argIndex]; - if (!parameter.type.checkBaseType(getValueBaseType(arg))) { - // For Chrome compatibility, use the default value if null or undefined - // is explicitly passed but is not a valid argument in this position. - if (parameter.optional && (arg === null || arg === undefined)) { - fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, global); - } else { - return false; - } - } else { - fixedArgs[parameterIndex] = arg; - } - - return check(parameterIndex + 1, argIndex + 1); - }; - - if (this.allowAmbiguousOptionalArguments) { - // When this option is set, it's up to the implementation to - // parse arguments. - // The last argument for asynchronous methods is either a function or null. - // This is specifically done for runtime.sendMessage. - if (this.hasAsyncCallback && typeof(args[args.length - 1]) != "function") { - args.push(null); - } - return args; - } - let success = check(0, 0); - if (!success) { - this.throwError(context, "Incorrect argument types"); - } - - // Now we normalize (and fully type check) all non-omitted arguments. - fixedArgs = fixedArgs.map((arg, parameterIndex) => { - if (arg === null) { - return null; - } - let parameter = this.parameters[parameterIndex]; - let r = parameter.type.normalize(arg, context); - if (r.error) { - this.throwError(context, `Type error for parameter ${parameter.name} (${r.error})`); - } - return r.value; - }); - - return fixedArgs; - } -} - -// Represents a "function" defined in a schema namespace. -class FunctionEntry extends CallEntry { - constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns, permissions) { - super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments); - this.unsupported = unsupported; - this.returns = returns; - this.permissions = permissions; - - this.isAsync = type.isAsync; - this.hasAsyncCallback = type.hasAsyncCallback; - } - - inject(path, name, dest, context) { - if (this.unsupported) { - return; - } - - if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) { - return; - } - - exportLazyGetter(dest, name, () => { - let apiImpl = context.getImplementation(path.join("."), name); - - let stub; - if (this.isAsync) { - stub = (...args) => { - this.checkDeprecated(context); - let actuals = this.checkParameters(args, context); - let callback = null; - if (this.hasAsyncCallback) { - callback = actuals.pop(); - } - if (callback === null && context.isChromeCompat) { - // We pass an empty stub function as a default callback for - // the `chrome` API, so promise objects are not returned, - // and lastError values are reported immediately. - callback = () => {}; - } - return apiImpl.callAsyncFunction(actuals, callback); - }; - } else if (!this.returns) { - stub = (...args) => { - this.checkDeprecated(context); - let actuals = this.checkParameters(args, context); - return apiImpl.callFunctionNoReturn(actuals); - }; - } else { - stub = (...args) => { - this.checkDeprecated(context); - let actuals = this.checkParameters(args, context); - return apiImpl.callFunction(actuals); - }; - } - return Cu.exportFunction(stub, dest); - }); - } -} - -// Represents an "event" defined in a schema namespace. -class Event extends CallEntry { - constructor(schema, path, name, type, extraParameters, unsupported, permissions) { - super(schema, path, name, extraParameters); - this.type = type; - this.unsupported = unsupported; - this.permissions = permissions; - } - - checkListener(listener, context) { - let r = this.type.normalize(listener, context); - if (r.error) { - this.throwError(context, "Invalid listener"); - } - return r.value; - } - - inject(path, name, dest, context) { - if (this.unsupported) { - return; - } - - if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) { - return; - } - - exportLazyGetter(dest, name, () => { - let apiImpl = context.getImplementation(path.join("."), name); - - let addStub = (listener, ...args) => { - listener = this.checkListener(listener, context); - let actuals = this.checkParameters(args, context); - apiImpl.addListener(listener, actuals); - }; - - let removeStub = (listener) => { - listener = this.checkListener(listener, context); - apiImpl.removeListener(listener); - }; - - let hasStub = (listener) => { - listener = this.checkListener(listener, context); - return apiImpl.hasListener(listener); - }; - - let obj = Cu.createObjectIn(dest); - - Cu.exportFunction(addStub, obj, {defineAs: "addListener"}); - Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"}); - Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"}); - - return obj; - }); - } -} - -const TYPES = Object.freeze(Object.assign(Object.create(null), { - any: AnyType, - array: ArrayType, - boolean: BooleanType, - function: FunctionType, - integer: IntegerType, - number: NumberType, - object: ObjectType, - string: StringType, -})); - -this.Schemas = { - initialized: false, - - // Maps a schema URL to the JSON contained in that schema file. This - // is useful for sending the JSON across processes. - schemaJSON: new Map(), - - // Map[<schema-name> -> Map[<symbol-name> -> Entry]] - // This keeps track of all the schemas that have been loaded so far. - namespaces: new Map(), - - register(namespaceName, symbol, value) { - let ns = this.namespaces.get(namespaceName); - if (!ns) { - ns = new Map(); - ns.name = namespaceName; - ns.permissions = null; - ns.allowedContexts = []; - ns.defaultContexts = []; - this.namespaces.set(namespaceName, ns); - } - ns.set(symbol, value); - }, - - parseSchema(schema, path, extraProperties = []) { - let allowedProperties = new Set(extraProperties); - - if ("choices" in schema) { - return ChoiceType.parseSchema(schema, path, allowedProperties); - } else if ("$ref" in schema) { - return RefType.parseSchema(schema, path, allowedProperties); - } - - if (!("type" in schema)) { - throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`); - } - - allowedProperties.add("type"); - - let type = TYPES[schema.type]; - if (!type) { - throw new Error(`Unexpected type ${schema.type}`); - } - return type.parseSchema(schema, path, allowedProperties); - }, - - parseFunction(path, fun) { - let f = new FunctionEntry(fun, path, fun.name, - this.parseSchema(fun, path, - ["name", "unsupported", "returns", - "permissions", - "allowAmbiguousOptionalArguments"]), - fun.unsupported || false, - fun.allowAmbiguousOptionalArguments || false, - fun.returns || null, - fun.permissions || null); - return f; - }, - - loadType(namespaceName, type) { - if ("$extend" in type) { - this.extendType(namespaceName, type); - } else { - this.register(namespaceName, type.id, this.parseSchema(type, [namespaceName], ["id"])); - } - }, - - extendType(namespaceName, type) { - let ns = Schemas.namespaces.get(namespaceName); - let targetType = ns && ns.get(type.$extend); - - // Only allow extending object and choices types for now. - if (targetType instanceof ObjectType) { - type.type = "object"; - } else if (!targetType) { - throw new Error(`Internal error: Attempt to extend a nonexistant type ${type.$extend}`); - } else if (!(targetType instanceof ChoiceType)) { - throw new Error(`Internal error: Attempt to extend a non-extensible type ${type.$extend}`); - } - - let parsed = this.parseSchema(type, [namespaceName], ["$extend"]); - if (parsed.constructor !== targetType.constructor) { - throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`); - } - - targetType.extend(parsed); - }, - - loadProperty(namespaceName, name, prop) { - if ("$ref" in prop) { - if (!prop.unsupported) { - this.register(namespaceName, name, new SubModuleProperty(prop, name, namespaceName, prop.$ref, - prop.properties || {})); - } - } else if ("value" in prop) { - this.register(namespaceName, name, new ValueProperty(prop, name, prop.value)); - } else { - // We ignore the "optional" attribute on properties since we - // don't inject anything here anyway. - let type = this.parseSchema(prop, [namespaceName], ["optional", "writable"]); - this.register(namespaceName, name, new TypeProperty(prop, namespaceName, name, type, prop.writable || false)); - } - }, - - loadFunction(namespaceName, fun) { - let f = this.parseFunction([namespaceName], fun); - this.register(namespaceName, fun.name, f); - }, - - loadEvent(namespaceName, event) { - let extras = event.extraParameters || []; - extras = extras.map(param => { - return { - type: this.parseSchema(param, [namespaceName], ["name", "optional", "default"]), - name: param.name, - optional: param.optional || false, - default: param.default == undefined ? null : param.default, - }; - }); - - // We ignore these properties for now. - /* eslint-disable no-unused-vars */ - let returns = event.returns; - let filters = event.filters; - /* eslint-enable no-unused-vars */ - - let type = this.parseSchema(event, [namespaceName], - ["name", "unsupported", "permissions", - "extraParameters", "returns", "filters"]); - - let e = new Event(event, [namespaceName], event.name, type, extras, - event.unsupported || false, - event.permissions || null); - this.register(namespaceName, event.name, e); - }, - - init() { - if (this.initialized) { - return; - } - this.initialized = true; - - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { - let data = Services.cpmm.initialProcessData; - let schemas = data["Extension:Schemas"]; - if (schemas) { - this.schemaJSON = schemas; - } - Services.cpmm.addMessageListener("Schema:Add", this); - } - - this.flushSchemas(); - }, - - receiveMessage(msg) { - switch (msg.name) { - case "Schema:Add": - this.schemaJSON.set(msg.data.url, msg.data.schema); - this.flushSchemas(); - break; - - case "Schema:Delete": - this.schemaJSON.delete(msg.data.url); - this.flushSchemas(); - break; - } - }, - - flushSchemas() { - XPCOMUtils.defineLazyGetter(this, "namespaces", - () => this.parseSchemas()); - }, - - parseSchemas() { - Object.defineProperty(this, "namespaces", { - enumerable: true, - configurable: true, - value: new Map(), - }); - - for (let json of this.schemaJSON.values()) { - try { - this.loadSchema(json); - } catch (e) { - Cu.reportError(e); - } - } - - return this.namespaces; - }, - - loadSchema(json) { - for (let namespace of json) { - let name = namespace.namespace; - - let types = namespace.types || []; - for (let type of types) { - this.loadType(name, type); - } - - let properties = namespace.properties || {}; - for (let propertyName of Object.keys(properties)) { - this.loadProperty(name, propertyName, properties[propertyName]); - } - - let functions = namespace.functions || []; - for (let fun of functions) { - this.loadFunction(name, fun); - } - - let events = namespace.events || []; - for (let event of events) { - this.loadEvent(name, event); - } - - let ns = this.namespaces.get(name); - ns.permissions = namespace.permissions || null; - ns.allowedContexts = namespace.allowedContexts || []; - ns.defaultContexts = namespace.defaultContexts || []; - } - }, - - load(url) { - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) { - return readJSON(url).then(json => { - this.schemaJSON.set(url, json); - - let data = Services.ppmm.initialProcessData; - data["Extension:Schemas"] = this.schemaJSON; - - Services.ppmm.broadcastAsyncMessage("Schema:Add", {url, schema: json}); - - this.flushSchemas(); - }); - } - }, - - unload(url) { - this.schemaJSON.delete(url); - - let data = Services.ppmm.initialProcessData; - data["Extension:Schemas"] = this.schemaJSON; - - Services.ppmm.broadcastAsyncMessage("Schema:Delete", {url}); - - this.flushSchemas(); - }, - - /** - * Checks whether a given object has the necessary permissions to - * expose the given namespace. - * - * @param {string} namespace - * The top-level namespace to check permissions for. - * @param {object} wrapperFuncs - * Wrapper functions for the given context. - * @param {function} wrapperFuncs.hasPermission - * A function which, when given a string argument, returns true - * if the context has the given permission. - * @returns {boolean} - * True if the context has permission for the given namespace. - */ - checkPermissions(namespace, wrapperFuncs) { - let ns = this.namespaces.get(namespace); - if (ns && ns.permissions) { - return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm)); - } - return true; - }, - - exportLazyGetter, - - /** - * Inject registered extension APIs into `dest`. - * - * @param {object} dest The root namespace for the APIs. - * This object is usually exposed to extensions as "chrome" or "browser". - * @param {object} wrapperFuncs An implementation of the InjectionContext - * interface, which runs the actual functionality of the generated API. - */ - inject(dest, wrapperFuncs) { - let context = new InjectionContext(wrapperFuncs); - - let createNamespace = ns => { - let obj = Cu.createObjectIn(dest); - - for (let [name, entry] of ns) { - let allowedContexts = entry.allowedContexts; - if (!allowedContexts.length) { - allowedContexts = ns.defaultContexts; - } - - if (context.shouldInject(ns.name, name, allowedContexts)) { - entry.inject([ns.name], name, obj, context); - } - } - - // Remove the namespace object if it is empty - if (Object.keys(obj).length) { - return obj; - } - }; - - let createNestedNamespaces = (parent, namespaces) => { - for (let [prop, namespace] of namespaces) { - if (namespace instanceof DeepMap) { - exportLazyGetter(parent, prop, () => { - let obj = Cu.createObjectIn(parent); - createNestedNamespaces(obj, namespace); - return obj; - }); - } else { - exportLazyGetter(parent, prop, - () => createNamespace(namespace)); - } - } - }; - - let nestedNamespaces = new DeepMap(); - for (let ns of this.namespaces.values()) { - if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) { - continue; - } - - if (!wrapperFuncs.shouldInject(ns.name, null, ns.allowedContexts)) { - continue; - } - - if (ns.name.includes(".")) { - let path = ns.name.split("."); - let leafName = path.pop(); - - let parent = nestedNamespaces.getPath(...path); - - parent.set(leafName, ns); - } else { - exportLazyGetter(dest, ns.name, - () => createNamespace(ns)); - } - } - - createNestedNamespaces(dest, nestedNamespaces); - }, - - /** - * Normalize `obj` according to the loaded schema for `typeName`. - * - * @param {object} obj The object to normalize against the schema. - * @param {string} typeName The name in the format namespace.propertyname - * @param {object} context An implementation of Context. Any validation errors - * are reported to the given context. - * @returns {object} The normalized object. - */ - normalize(obj, typeName, context) { - let [namespaceName, prop] = typeName.split("."); - let ns = this.namespaces.get(namespaceName); - let type = ns.get(prop); - - return type.normalize(obj, new Context(context)); - }, -}; diff --git a/toolkit/components/webextensions/ext-alarms.js b/toolkit/components/webextensions/ext-alarms.js deleted file mode 100644 index 2171e7dba..000000000 --- a/toolkit/components/webextensions/ext-alarms.js +++ /dev/null @@ -1,155 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -var { - EventManager, -} = ExtensionUtils; - -// WeakMap[Extension -> Map[name -> Alarm]] -var alarmsMap = new WeakMap(); - -// WeakMap[Extension -> Set[callback]] -var alarmCallbacksMap = new WeakMap(); - -// Manages an alarm created by the extension (alarms API). -function Alarm(extension, name, alarmInfo) { - this.extension = extension; - this.name = name; - this.when = alarmInfo.when; - this.delayInMinutes = alarmInfo.delayInMinutes; - this.periodInMinutes = alarmInfo.periodInMinutes; - this.canceled = false; - - let delay, scheduledTime; - if (this.when) { - scheduledTime = this.when; - delay = this.when - Date.now(); - } else { - if (!this.delayInMinutes) { - this.delayInMinutes = this.periodInMinutes; - } - delay = this.delayInMinutes * 60 * 1000; - scheduledTime = Date.now() + delay; - } - - this.scheduledTime = scheduledTime; - - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); - this.timer = timer; -} - -Alarm.prototype = { - clear() { - this.timer.cancel(); - alarmsMap.get(this.extension).delete(this.name); - this.canceled = true; - }, - - observe(subject, topic, data) { - if (this.canceled) { - return; - } - - for (let callback of alarmCallbacksMap.get(this.extension)) { - callback(this); - } - - if (!this.periodInMinutes) { - this.clear(); - return; - } - - let delay = this.periodInMinutes * 60 * 1000; - this.scheduledTime = Date.now() + delay; - this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); - }, - - get data() { - return { - name: this.name, - scheduledTime: this.scheduledTime, - periodInMinutes: this.periodInMinutes, - }; - }, -}; - -/* eslint-disable mozilla/balanced-listeners */ -extensions.on("startup", (type, extension) => { - alarmsMap.set(extension, new Map()); - alarmCallbacksMap.set(extension, new Set()); -}); - -extensions.on("shutdown", (type, extension) => { - if (alarmsMap.has(extension)) { - for (let alarm of alarmsMap.get(extension).values()) { - alarm.clear(); - } - alarmsMap.delete(extension); - alarmCallbacksMap.delete(extension); - } -}); -/* eslint-enable mozilla/balanced-listeners */ - -extensions.registerSchemaAPI("alarms", "addon_parent", context => { - let {extension} = context; - return { - alarms: { - create: function(name, alarmInfo) { - name = name || ""; - let alarms = alarmsMap.get(extension); - if (alarms.has(name)) { - alarms.get(name).clear(); - } - let alarm = new Alarm(extension, name, alarmInfo); - alarms.set(alarm.name, alarm); - }, - - get: function(name) { - name = name || ""; - let alarms = alarmsMap.get(extension); - if (alarms.has(name)) { - return Promise.resolve(alarms.get(name).data); - } - return Promise.resolve(); - }, - - getAll: function() { - let result = Array.from(alarmsMap.get(extension).values(), alarm => alarm.data); - return Promise.resolve(result); - }, - - clear: function(name) { - name = name || ""; - let alarms = alarmsMap.get(extension); - if (alarms.has(name)) { - alarms.get(name).clear(); - return Promise.resolve(true); - } - return Promise.resolve(false); - }, - - clearAll: function() { - let cleared = false; - for (let alarm of alarmsMap.get(extension).values()) { - alarm.clear(); - cleared = true; - } - return Promise.resolve(cleared); - }, - - onAlarm: new EventManager(context, "alarms.onAlarm", fire => { - let callback = alarm => { - fire(alarm.data); - }; - - alarmCallbacksMap.get(extension).add(callback); - return () => { - alarmCallbacksMap.get(extension).delete(callback); - }; - }).api(), - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-backgroundPage.js b/toolkit/components/webextensions/ext-backgroundPage.js deleted file mode 100644 index fce6100ca..000000000 --- a/toolkit/components/webextensions/ext-backgroundPage.js +++ /dev/null @@ -1,147 +0,0 @@ -"use strict"; - -var {interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -const { - promiseDocumentLoaded, - promiseObserved, -} = ExtensionUtils; - -const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI( - `<?xml version="1.0"?> - <window id="documentElement"/>`); - -// WeakMap[Extension -> BackgroundPage] -var backgroundPagesMap = new WeakMap(); - -// Responsible for the background_page section of the manifest. -function BackgroundPage(options, extension) { - this.extension = extension; - this.page = options.page || null; - this.isGenerated = !!options.scripts; - this.windowlessBrowser = null; - this.webNav = null; -} - -BackgroundPage.prototype = { - build: Task.async(function* () { - let windowlessBrowser = Services.appShell.createWindowlessBrowser(true); - this.windowlessBrowser = windowlessBrowser; - - let url; - if (this.page) { - url = this.extension.baseURI.resolve(this.page); - } else if (this.isGenerated) { - url = this.extension.baseURI.resolve("_generated_background_page.html"); - } - - if (!this.extension.isExtensionURL(url)) { - this.extension.manifestError("Background page must be a file within the extension"); - url = this.extension.baseURI.resolve("_blank.html"); - } - - let system = Services.scriptSecurityManager.getSystemPrincipal(); - - // The windowless browser is a thin wrapper around a docShell that keeps - // its related resources alive. It implements nsIWebNavigation and - // forwards its methods to the underlying docShell, but cannot act as a - // docShell itself. Calling `getInterface(nsIDocShell)` gives us the - // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us - // access to the webNav methods that are already available on the - // windowless browser, but contrary to appearances, they are not the same - // object. - let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIWebNavigation); - - if (PrivateBrowsingUtils.permanentPrivateBrowsing) { - let attrs = chromeShell.getOriginAttributes(); - attrs.privateBrowsingId = 1; - chromeShell.setOriginAttributes(attrs); - } - - chromeShell.useGlobalHistory = false; - chromeShell.createAboutBlankContentViewer(system); - chromeShell.loadURI(XUL_URL, 0, null, null, null); - - - yield promiseObserved("chrome-document-global-created", - win => win.document == chromeShell.document); - - let chromeDoc = yield promiseDocumentLoaded(chromeShell.document); - - let browser = chromeDoc.createElement("browser"); - browser.setAttribute("type", "content"); - browser.setAttribute("disableglobalhistory", "true"); - chromeDoc.documentElement.appendChild(browser); - - extensions.emit("extension-browser-inserted", browser); - browser.messageManager.sendAsyncMessage("Extension:InitExtensionView", { - viewType: "background", - url, - }); - - yield new Promise(resolve => { - browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() { - browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad); - resolve(); - }); - }); - - // TODO(robwu): This is not webext-oop compatible. - this.webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation); - let window = this.webNav.document.defaultView; - - - // Set the add-on's main debugger global, for use in the debugger - // console. - if (this.extension.addonData.instanceID) { - AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID) - .then(addon => addon.setDebugGlobal(window)); - } - - this.extension.emit("startup"); - }), - - shutdown() { - if (this.extension.addonData.instanceID) { - AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID) - .then(addon => addon.setDebugGlobal(null)); - } - - // Navigate away from the background page to invalidate any - // setTimeouts or other callbacks. - if (this.webNav) { - this.webNav.loadURI("about:blank", 0, null, null, null); - this.webNav = null; - } - - this.windowlessBrowser.loadURI("about:blank", 0, null, null, null); - this.windowlessBrowser.close(); - this.windowlessBrowser = null; - }, -}; - -/* eslint-disable mozilla/balanced-listeners */ -extensions.on("manifest_background", (type, directive, extension, manifest) => { - let bgPage = new BackgroundPage(manifest.background, extension); - backgroundPagesMap.set(extension, bgPage); - return bgPage.build(); -}); - -extensions.on("shutdown", (type, extension) => { - if (backgroundPagesMap.has(extension)) { - backgroundPagesMap.get(extension).shutdown(); - backgroundPagesMap.delete(extension); - } -}); -/* eslint-enable mozilla/balanced-listeners */ diff --git a/toolkit/components/webextensions/ext-browser-content.js b/toolkit/components/webextensions/ext-browser-content.js deleted file mode 100644 index e14ca50d6..000000000 --- a/toolkit/components/webextensions/ext-browser-content.js +++ /dev/null @@ -1,217 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "require", - "resource://devtools/shared/Loader.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); - -XPCOMUtils.defineLazyGetter(this, "colorUtils", () => { - return require("devtools/shared/css/color").colorUtils; -}); - -const { - stylesheetMap, -} = ExtensionUtils; - -/* globals addMessageListener, content, docShell, sendAsyncMessage */ - -// Minimum time between two resizes. -const RESIZE_TIMEOUT = 100; - -const BrowserListener = { - init({allowScriptsToClose, fixedWidth, maxHeight, maxWidth, stylesheets}) { - this.fixedWidth = fixedWidth; - this.stylesheets = stylesheets || []; - - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - - this.oldBackground = null; - - if (allowScriptsToClose) { - content.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .allowScriptsToClose(); - } - - addEventListener("DOMWindowCreated", this, true); - addEventListener("load", this, true); - addEventListener("DOMContentLoaded", this, true); - addEventListener("DOMWindowClose", this, true); - addEventListener("MozScrolledAreaChanged", this, true); - }, - - destroy() { - removeEventListener("DOMWindowCreated", this, true); - removeEventListener("load", this, true); - removeEventListener("DOMContentLoaded", this, true); - removeEventListener("DOMWindowClose", this, true); - removeEventListener("MozScrolledAreaChanged", this, true); - }, - - receiveMessage({name, data}) { - if (name === "Extension:InitBrowser") { - this.init(data); - } - }, - - handleEvent(event) { - switch (event.type) { - case "DOMWindowCreated": - if (event.target === content.document) { - let winUtils = content.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - - for (let url of this.stylesheets) { - winUtils.addSheet(stylesheetMap.get(url), winUtils.AGENT_SHEET); - } - } - break; - - case "DOMWindowClose": - if (event.target === content) { - event.preventDefault(); - - sendAsyncMessage("Extension:DOMWindowClose"); - } - break; - - case "DOMContentLoaded": - if (event.target === content.document) { - sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href}); - this.handleDOMChange(true); - } - break; - - case "load": - if (event.target.contentWindow === content) { - // For about:addons inline <browsers>, we currently receive a load - // event on the <browser> element, but no load or DOMContentLoaded - // events from the content window. - sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href}); - } else if (event.target !== content.document) { - break; - } - - // We use a capturing listener, so we get this event earlier than any - // load listeners in the content page. Resizing after a timeout ensures - // that we calculate the size after the entire event cycle has completed - // (unless someone spins the event loop, anyway), and hopefully after - // the content has made any modifications. - Promise.resolve().then(() => { - this.handleDOMChange(true); - }); - - // Mutation observer to make sure the panel shrinks when the content does. - new content.MutationObserver(this.handleDOMChange.bind(this)).observe( - content.document.documentElement, { - attributes: true, - characterData: true, - childList: true, - subtree: true, - }); - break; - - case "MozScrolledAreaChanged": - this.handleDOMChange(); - break; - } - }, - - // Resizes the browser to match the preferred size of the content (debounced). - handleDOMChange(ignoreThrottling = false) { - if (ignoreThrottling && this.resizeTimeout) { - clearTimeout(this.resizeTimeout); - this.resizeTimeout = null; - } - - if (this.resizeTimeout == null) { - this.resizeTimeout = setTimeout(() => { - try { - if (content) { - this._handleDOMChange("delayed"); - } - } finally { - this.resizeTimeout = null; - } - }, RESIZE_TIMEOUT); - - this._handleDOMChange(); - } - }, - - _handleDOMChange(detail) { - let doc = content.document; - - let body = doc.body; - if (!body || doc.compatMode === "BackCompat") { - // In quirks mode, the root element is used as the scroll frame, and the - // body lies about its scroll geometry, and returns the values for the - // root instead. - body = doc.documentElement; - } - - - let result; - if (this.fixedWidth) { - // If we're in a fixed-width area (namely a slide-in subview of the main - // menu panel), we need to calculate the view height based on the - // preferred height of the content document's root scrollable element at the - // current width, rather than the complete preferred dimensions of the - // content window. - - // Compensate for any offsets (margin, padding, ...) between the scroll - // area of the body and the outer height of the document. - let getHeight = elem => elem.getBoundingClientRect(elem).height; - let bodyPadding = getHeight(doc.documentElement) - getHeight(body); - - let height = Math.ceil(body.scrollHeight + bodyPadding); - - result = {height, detail}; - } else { - let background = doc.defaultView.getComputedStyle(body).backgroundColor; - let bgColor = colorUtils.colorToRGBA(background); - if (bgColor.a !== 1) { - // Ignore non-opaque backgrounds. - background = null; - } - - if (background !== this.oldBackground) { - sendAsyncMessage("Extension:BrowserBackgroundChanged", {background}); - } - this.oldBackground = background; - - - // Adjust the size of the browser based on its content's preferred size. - let {contentViewer} = docShell; - let ratio = content.devicePixelRatio; - - let w = {}, h = {}; - contentViewer.getContentSizeConstrained(this.maxWidth * ratio, - this.maxHeight * ratio, - w, h); - - let width = Math.ceil(w.value / ratio); - let height = Math.ceil(h.value / ratio); - - result = {width, height, detail}; - } - - sendAsyncMessage("Extension:BrowserResized", result); - }, -}; - -addMessageListener("Extension:InitBrowser", BrowserListener); diff --git a/toolkit/components/webextensions/ext-c-backgroundPage.js b/toolkit/components/webextensions/ext-c-backgroundPage.js deleted file mode 100644 index ca446ce79..000000000 --- a/toolkit/components/webextensions/ext-c-backgroundPage.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; - -global.initializeBackgroundPage = (contentWindow) => { - // Override the `alert()` method inside background windows; - // we alias it to console.log(). - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1203394 - let alertDisplayedWarning = false; - let alertOverwrite = text => { - if (!alertDisplayedWarning) { - require("devtools/client/framework/devtools-browser"); - - let {HUDService} = require("devtools/client/webconsole/hudservice"); - HUDService.openBrowserConsoleOrFocus(); - - contentWindow.console.warn("alert() is not supported in background windows; please use console.log instead."); - - alertDisplayedWarning = true; - } - - contentWindow.console.log(text); - }; - Cu.exportFunction(alertOverwrite, contentWindow, {defineAs: "alert"}); -}; - -extensions.registerSchemaAPI("extension", "addon_child", context => { - function getBackgroundPage() { - for (let view of context.extension.views) { - if (view.viewType == "background" && context.principal.subsumes(view.principal)) { - return view.contentWindow; - } - } - return null; - } - return { - extension: { - getBackgroundPage, - }, - - runtime: { - getBackgroundPage() { - return context.cloneScope.Promise.resolve(getBackgroundPage()); - }, - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-c-extension.js b/toolkit/components/webextensions/ext-c-extension.js deleted file mode 100644 index 669309bea..000000000 --- a/toolkit/components/webextensions/ext-c-extension.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; - -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); - -function extensionApiFactory(context) { - return { - extension: { - getURL(url) { - return context.extension.baseURI.resolve(url); - }, - - get lastError() { - return context.lastError; - }, - - get inIncognitoContext() { - return context.incognito; - }, - }, - }; -} - -extensions.registerSchemaAPI("extension", "addon_child", extensionApiFactory); -extensions.registerSchemaAPI("extension", "content_child", extensionApiFactory); -extensions.registerSchemaAPI("extension", "addon_child", context => { - return { - extension: { - getViews: function(fetchProperties) { - let result = Cu.cloneInto([], context.cloneScope); - - for (let view of context.extension.views) { - if (!view.active) { - continue; - } - if (!context.principal.subsumes(view.principal)) { - continue; - } - - if (fetchProperties !== null) { - if (fetchProperties.type !== null && view.viewType != fetchProperties.type) { - continue; - } - - if (fetchProperties.windowId !== null && view.windowId != fetchProperties.windowId) { - continue; - } - } - - result.push(view.contentWindow); - } - - return result; - }, - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-c-runtime.js b/toolkit/components/webextensions/ext-c-runtime.js deleted file mode 100644 index 1dcac35da..000000000 --- a/toolkit/components/webextensions/ext-c-runtime.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict"; - -function runtimeApiFactory(context) { - let {extension} = context; - - return { - runtime: { - onConnect: context.messenger.onConnect("runtime.onConnect"), - - onMessage: context.messenger.onMessage("runtime.onMessage"), - - onConnectExternal: context.messenger.onConnectExternal("runtime.onConnectExternal"), - - onMessageExternal: context.messenger.onMessageExternal("runtime.onMessageExternal"), - - connect: function(extensionId, connectInfo) { - let name = connectInfo !== null && connectInfo.name || ""; - extensionId = extensionId || extension.id; - let recipient = {extensionId}; - - return context.messenger.connect(context.messageManager, name, recipient); - }, - - sendMessage: function(...args) { - let options; // eslint-disable-line no-unused-vars - let extensionId, message, responseCallback; - if (typeof args[args.length - 1] == "function") { - responseCallback = args.pop(); - } - if (!args.length) { - return Promise.reject({message: "runtime.sendMessage's message argument is missing"}); - } else if (args.length == 1) { - message = args[0]; - } else if (args.length == 2) { - if (typeof args[0] == "string" && args[0]) { - [extensionId, message] = args; - } else { - [message, options] = args; - } - } else if (args.length == 3) { - [extensionId, message, options] = args; - } else if (args.length == 4 && !responseCallback) { - return Promise.reject({message: "runtime.sendMessage's last argument is not a function"}); - } else { - return Promise.reject({message: "runtime.sendMessage received too many arguments"}); - } - - if (extensionId != null && typeof extensionId != "string") { - return Promise.reject({message: "runtime.sendMessage's extensionId argument is invalid"}); - } - if (options != null && typeof options != "object") { - return Promise.reject({message: "runtime.sendMessage's options argument is invalid"}); - } - - extensionId = extensionId || extension.id; - let recipient = {extensionId}; - - return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback); - }, - - connectNative(application) { - let recipient = { - childId: context.childManager.id, - toNativeApp: application, - }; - - return context.messenger.connectNative(context.messageManager, "", recipient); - }, - - sendNativeMessage(application, message) { - let recipient = { - childId: context.childManager.id, - toNativeApp: application, - }; - return context.messenger.sendNativeMessage(context.messageManager, message, recipient); - }, - - get lastError() { - return context.lastError; - }, - - getManifest() { - return Cu.cloneInto(extension.manifest, context.cloneScope); - }, - - id: extension.id, - - getURL: function(url) { - return extension.baseURI.resolve(url); - }, - }, - }; -} - -extensions.registerSchemaAPI("runtime", "addon_child", runtimeApiFactory); -extensions.registerSchemaAPI("runtime", "content_child", runtimeApiFactory); diff --git a/toolkit/components/webextensions/ext-c-storage.js b/toolkit/components/webextensions/ext-c-storage.js deleted file mode 100644 index e8d53058f..000000000 --- a/toolkit/components/webextensions/ext-c-storage.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; - -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage", - "resource://gre/modules/ExtensionStorage.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -function storageApiFactory(context) { - function sanitize(items) { - // The schema validator already takes care of arrays (which are only allowed - // to contain strings). Strings and null are safe values. - if (typeof items != "object" || items === null || Array.isArray(items)) { - return items; - } - // If we got here, then `items` is an object generated by `ObjectType`'s - // `normalize` method from Schemas.jsm. The object returned by `normalize` - // lives in this compartment, while the values live in compartment of - // `context.contentWindow`. The `sanitize` method runs with the principal - // of `context`, so we cannot just use `ExtensionStorage.sanitize` because - // it is not allowed to access properties of `items`. - // So we enumerate all properties and sanitize each value individually. - let sanitized = {}; - for (let [key, value] of Object.entries(items)) { - sanitized[key] = ExtensionStorage.sanitize(value, context); - } - return sanitized; - } - return { - storage: { - local: { - get: function(keys) { - keys = sanitize(keys); - return context.childManager.callParentAsyncFunction("storage.local.get", [ - keys, - ]); - }, - set: function(items) { - items = sanitize(items); - return context.childManager.callParentAsyncFunction("storage.local.set", [ - items, - ]); - }, - }, - - sync: { - get: function(keys) { - keys = sanitize(keys); - return context.childManager.callParentAsyncFunction("storage.sync.get", [ - keys, - ]); - }, - set: function(items) { - items = sanitize(items); - return context.childManager.callParentAsyncFunction("storage.sync.set", [ - items, - ]); - }, - }, - }, - }; -} -extensions.registerSchemaAPI("storage", "addon_child", storageApiFactory); -extensions.registerSchemaAPI("storage", "content_child", storageApiFactory); diff --git a/toolkit/components/webextensions/ext-c-test.js b/toolkit/components/webextensions/ext-c-test.js deleted file mode 100644 index b0c92f79f..000000000 --- a/toolkit/components/webextensions/ext-c-test.js +++ /dev/null @@ -1,188 +0,0 @@ -"use strict"; - -Components.utils.import("resource://gre/modules/ExtensionUtils.jsm"); -var { - SingletonEventManager, -} = ExtensionUtils; - -/** - * Checks whether the given error matches the given expectations. - * - * @param {*} error - * The error to check. - * @param {string|RegExp|function|null} expectedError - * The expectation to check against. If this parameter is: - * - * - a string, the error message must exactly equal the string. - * - a regular expression, it must match the error message. - * - a function, it is called with the error object and its - * return value is returned. - * - null, the function always returns true. - * @param {BaseContext} context - * - * @returns {boolean} - * True if the error matches the expected error. - */ -function errorMatches(error, expectedError, context) { - if (expectedError === null) { - return true; - } - - if (typeof expectedError === "function") { - return context.runSafeWithoutClone(expectedError, error); - } - - if (typeof error !== "object" || error == null || - typeof error.message !== "string") { - return false; - } - - if (typeof expectedError === "string") { - return error.message === expectedError; - } - - try { - return expectedError.test(error.message); - } catch (e) { - Cu.reportError(e); - } - - return false; -} - -/** - * Calls .toSource() on the given value, but handles null, undefined, - * and errors. - * - * @param {*} value - * @returns {string} - */ -function toSource(value) { - if (value === null) { - return "null"; - } - if (value === undefined) { - return "undefined"; - } - if (typeof value === "string") { - return JSON.stringify(value); - } - - try { - return String(value.toSource()); - } catch (e) { - return "<unknown>"; - } -} - -function makeTestAPI(context) { - const {extension} = context; - - function getStack() { - return new context.cloneScope.Error().stack.replace(/^/gm, " "); - } - - function assertTrue(value, msg) { - extension.emit("test-result", Boolean(value), String(msg), getStack()); - } - - return { - test: { - sendMessage(...args) { - extension.emit("test-message", ...args); - }, - - notifyPass(msg) { - extension.emit("test-done", true, msg, getStack()); - }, - - notifyFail(msg) { - extension.emit("test-done", false, msg, getStack()); - }, - - log(msg) { - extension.emit("test-log", true, msg, getStack()); - }, - - fail(msg) { - assertTrue(false, msg); - }, - - succeed(msg) { - assertTrue(true, msg); - }, - - assertTrue(value, msg) { - assertTrue(value, msg); - }, - - assertFalse(value, msg) { - assertTrue(!value, msg); - }, - - assertEq(expected, actual, msg) { - let equal = expected === actual; - - expected = String(expected); - actual = String(actual); - - if (!equal && expected === actual) { - actual += " (different)"; - } - extension.emit("test-eq", equal, String(msg), expected, actual, getStack()); - }, - - assertRejects(promise, expectedError, msg) { - // Wrap in a native promise for consistency. - promise = Promise.resolve(promise); - - if (msg) { - msg = `: ${msg}`; - } - - return promise.then(result => { - assertTrue(false, `Promise resolved, expected rejection${msg}`); - }, error => { - let errorMessage = toSource(error && error.message); - - assertTrue(errorMatches(error, expectedError, context), - `Promise rejected, expecting rejection to match ${toSource(expectedError)}, ` + - `got ${errorMessage}${msg}`); - }); - }, - - assertThrows(func, expectedError, msg) { - if (msg) { - msg = `: ${msg}`; - } - - try { - func(); - - assertTrue(false, `Function did not throw, expected error${msg}`); - } catch (error) { - let errorMessage = toSource(error && error.message); - - assertTrue(errorMatches(error, expectedError, context), - `Function threw, expecting error to match ${toSource(expectedError)}` + - `got ${errorMessage}${msg}`); - } - }, - - onMessage: new SingletonEventManager(context, "test.onMessage", fire => { - let handler = (event, ...args) => { - context.runSafe(fire, ...args); - }; - - extension.on("test-harness-message", handler); - return () => { - extension.off("test-harness-message", handler); - }; - }).api(), - }, - }; -} - -extensions.registerSchemaAPI("test", "addon_child", makeTestAPI); -extensions.registerSchemaAPI("test", "content_child", makeTestAPI); - diff --git a/toolkit/components/webextensions/ext-cookies.js b/toolkit/components/webextensions/ext-cookies.js deleted file mode 100644 index d0a703421..000000000 --- a/toolkit/components/webextensions/ext-cookies.js +++ /dev/null @@ -1,484 +0,0 @@ -"use strict"; - -const {interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", - "resource://gre/modules/ContextualIdentityService.jsm"); - -var { - EventManager, -} = ExtensionUtils; - -var DEFAULT_STORE = "firefox-default"; -var PRIVATE_STORE = "firefox-private"; -var CONTAINER_STORE = "firefox-container-"; - -global.getCookieStoreIdForTab = function(data, tab) { - if (data.incognito) { - return PRIVATE_STORE; - } - - if (tab.userContextId) { - return CONTAINER_STORE + tab.userContextId; - } - - return DEFAULT_STORE; -}; - -global.isPrivateCookieStoreId = function(storeId) { - return storeId == PRIVATE_STORE; -}; - -global.isDefaultCookieStoreId = function(storeId) { - return storeId == DEFAULT_STORE; -}; - -global.isContainerCookieStoreId = function(storeId) { - return storeId !== null && storeId.startsWith(CONTAINER_STORE); -}; - -global.getContainerForCookieStoreId = function(storeId) { - if (!global.isContainerCookieStoreId(storeId)) { - return null; - } - - let containerId = storeId.substring(CONTAINER_STORE.length); - if (ContextualIdentityService.getIdentityFromId(containerId)) { - return parseInt(containerId, 10); - } - - return null; -}; - -global.isValidCookieStoreId = function(storeId) { - return global.isDefaultCookieStoreId(storeId) || - global.isPrivateCookieStoreId(storeId) || - global.isContainerCookieStoreId(storeId); -}; - -function convert({cookie, isPrivate}) { - let result = { - name: cookie.name, - value: cookie.value, - domain: cookie.host, - hostOnly: !cookie.isDomain, - path: cookie.path, - secure: cookie.isSecure, - httpOnly: cookie.isHttpOnly, - session: cookie.isSession, - }; - - if (!cookie.isSession) { - result.expirationDate = cookie.expiry; - } - - if (cookie.originAttributes.userContextId) { - result.storeId = CONTAINER_STORE + cookie.originAttributes.userContextId; - } else if (cookie.originAttributes.privateBrowsingId || isPrivate) { - result.storeId = PRIVATE_STORE; - } else { - result.storeId = DEFAULT_STORE; - } - - return result; -} - -function isSubdomain(otherDomain, baseDomain) { - return otherDomain == baseDomain || otherDomain.endsWith("." + baseDomain); -} - -// Checks that the given extension has permission to set the given cookie for -// the given URI. -function checkSetCookiePermissions(extension, uri, cookie) { - // Permission checks: - // - // - If the extension does not have permissions for the specified - // URL, it cannot set cookies for it. - // - // - If the specified URL could not set the given cookie, neither can - // the extension. - // - // Ideally, we would just have the cookie service make the latter - // determination, but that turns out to be quite complicated. At the - // moment, it requires constructing a cookie string and creating a - // dummy channel, both of which can be problematic. It also triggers - // a whole set of additional permission and preference checks, which - // may or may not be desirable. - // - // So instead, we do a similar set of checks here. Exactly what - // cookies a given URL should be able to set is not well-documented, - // and is not standardized in any standard that anyone actually - // follows. So instead, we follow the rules used by the cookie - // service. - // - // See source/netwerk/cookie/nsCookieService.cpp, in particular - // CheckDomain() and SetCookieInternal(). - - if (uri.scheme != "http" && uri.scheme != "https") { - return false; - } - - if (!extension.whiteListedHosts.matchesIgnoringPath(uri)) { - return false; - } - - if (!cookie.host) { - // If no explicit host is specified, this becomes a host-only cookie. - cookie.host = uri.host; - return true; - } - - // A leading "." is not expected, but is tolerated if it's not the only - // character in the host. If there is one, start by stripping it off. We'll - // add a new one on success. - if (cookie.host.length > 1) { - cookie.host = cookie.host.replace(/^\./, ""); - } - cookie.host = cookie.host.toLowerCase(); - - if (cookie.host != uri.host) { - // Not an exact match, so check for a valid subdomain. - let baseDomain; - try { - baseDomain = Services.eTLD.getBaseDomain(uri); - } catch (e) { - if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS || - e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { - // The cookie service uses these to determine whether the domain - // requires an exact match. We already know we don't have an exact - // match, so return false. In all other cases, re-raise the error. - return false; - } - throw e; - } - - // The cookie domain must be a subdomain of the base domain. This prevents - // us from setting cookies for domains like ".co.uk". - // The domain of the requesting URL must likewise be a subdomain of the - // cookie domain. This prevents us from setting cookies for entirely - // unrelated domains. - if (!isSubdomain(cookie.host, baseDomain) || - !isSubdomain(uri.host, cookie.host)) { - return false; - } - - // RFC2109 suggests that we may only add cookies for sub-domains 1-level - // below us, but enforcing that would break the web, so we don't. - } - - // An explicit domain was passed, so add a leading "." to make this a - // domain cookie. - cookie.host = "." + cookie.host; - - // We don't do any significant checking of path permissions. RFC2109 - // suggests we only allow sites to add cookies for sub-paths, similar to - // same origin policy enforcement, but no-one implements this. - - return true; -} - -function* query(detailsIn, props, context) { - // Different callers want to filter on different properties. |props| - // tells us which ones they're interested in. - let details = {}; - props.forEach(property => { - if (detailsIn[property] !== null) { - details[property] = detailsIn[property]; - } - }); - - if ("domain" in details) { - details.domain = details.domain.toLowerCase().replace(/^\./, ""); - } - - let userContextId = 0; - let isPrivate = context.incognito; - if (details.storeId) { - if (!global.isValidCookieStoreId(details.storeId)) { - return; - } - - if (global.isDefaultCookieStoreId(details.storeId)) { - isPrivate = false; - } else if (global.isPrivateCookieStoreId(details.storeId)) { - isPrivate = true; - } else if (global.isContainerCookieStoreId(details.storeId)) { - isPrivate = false; - userContextId = global.getContainerForCookieStoreId(details.storeId); - if (!userContextId) { - return; - } - } - } - - let storeId = DEFAULT_STORE; - if (isPrivate) { - storeId = PRIVATE_STORE; - } else if ("storeId" in details) { - storeId = details.storeId; - } - - // We can use getCookiesFromHost for faster searching. - let enumerator; - let uri; - if ("url" in details) { - try { - uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL); - Services.cookies.usePrivateMode(isPrivate, () => { - enumerator = Services.cookies.getCookiesFromHost(uri.host, {userContextId}); - }); - } catch (ex) { - // This often happens for about: URLs - return; - } - } else if ("domain" in details) { - Services.cookies.usePrivateMode(isPrivate, () => { - enumerator = Services.cookies.getCookiesFromHost(details.domain, {userContextId}); - }); - } else { - Services.cookies.usePrivateMode(isPrivate, () => { - enumerator = Services.cookies.enumerator; - }); - } - - // Based on nsCookieService::GetCookieStringInternal - function matches(cookie) { - function domainMatches(host) { - return cookie.rawHost == host || (cookie.isDomain && host.endsWith(cookie.host)); - } - - function pathMatches(path) { - let cookiePath = cookie.path.replace(/\/$/, ""); - - if (!path.startsWith(cookiePath)) { - return false; - } - - // path == cookiePath, but without the redundant string compare. - if (path.length == cookiePath.length) { - return true; - } - - // URL path is a substring of the cookie path, so it matches if, and - // only if, the next character is a path delimiter. - let pathDelimiters = ["/", "?", "#", ";"]; - return pathDelimiters.includes(path[cookiePath.length]); - } - - // "Restricts the retrieved cookies to those that would match the given URL." - if (uri) { - if (!domainMatches(uri.host)) { - return false; - } - - if (cookie.isSecure && uri.scheme != "https") { - return false; - } - - if (!pathMatches(uri.path)) { - return false; - } - } - - if ("name" in details && details.name != cookie.name) { - return false; - } - - if (userContextId != cookie.originAttributes.userContextId) { - return false; - } - - // "Restricts the retrieved cookies to those whose domains match or are subdomains of this one." - if ("domain" in details && !isSubdomain(cookie.rawHost, details.domain)) { - return false; - } - - // "Restricts the retrieved cookies to those whose path exactly matches this string."" - if ("path" in details && details.path != cookie.path) { - return false; - } - - if ("secure" in details && details.secure != cookie.isSecure) { - return false; - } - - if ("session" in details && details.session != cookie.isSession) { - return false; - } - - // Check that the extension has permissions for this host. - if (!context.extension.whiteListedHosts.matchesCookie(cookie)) { - return false; - } - - return true; - } - - while (enumerator.hasMoreElements()) { - let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); - if (matches(cookie)) { - yield {cookie, isPrivate, storeId}; - } - } -} - -extensions.registerSchemaAPI("cookies", "addon_parent", context => { - let {extension} = context; - let self = { - cookies: { - get: function(details) { - // FIXME: We don't sort by length of path and creation time. - for (let cookie of query(details, ["url", "name", "storeId"], context)) { - return Promise.resolve(convert(cookie)); - } - - // Found no match. - return Promise.resolve(null); - }, - - getAll: function(details) { - let allowed = ["url", "name", "domain", "path", "secure", "session", "storeId"]; - let result = Array.from(query(details, allowed, context), convert); - - return Promise.resolve(result); - }, - - set: function(details) { - let uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL); - - let path; - if (details.path !== null) { - path = details.path; - } else { - // This interface essentially emulates the behavior of the - // Set-Cookie header. In the case of an omitted path, the cookie - // service uses the directory path of the requesting URL, ignoring - // any filename or query parameters. - path = uri.directory; - } - - let name = details.name !== null ? details.name : ""; - let value = details.value !== null ? details.value : ""; - let secure = details.secure !== null ? details.secure : false; - let httpOnly = details.httpOnly !== null ? details.httpOnly : false; - let isSession = details.expirationDate === null; - let expiry = isSession ? Number.MAX_SAFE_INTEGER : details.expirationDate; - let isPrivate = context.incognito; - let userContextId = 0; - if (global.isDefaultCookieStoreId(details.storeId)) { - isPrivate = false; - } else if (global.isPrivateCookieStoreId(details.storeId)) { - isPrivate = true; - } else if (global.isContainerCookieStoreId(details.storeId)) { - let containerId = global.getContainerForCookieStoreId(details.storeId); - if (containerId === null) { - return Promise.reject({message: `Illegal storeId: ${details.storeId}`}); - } - isPrivate = false; - userContextId = containerId; - } else if (details.storeId !== null) { - return Promise.reject({message: "Unknown storeId"}); - } - - let cookieAttrs = {host: details.domain, path: path, isSecure: secure}; - if (!checkSetCookiePermissions(extension, uri, cookieAttrs)) { - return Promise.reject({message: `Permission denied to set cookie ${JSON.stringify(details)}`}); - } - - // The permission check may have modified the domain, so use - // the new value instead. - Services.cookies.usePrivateMode(isPrivate, () => { - Services.cookies.add(cookieAttrs.host, path, name, value, - secure, httpOnly, isSession, expiry, {userContextId}); - }); - - return self.cookies.get(details); - }, - - remove: function(details) { - for (let {cookie, isPrivate, storeId} of query(details, ["url", "name", "storeId"], context)) { - Services.cookies.usePrivateMode(isPrivate, () => { - Services.cookies.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes); - }); - - // Todo: could there be multiple per subdomain? - return Promise.resolve({ - url: details.url, - name: details.name, - storeId, - }); - } - - return Promise.resolve(null); - }, - - getAllCookieStores: function() { - let data = {}; - for (let window of WindowListManager.browserWindows()) { - let tabs = TabManager.for(extension).getTabs(window); - for (let tab of tabs) { - if (!(tab.cookieStoreId in data)) { - data[tab.cookieStoreId] = []; - } - data[tab.cookieStoreId].push(tab); - } - } - - let result = []; - for (let key in data) { - result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE}); - } - return Promise.resolve(result); - }, - - onChanged: new EventManager(context, "cookies.onChanged", fire => { - let observer = (subject, topic, data) => { - let notify = (removed, cookie, cause) => { - cookie.QueryInterface(Ci.nsICookie2); - - if (extension.whiteListedHosts.matchesCookie(cookie)) { - fire({removed, cookie: convert({cookie, isPrivate: topic == "private-cookie-changed"}), cause}); - } - }; - - // We do our best effort here to map the incompatible states. - switch (data) { - case "deleted": - notify(true, subject, "explicit"); - break; - case "added": - notify(false, subject, "explicit"); - break; - case "changed": - notify(true, subject, "overwrite"); - notify(false, subject, "explicit"); - break; - case "batch-deleted": - subject.QueryInterface(Ci.nsIArray); - for (let i = 0; i < subject.length; i++) { - let cookie = subject.queryElementAt(i, Ci.nsICookie2); - if (!cookie.isSession && cookie.expiry * 1000 <= Date.now()) { - notify(true, cookie, "expired"); - } else { - notify(true, cookie, "evicted"); - } - } - break; - } - }; - - Services.obs.addObserver(observer, "cookie-changed", false); - Services.obs.addObserver(observer, "private-cookie-changed", false); - return () => { - Services.obs.removeObserver(observer, "cookie-changed"); - Services.obs.removeObserver(observer, "private-cookie-changed"); - }; - }).api(), - }, - }; - - return self; -}); diff --git a/toolkit/components/webextensions/ext-downloads.js b/toolkit/components/webextensions/ext-downloads.js deleted file mode 100644 index 132814ae4..000000000 --- a/toolkit/components/webextensions/ext-downloads.js +++ /dev/null @@ -1,799 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Downloads", - "resource://gre/modules/Downloads.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths", - "resource://gre/modules/DownloadPaths.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", - "resource://devtools/shared/event-emitter.js"); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -const { - ignoreEvent, - normalizeTime, - runSafeSync, - SingletonEventManager, - PlatformInfo, -} = ExtensionUtils; - -const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito", - "danger", "mime", "startTime", "endTime", - "estimatedEndTime", "state", - "paused", "canResume", "error", - "bytesReceived", "totalBytes", - "fileSize", "exists", - "byExtensionId", "byExtensionName"]; - -// Fields that we generate onChanged events for. -const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume", - "error", "exists"]; - -// From https://fetch.spec.whatwg.org/#forbidden-header-name -const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING", - "ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD", - "CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT", - "EXPECT", "HOST", "KEEP-ALIVE", "ORIGIN", "REFERER", "TE", "TRAILER", - "TRANSFER-ENCODING", "UPGRADE", "VIA"]; - -const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i; - -class DownloadItem { - constructor(id, download, extension) { - this.id = id; - this.download = download; - this.extension = extension; - this.prechange = {}; - } - - get url() { return this.download.source.url; } - get referrer() { return this.download.source.referrer; } - get filename() { return this.download.target.path; } - get incognito() { return this.download.source.isPrivate; } - get danger() { return "safe"; } // TODO - get mime() { return this.download.contentType; } - get startTime() { return this.download.startTime; } - get endTime() { return null; } // TODO - get estimatedEndTime() { return null; } // TODO - get state() { - if (this.download.succeeded) { - return "complete"; - } - if (this.download.canceled) { - return "interrupted"; - } - return "in_progress"; - } - get paused() { - return this.download.canceled && this.download.hasPartialData && !this.download.error; - } - get canResume() { - return (this.download.stopped || this.download.canceled) && - this.download.hasPartialData && !this.download.error; - } - get error() { - if (!this.download.stopped || this.download.succeeded) { - return null; - } - // TODO store this instead of calculating it - - if (this.download.error) { - if (this.download.error.becauseSourceFailed) { - return "NETWORK_FAILED"; // TODO - } - if (this.download.error.becauseTargetFailed) { - return "FILE_FAILED"; // TODO - } - return "CRASH"; - } - return "USER_CANCELED"; - } - get bytesReceived() { - return this.download.currentBytes; - } - get totalBytes() { - return this.download.hasProgress ? this.download.totalBytes : -1; - } - get fileSize() { - // todo: this is supposed to be post-compression - return this.download.succeeded ? this.download.target.size : -1; - } - get exists() { return this.download.target.exists; } - get byExtensionId() { return this.extension ? this.extension.id : undefined; } - get byExtensionName() { return this.extension ? this.extension.name : undefined; } - - /** - * Create a cloneable version of this object by pulling all the - * fields into simple properties (instead of getters). - * - * @returns {object} A DownloadItem with flat properties, - * suitable for cloning. - */ - serialize() { - let obj = {}; - for (let field of DOWNLOAD_ITEM_FIELDS) { - obj[field] = this[field]; - } - if (obj.startTime) { - obj.startTime = obj.startTime.toISOString(); - } - return obj; - } - - // When a change event fires, handlers can look at how an individual - // field changed by comparing item.fieldname with item.prechange.fieldname. - // After all handlers have been invoked, this gets called to store the - // current values of all fields ahead of the next event. - _change() { - for (let field of DOWNLOAD_ITEM_CHANGE_FIELDS) { - this.prechange[field] = this[field]; - } - } -} - - -// DownloadMap maps back and forth betwen the numeric identifiers used in -// the downloads WebExtension API and a Download object from the Downloads jsm. -// todo: make id and extension info persistent (bug 1247794) -const DownloadMap = { - currentId: 0, - loadPromise: null, - - // Maps numeric id -> DownloadItem - byId: new Map(), - - // Maps Download object -> DownloadItem - byDownload: new WeakMap(), - - lazyInit() { - if (this.loadPromise == null) { - EventEmitter.decorate(this); - this.loadPromise = Downloads.getList(Downloads.ALL).then(list => { - let self = this; - return list.addView({ - onDownloadAdded(download) { - const item = self.newFromDownload(download, null); - self.emit("create", item); - }, - - onDownloadRemoved(download) { - const item = self.byDownload.get(download); - if (item != null) { - self.emit("erase", item); - self.byDownload.delete(download); - self.byId.delete(item.id); - } - }, - - onDownloadChanged(download) { - const item = self.byDownload.get(download); - if (item == null) { - Cu.reportError("Got onDownloadChanged for unknown download object"); - } else { - // We get the first one of these when the download is started. - // In this case, don't emit anything, just initialize prechange. - if (Object.keys(item.prechange).length > 0) { - self.emit("change", item); - } - item._change(); - } - }, - }).then(() => list.getAll()) - .then(downloads => { - downloads.forEach(download => { - this.newFromDownload(download, null); - }); - }) - .then(() => list); - }); - } - return this.loadPromise; - }, - - getDownloadList() { - return this.lazyInit(); - }, - - getAll() { - return this.lazyInit().then(() => this.byId.values()); - }, - - fromId(id) { - const download = this.byId.get(id); - if (!download) { - throw new Error(`Invalid download id ${id}`); - } - return download; - }, - - newFromDownload(download, extension) { - if (this.byDownload.has(download)) { - return this.byDownload.get(download); - } - - const id = ++this.currentId; - let item = new DownloadItem(id, download, extension); - this.byId.set(id, item); - this.byDownload.set(download, item); - return item; - }, - - erase(item) { - // This will need to get more complicated for bug 1255507 but for now we - // only work with downloads in the DownloadList from getAll() - return this.getDownloadList().then(list => { - list.remove(item.download); - }); - }, -}; - -// Create a callable function that filters a DownloadItem based on a -// query object of the type passed to search() or erase(). -function downloadQuery(query) { - let queryTerms = []; - let queryNegativeTerms = []; - if (query.query != null) { - for (let term of query.query) { - if (term[0] == "-") { - queryNegativeTerms.push(term.slice(1).toLowerCase()); - } else { - queryTerms.push(term.toLowerCase()); - } - } - } - - function normalizeDownloadTime(arg, before) { - if (arg == null) { - return before ? Number.MAX_VALUE : 0; - } - return normalizeTime(arg).getTime(); - } - - const startedBefore = normalizeDownloadTime(query.startedBefore, true); - const startedAfter = normalizeDownloadTime(query.startedAfter, false); - // const endedBefore = normalizeDownloadTime(query.endedBefore, true); - // const endedAfter = normalizeDownloadTime(query.endedAfter, false); - - const totalBytesGreater = query.totalBytesGreater || 0; - const totalBytesLess = (query.totalBytesLess != null) - ? query.totalBytesLess : Number.MAX_VALUE; - - // Handle options for which we can have a regular expression and/or - // an explicit value to match. - function makeMatch(regex, value, field) { - if (value == null && regex == null) { - return input => true; - } - - let re; - try { - re = new RegExp(regex || "", "i"); - } catch (err) { - throw new Error(`Invalid ${field}Regex: ${err.message}`); - } - if (value == null) { - return input => re.test(input); - } - - value = value.toLowerCase(); - if (re.test(value)) { - return input => (value == input); - } - return input => false; - } - - const matchFilename = makeMatch(query.filenameRegex, query.filename, "filename"); - const matchUrl = makeMatch(query.urlRegex, query.url, "url"); - - return function(item) { - const url = item.url.toLowerCase(); - const filename = item.filename.toLowerCase(); - - if (!queryTerms.every(term => url.includes(term) || filename.includes(term))) { - return false; - } - - if (queryNegativeTerms.some(term => url.includes(term) || filename.includes(term))) { - return false; - } - - if (!matchFilename(filename) || !matchUrl(url)) { - return false; - } - - if (!item.startTime) { - if (query.startedBefore != null || query.startedAfter != null) { - return false; - } - } else if (item.startTime > startedBefore || item.startTime < startedAfter) { - return false; - } - - // todo endedBefore, endedAfter - - if (item.totalBytes == -1) { - if (query.totalBytesGreater != null || query.totalBytesLess != null) { - return false; - } - } else if (item.totalBytes <= totalBytesGreater || item.totalBytes >= totalBytesLess) { - return false; - } - - // todo: include danger - const SIMPLE_ITEMS = ["id", "mime", "startTime", "endTime", "state", - "paused", "error", - "bytesReceived", "totalBytes", "fileSize", "exists"]; - for (let field of SIMPLE_ITEMS) { - if (query[field] != null && item[field] != query[field]) { - return false; - } - } - - return true; - }; -} - -function queryHelper(query) { - let matchFn; - try { - matchFn = downloadQuery(query); - } catch (err) { - return Promise.reject({message: err.message}); - } - - let compareFn; - if (query.orderBy != null) { - const fields = query.orderBy.map(field => field[0] == "-" - ? {reverse: true, name: field.slice(1)} - : {reverse: false, name: field}); - - for (let field of fields) { - if (!DOWNLOAD_ITEM_FIELDS.includes(field.name)) { - return Promise.reject({message: `Invalid orderBy field ${field.name}`}); - } - } - - compareFn = (dl1, dl2) => { - for (let field of fields) { - const val1 = dl1[field.name]; - const val2 = dl2[field.name]; - - if (val1 < val2) { - return field.reverse ? 1 : -1; - } else if (val1 > val2) { - return field.reverse ? -1 : 1; - } - } - return 0; - }; - } - - return DownloadMap.getAll().then(downloads => { - if (compareFn) { - downloads = Array.from(downloads); - downloads.sort(compareFn); - } - let results = []; - for (let download of downloads) { - if (query.limit && results.length >= query.limit) { - break; - } - if (matchFn(download)) { - results.push(download); - } - } - return results; - }); -} - -extensions.registerSchemaAPI("downloads", "addon_parent", context => { - let {extension} = context; - return { - downloads: { - download(options) { - let {filename} = options; - if (filename && PlatformInfo.os === "win") { - // cross platform javascript code uses "/" - filename = filename.replace(/\//g, "\\"); - } - - if (filename != null) { - if (filename.length == 0) { - return Promise.reject({message: "filename must not be empty"}); - } - - let path = OS.Path.split(filename); - if (path.absolute) { - return Promise.reject({message: "filename must not be an absolute path"}); - } - - if (path.components.some(component => component == "..")) { - return Promise.reject({message: "filename must not contain back-references (..)"}); - } - } - - if (options.conflictAction == "prompt") { - // TODO - return Promise.reject({message: "conflictAction prompt not yet implemented"}); - } - - if (options.headers) { - for (let {name} of options.headers) { - if (FORBIDDEN_HEADERS.includes(name.toUpperCase()) || name.match(FORBIDDEN_PREFIXES)) { - return Promise.reject({message: "Forbidden request header name"}); - } - } - } - - // Handle method, headers and body options. - function adjustChannel(channel) { - if (channel instanceof Ci.nsIHttpChannel) { - const method = options.method || "GET"; - channel.requestMethod = method; - - if (options.headers) { - for (let {name, value} of options.headers) { - channel.setRequestHeader(name, value, false); - } - } - - if (options.body != null) { - const stream = Cc["@mozilla.org/io/string-input-stream;1"] - .createInstance(Ci.nsIStringInputStream); - stream.setData(options.body, options.body.length); - - channel.QueryInterface(Ci.nsIUploadChannel2); - channel.explicitSetUploadStream(stream, null, -1, method, false); - } - } - return Promise.resolve(); - } - - function createTarget(downloadsDir) { - let target; - if (filename) { - target = OS.Path.join(downloadsDir, filename); - } else { - let uri = NetUtil.newURI(options.url); - - let remote = "download"; - if (uri instanceof Ci.nsIURL) { - remote = uri.fileName; - } - target = OS.Path.join(downloadsDir, remote); - } - - // Create any needed subdirectories if required by filename. - const dir = OS.Path.dirname(target); - return OS.File.makeDir(dir, {from: downloadsDir}).then(() => { - return OS.File.exists(target); - }).then(exists => { - // This has a race, something else could come along and create - // the file between this test and them time the download code - // creates the target file. But we can't easily fix it without - // modifying DownloadCore so we live with it for now. - if (exists) { - switch (options.conflictAction) { - case "uniquify": - default: - target = DownloadPaths.createNiceUniqueFile(new FileUtils.File(target)).path; - break; - - case "overwrite": - break; - } - } - }).then(() => { - if (!options.saveAs) { - return Promise.resolve(target); - } - - // Setup the file picker Save As dialog. - const picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - const window = Services.wm.getMostRecentWindow("navigator:browser"); - picker.init(window, null, Ci.nsIFilePicker.modeSave); - picker.displayDirectory = new FileUtils.File(dir); - picker.appendFilters(Ci.nsIFilePicker.filterAll); - picker.defaultString = OS.Path.basename(target); - - // Open the dialog and resolve/reject with the result. - return new Promise((resolve, reject) => { - picker.open(result => { - if (result === Ci.nsIFilePicker.returnCancel) { - reject({message: "Download canceled by the user"}); - } else { - resolve(picker.file.path); - } - }); - }); - }); - } - - let download; - return Downloads.getPreferredDownloadsDirectory() - .then(downloadsDir => createTarget(downloadsDir)) - .then(target => { - const source = { - url: options.url, - }; - - if (options.method || options.headers || options.body) { - source.adjustChannel = adjustChannel; - } - - return Downloads.createDownload({ - source, - target: { - path: target, - partFilePath: target + ".part", - }, - }); - }).then(dl => { - download = dl; - return DownloadMap.getDownloadList(); - }).then(list => { - list.add(download); - - // This is necessary to make pause/resume work. - download.tryToKeepPartialData = true; - download.start(); - - const item = DownloadMap.newFromDownload(download, extension); - return item.id; - }); - }, - - removeFile(id) { - return DownloadMap.lazyInit().then(() => { - let item; - try { - item = DownloadMap.fromId(id); - } catch (err) { - return Promise.reject({message: `Invalid download id ${id}`}); - } - if (item.state !== "complete") { - return Promise.reject({message: `Cannot remove incomplete download id ${id}`}); - } - return OS.File.remove(item.filename, {ignoreAbsent: false}).catch((err) => { - return Promise.reject({message: `Could not remove download id ${item.id} because the file doesn't exist`}); - }); - }); - }, - - search(query) { - return queryHelper(query) - .then(items => items.map(item => item.serialize())); - }, - - pause(id) { - return DownloadMap.lazyInit().then(() => { - let item; - try { - item = DownloadMap.fromId(id); - } catch (err) { - return Promise.reject({message: `Invalid download id ${id}`}); - } - if (item.state != "in_progress") { - return Promise.reject({message: `Download ${id} cannot be paused since it is in state ${item.state}`}); - } - - return item.download.cancel(); - }); - }, - - resume(id) { - return DownloadMap.lazyInit().then(() => { - let item; - try { - item = DownloadMap.fromId(id); - } catch (err) { - return Promise.reject({message: `Invalid download id ${id}`}); - } - if (!item.canResume) { - return Promise.reject({message: `Download ${id} cannot be resumed`}); - } - - return item.download.start(); - }); - }, - - cancel(id) { - return DownloadMap.lazyInit().then(() => { - let item; - try { - item = DownloadMap.fromId(id); - } catch (err) { - return Promise.reject({message: `Invalid download id ${id}`}); - } - if (item.download.succeeded) { - return Promise.reject({message: `Download ${id} is already complete`}); - } - return item.download.finalize(true); - }); - }, - - showDefaultFolder() { - Downloads.getPreferredDownloadsDirectory().then(dir => { - let dirobj = new FileUtils.File(dir); - if (dirobj.isDirectory()) { - dirobj.launch(); - } else { - throw new Error(`Download directory ${dirobj.path} is not actually a directory`); - } - }).catch(Cu.reportError); - }, - - erase(query) { - return queryHelper(query).then(items => { - let results = []; - let promises = []; - for (let item of items) { - promises.push(DownloadMap.erase(item)); - results.push(item.id); - } - return Promise.all(promises).then(() => results); - }); - }, - - open(downloadId) { - return DownloadMap.lazyInit().then(() => { - let download = DownloadMap.fromId(downloadId).download; - if (download.succeeded) { - return download.launch(); - } - return Promise.reject({message: "Download has not completed."}); - }).catch((error) => { - return Promise.reject({message: error.message}); - }); - }, - - show(downloadId) { - return DownloadMap.lazyInit().then(() => { - let download = DownloadMap.fromId(downloadId); - return download.download.showContainingDirectory(); - }).then(() => { - return true; - }).catch(error => { - return Promise.reject({message: error.message}); - }); - }, - - getFileIcon(downloadId, options) { - return DownloadMap.lazyInit().then(() => { - let size = options && options.size ? options.size : 32; - let download = DownloadMap.fromId(downloadId).download; - let pathPrefix = ""; - let path; - - if (download.succeeded) { - let file = FileUtils.File(download.target.path); - path = Services.io.newFileURI(file).spec; - } else { - path = OS.Path.basename(download.target.path); - pathPrefix = "//"; - } - - return new Promise((resolve, reject) => { - let chromeWebNav = Services.appShell.createWindowlessBrowser(true); - chromeWebNav - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal()); - - let img = chromeWebNav.document.createElement("img"); - img.width = size; - img.height = size; - - let handleLoad; - let handleError; - const cleanup = () => { - img.removeEventListener("load", handleLoad); - img.removeEventListener("error", handleError); - chromeWebNav.close(); - chromeWebNav = null; - }; - - handleLoad = () => { - let canvas = chromeWebNav.document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - let context = canvas.getContext("2d"); - context.drawImage(img, 0, 0, size, size); - let dataURL = canvas.toDataURL("image/png"); - cleanup(); - resolve(dataURL); - }; - - handleError = (error) => { - Cu.reportError(error); - cleanup(); - reject(new Error("An unexpected error occurred")); - }; - - img.addEventListener("load", handleLoad); - img.addEventListener("error", handleError); - img.src = `moz-icon:${pathPrefix}${path}?size=${size}`; - }); - }).catch((error) => { - return Promise.reject({message: error.message}); - }); - }, - - // When we do setShelfEnabled(), check for additional "downloads.shelf" permission. - // i.e.: - // setShelfEnabled(enabled) { - // if (!extension.hasPermission("downloads.shelf")) { - // throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing."); - // } - // ... - // } - - onChanged: new SingletonEventManager(context, "downloads.onChanged", fire => { - const handler = (what, item) => { - let changes = {}; - const noundef = val => (val === undefined) ? null : val; - DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => { - if (item[fld] != item.prechange[fld]) { - changes[fld] = { - previous: noundef(item.prechange[fld]), - current: noundef(item[fld]), - }; - } - }); - if (Object.keys(changes).length > 0) { - changes.id = item.id; - runSafeSync(context, fire, changes); - } - }; - - let registerPromise = DownloadMap.getDownloadList().then(() => { - DownloadMap.on("change", handler); - }); - return () => { - registerPromise.then(() => { - DownloadMap.off("change", handler); - }); - }; - }).api(), - - onCreated: new SingletonEventManager(context, "downloads.onCreated", fire => { - const handler = (what, item) => { - runSafeSync(context, fire, item.serialize()); - }; - let registerPromise = DownloadMap.getDownloadList().then(() => { - DownloadMap.on("create", handler); - }); - return () => { - registerPromise.then(() => { - DownloadMap.off("create", handler); - }); - }; - }).api(), - - onErased: new SingletonEventManager(context, "downloads.onErased", fire => { - const handler = (what, item) => { - runSafeSync(context, fire, item.id); - }; - let registerPromise = DownloadMap.getDownloadList().then(() => { - DownloadMap.on("erase", handler); - }); - return () => { - registerPromise.then(() => { - DownloadMap.off("erase", handler); - }); - }; - }).api(), - - onDeterminingFilename: ignoreEvent(context, "downloads.onDeterminingFilename"), - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-extension.js b/toolkit/components/webextensions/ext-extension.js deleted file mode 100644 index c4bdc8b63..000000000 --- a/toolkit/components/webextensions/ext-extension.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; - -extensions.registerSchemaAPI("extension", "addon_parent", context => { - return { - extension: { - get lastError() { - return context.lastError; - }, - - isAllowedIncognitoAccess() { - return Promise.resolve(true); - }, - - isAllowedFileSchemeAccess() { - return Promise.resolve(false); - }, - }, - }; -}); - diff --git a/toolkit/components/webextensions/ext-i18n.js b/toolkit/components/webextensions/ext-i18n.js deleted file mode 100644 index bb4bde4bd..000000000 --- a/toolkit/components/webextensions/ext-i18n.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -var { - detectLanguage, -} = ExtensionUtils; - -function i18nApiFactory(context) { - let {extension} = context; - return { - i18n: { - getMessage: function(messageName, substitutions) { - return extension.localizeMessage(messageName, substitutions, {cloneScope: context.cloneScope}); - }, - - getAcceptLanguages: function() { - let result = extension.localeData.acceptLanguages; - return Promise.resolve(result); - }, - - getUILanguage: function() { - return extension.localeData.uiLocale; - }, - - detectLanguage: function(text) { - return detectLanguage(text); - }, - }, - }; -} -extensions.registerSchemaAPI("i18n", "addon_child", i18nApiFactory); -extensions.registerSchemaAPI("i18n", "content_child", i18nApiFactory); diff --git a/toolkit/components/webextensions/ext-idle.js b/toolkit/components/webextensions/ext-idle.js deleted file mode 100644 index c5be4b600..000000000 --- a/toolkit/components/webextensions/ext-idle.js +++ /dev/null @@ -1,94 +0,0 @@ -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", - "resource://devtools/shared/event-emitter.js"); -XPCOMUtils.defineLazyServiceGetter(this, "idleService", - "@mozilla.org/widget/idleservice;1", - "nsIIdleService"); -const { - SingletonEventManager, -} = ExtensionUtils; - -// WeakMap[Extension -> Object] -var observersMap = new WeakMap(); - -function getObserverInfo(extension, context) { - let observerInfo = observersMap.get(extension); - if (!observerInfo) { - observerInfo = { - observer: null, - detectionInterval: 60, - }; - observersMap.set(extension, observerInfo); - context.callOnClose({ - close: () => { - let {observer, detectionInterval} = observersMap.get(extension); - if (observer) { - idleService.removeIdleObserver(observer, detectionInterval); - } - observersMap.delete(extension); - }, - }); - } - return observerInfo; -} - -function getObserver(extension, context) { - let observerInfo = getObserverInfo(extension, context); - let {observer, detectionInterval} = observerInfo; - if (!observer) { - observer = { - observe: function(subject, topic, data) { - if (topic == "idle" || topic == "active") { - this.emit("stateChanged", topic); - } - }, - }; - EventEmitter.decorate(observer); - idleService.addIdleObserver(observer, detectionInterval); - observerInfo.observer = observer; - observerInfo.detectionInterval = detectionInterval; - } - return observer; -} - -function setDetectionInterval(extension, context, newInterval) { - let observerInfo = getObserverInfo(extension, context); - let {observer, detectionInterval} = observerInfo; - if (observer) { - idleService.removeIdleObserver(observer, detectionInterval); - idleService.addIdleObserver(observer, newInterval); - } - observerInfo.detectionInterval = newInterval; -} - -extensions.registerSchemaAPI("idle", "addon_parent", context => { - let {extension} = context; - return { - idle: { - queryState: function(detectionIntervalInSeconds) { - if (idleService.idleTime < detectionIntervalInSeconds * 1000) { - return Promise.resolve("active"); - } - return Promise.resolve("idle"); - }, - setDetectionInterval: function(detectionIntervalInSeconds) { - setDetectionInterval(extension, context, detectionIntervalInSeconds); - }, - onStateChanged: new SingletonEventManager(context, "idle.onStateChanged", fire => { - let listener = (event, data) => { - context.runSafe(fire, data); - }; - - getObserver(extension, context).on("stateChanged", listener); - return () => { - getObserver(extension, context).off("stateChanged", listener); - }; - }).api(), - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-management.js b/toolkit/components/webextensions/ext-management.js deleted file mode 100644 index 59a7959d7..000000000 --- a/toolkit/components/webextensions/ext-management.js +++ /dev/null @@ -1,109 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -XPCOMUtils.defineLazyGetter(this, "strBundle", function() { - const stringSvc = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService); - return stringSvc.createBundle("chrome://global/locale/extensions.properties"); -}); -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "promptService", - "@mozilla.org/embedcomp/prompt-service;1", - "nsIPromptService"); - -function _(key, ...args) { - if (args.length) { - return strBundle.formatStringFromName(key, args, args.length); - } - return strBundle.GetStringFromName(key); -} - -function installType(addon) { - if (addon.temporarilyInstalled) { - return "development"; - } else if (addon.foreignInstall) { - return "sideload"; - } else if (addon.isSystem) { - return "other"; - } - return "normal"; -} - -extensions.registerSchemaAPI("management", "addon_parent", context => { - let {extension} = context; - return { - management: { - getSelf: function() { - return new Promise((resolve, reject) => AddonManager.getAddonByID(extension.id, addon => { - try { - let m = extension.manifest; - let extInfo = { - id: extension.id, - name: addon.name, - shortName: m.short_name || "", - description: addon.description || "", - version: addon.version, - mayDisable: !!(addon.permissions & AddonManager.PERM_CAN_DISABLE), - enabled: addon.isActive, - optionsUrl: addon.optionsURL || "", - permissions: Array.from(extension.permissions).filter(perm => { - return !extension.whiteListedHosts.pat.includes(perm); - }), - hostPermissions: extension.whiteListedHosts.pat, - installType: installType(addon), - }; - if (addon.homepageURL) { - extInfo.homepageUrl = addon.homepageURL; - } - if (addon.updateURL) { - extInfo.updateUrl = addon.updateURL; - } - if (m.icons) { - extInfo.icons = Object.keys(m.icons).map(key => { - return {size: Number(key), url: m.icons[key]}; - }); - } - - resolve(extInfo); - } catch (err) { - reject(err); - } - })); - }, - - uninstallSelf: function(options) { - return new Promise((resolve, reject) => { - if (options && options.showConfirmDialog) { - let message = _("uninstall.confirmation.message", extension.name); - if (options.dialogMessage) { - message = `${options.dialogMessage}\n${message}`; - } - let title = _("uninstall.confirmation.title", extension.name); - let buttonFlags = promptService.BUTTON_POS_0 * promptService.BUTTON_TITLE_IS_STRING + - promptService.BUTTON_POS_1 * promptService.BUTTON_TITLE_IS_STRING; - let button0Title = _("uninstall.confirmation.button-0.label"); - let button1Title = _("uninstall.confirmation.button-1.label"); - let response = promptService.confirmEx(null, title, message, buttonFlags, button0Title, button1Title, null, null, {value: 0}); - if (response == 1) { - return reject({message: "User cancelled uninstall of extension"}); - } - } - AddonManager.getAddonByID(extension.id, addon => { - let canUninstall = Boolean(addon.permissions & AddonManager.PERM_CAN_UNINSTALL); - if (!canUninstall) { - return reject({message: "The add-on cannot be uninstalled"}); - } - try { - addon.uninstall(); - } catch (err) { - return reject(err); - } - }); - }); - }, - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-notifications.js b/toolkit/components/webextensions/ext-notifications.js deleted file mode 100644 index 1df96a2ac..000000000 --- a/toolkit/components/webextensions/ext-notifications.js +++ /dev/null @@ -1,161 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", - "resource://devtools/shared/event-emitter.js"); - -var { - EventManager, - ignoreEvent, -} = ExtensionUtils; - -// WeakMap[Extension -> Map[id -> Notification]] -var notificationsMap = new WeakMap(); - -// Manages a notification popup (notifications API) created by the extension. -function Notification(extension, id, options) { - this.extension = extension; - this.id = id; - this.options = options; - - let imageURL; - if (options.iconUrl) { - imageURL = this.extension.baseURI.resolve(options.iconUrl); - } - - try { - let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); - svc.showAlertNotification(imageURL, - options.title, - options.message, - true, // textClickable - this.id, - this, - this.id); - } catch (e) { - // This will fail if alerts aren't available on the system. - } -} - -Notification.prototype = { - clear() { - try { - let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); - svc.closeAlert(this.id); - } catch (e) { - // This will fail if the OS doesn't support this function. - } - notificationsMap.get(this.extension).delete(this.id); - }, - - observe(subject, topic, data) { - let notifications = notificationsMap.get(this.extension); - - let emitAndDelete = event => { - notifications.emit(event, data); - notifications.delete(this.id); - }; - - // Don't try to emit events if the extension has been unloaded - if (!notifications) { - return; - } - - if (topic === "alertclickcallback") { - emitAndDelete("clicked"); - } - if (topic === "alertfinished") { - emitAndDelete("closed"); - } - }, -}; - -/* eslint-disable mozilla/balanced-listeners */ -extensions.on("startup", (type, extension) => { - let map = new Map(); - EventEmitter.decorate(map); - notificationsMap.set(extension, map); -}); - -extensions.on("shutdown", (type, extension) => { - if (notificationsMap.has(extension)) { - for (let notification of notificationsMap.get(extension).values()) { - notification.clear(); - } - notificationsMap.delete(extension); - } -}); -/* eslint-enable mozilla/balanced-listeners */ - -var nextId = 0; - -extensions.registerSchemaAPI("notifications", "addon_parent", context => { - let {extension} = context; - return { - notifications: { - create: function(notificationId, options) { - if (!notificationId) { - notificationId = String(nextId++); - } - - let notifications = notificationsMap.get(extension); - if (notifications.has(notificationId)) { - notifications.get(notificationId).clear(); - } - - // FIXME: Lots of options still aren't supported, especially - // buttons. - let notification = new Notification(extension, notificationId, options); - notificationsMap.get(extension).set(notificationId, notification); - - return Promise.resolve(notificationId); - }, - - clear: function(notificationId) { - let notifications = notificationsMap.get(extension); - if (notifications.has(notificationId)) { - notifications.get(notificationId).clear(); - return Promise.resolve(true); - } - return Promise.resolve(false); - }, - - getAll: function() { - let result = {}; - notificationsMap.get(extension).forEach((value, key) => { - result[key] = value.options; - }); - return Promise.resolve(result); - }, - - onClosed: new EventManager(context, "notifications.onClosed", fire => { - let listener = (event, notificationId) => { - // FIXME: Support the byUser argument. - fire(notificationId, true); - }; - - notificationsMap.get(extension).on("closed", listener); - return () => { - notificationsMap.get(extension).off("closed", listener); - }; - }).api(), - - onClicked: new EventManager(context, "notifications.onClicked", fire => { - let listener = (event, notificationId) => { - fire(notificationId, true); - }; - - notificationsMap.get(extension).on("clicked", listener); - return () => { - notificationsMap.get(extension).off("clicked", listener); - }; - }).api(), - - // Intend to implement this later: https://bugzilla.mozilla.org/show_bug.cgi?id=1190681 - onButtonClicked: ignoreEvent(context, "notifications.onButtonClicked"), - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-runtime.js b/toolkit/components/webextensions/ext-runtime.js deleted file mode 100644 index aed3ffd4b..000000000 --- a/toolkit/components/webextensions/ext-runtime.js +++ /dev/null @@ -1,134 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Extension", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); - -var { - SingletonEventManager, -} = ExtensionUtils; - -extensions.registerSchemaAPI("runtime", "addon_parent", context => { - let {extension} = context; - return { - runtime: { - onStartup: new SingletonEventManager(context, "runtime.onStartup", fire => { - if (context.incognito) { - // This event should not fire if we are operating in a private profile. - return () => {}; - } - let listener = () => { - if (extension.startupReason === "APP_STARTUP") { - fire(); - } - }; - extension.on("startup", listener); - return () => { - extension.off("startup", listener); - }; - }).api(), - - onInstalled: new SingletonEventManager(context, "runtime.onInstalled", fire => { - let listener = () => { - switch (extension.startupReason) { - case "APP_STARTUP": - if (Extension.browserUpdated) { - fire({reason: "browser_update"}); - } - break; - case "ADDON_INSTALL": - fire({reason: "install"}); - break; - case "ADDON_UPGRADE": - fire({reason: "update"}); - break; - } - }; - extension.on("startup", listener); - return () => { - extension.off("startup", listener); - }; - }).api(), - - onUpdateAvailable: new SingletonEventManager(context, "runtime.onUpdateAvailable", fire => { - let instanceID = extension.addonData.instanceID; - AddonManager.addUpgradeListener(instanceID, upgrade => { - extension.upgrade = upgrade; - let details = { - version: upgrade.version, - }; - context.runSafe(fire, details); - }); - return () => { - AddonManager.removeUpgradeListener(instanceID); - }; - }).api(), - - reload: () => { - if (extension.upgrade) { - // If there is a pending update, install it now. - extension.upgrade.install(); - } else { - // Otherwise, reload the current extension. - AddonManager.getAddonByID(extension.id, addon => { - addon.reload(); - }); - } - }, - - get lastError() { - // TODO(robwu): Figure out how to make sure that errors in the parent - // process are propagated to the child process. - // lastError should not be accessed from the parent. - return context.lastError; - }, - - getBrowserInfo: function() { - const {name, vendor, version, appBuildID} = Services.appinfo; - const info = {name, vendor, version, buildID: appBuildID}; - return Promise.resolve(info); - }, - - getPlatformInfo: function() { - return Promise.resolve(ExtensionUtils.PlatformInfo); - }, - - openOptionsPage: function() { - if (!extension.manifest.options_ui) { - return Promise.reject({message: "No `options_ui` declared"}); - } - - return openOptionsPage(extension).then(() => {}); - }, - - setUninstallURL: function(url) { - if (url.length == 0) { - return Promise.resolve(); - } - - let uri; - try { - uri = NetUtil.newURI(url); - } catch (e) { - return Promise.reject({message: `Invalid URL: ${JSON.stringify(url)}`}); - } - - if (uri.scheme != "http" && uri.scheme != "https") { - return Promise.reject({message: "url must have the scheme http or https"}); - } - - extension.uninstallURL = url; - return Promise.resolve(); - }, - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-storage.js b/toolkit/components/webextensions/ext-storage.js deleted file mode 100644 index b1e22c46c..000000000 --- a/toolkit/components/webextensions/ext-storage.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage", - "resource://gre/modules/ExtensionStorage.jsm"); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -var { - EventManager, -} = ExtensionUtils; - -function storageApiFactory(context) { - let {extension} = context; - return { - storage: { - local: { - get: function(spec) { - return ExtensionStorage.get(extension.id, spec); - }, - set: function(items) { - return ExtensionStorage.set(extension.id, items, context); - }, - remove: function(keys) { - return ExtensionStorage.remove(extension.id, keys); - }, - clear: function() { - return ExtensionStorage.clear(extension.id); - }, - }, - - onChanged: new EventManager(context, "storage.onChanged", fire => { - let listenerLocal = changes => { - fire(changes, "local"); - }; - - ExtensionStorage.addOnChangedListener(extension.id, listenerLocal); - return () => { - ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal); - }; - }).api(), - }, - }; -} -extensions.registerSchemaAPI("storage", "addon_parent", storageApiFactory); -extensions.registerSchemaAPI("storage", "content_parent", storageApiFactory); diff --git a/toolkit/components/webextensions/ext-topSites.js b/toolkit/components/webextensions/ext-topSites.js deleted file mode 100644 index a66ac85d9..000000000 --- a/toolkit/components/webextensions/ext-topSites.js +++ /dev/null @@ -1,24 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", - "resource://gre/modules/NewTabUtils.jsm"); - -extensions.registerSchemaAPI("topSites", "addon_parent", context => { - return { - topSites: { - get: function() { - let urls = NewTabUtils.links.getLinks() - .filter(link => !!link) - .map(link => { - return { - url: link.url, - title: link.title, - }; - }); - return Promise.resolve(urls); - }, - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-webNavigation.js b/toolkit/components/webextensions/ext-webNavigation.js deleted file mode 100644 index 904f3a4a7..000000000 --- a/toolkit/components/webextensions/ext-webNavigation.js +++ /dev/null @@ -1,192 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchURLFilters", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "WebNavigation", - "resource://gre/modules/WebNavigation.jsm"); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -var { - SingletonEventManager, - ignoreEvent, -} = ExtensionUtils; - -const defaultTransitionTypes = { - topFrame: "link", - subFrame: "auto_subframe", -}; - -const frameTransitions = { - anyFrame: { - qualifiers: ["server_redirect", "client_redirect", "forward_back"], - }, - topFrame: { - types: ["reload", "form_submit"], - }, -}; - -const tabTransitions = { - topFrame: { - qualifiers: ["from_address_bar"], - types: ["auto_bookmark", "typed", "keyword", "generated", "link"], - }, - subFrame: { - types: ["manual_subframe"], - }, -}; - -function isTopLevelFrame({frameId, parentFrameId}) { - return frameId == 0 && parentFrameId == -1; -} - -function fillTransitionProperties(eventName, src, dst) { - if (eventName == "onCommitted" || eventName == "onHistoryStateUpdated") { - let frameTransitionData = src.frameTransitionData || {}; - let tabTransitionData = src.tabTransitionData || {}; - - let transitionType, transitionQualifiers = []; - - // Fill transition properties for any frame. - for (let qualifier of frameTransitions.anyFrame.qualifiers) { - if (frameTransitionData[qualifier]) { - transitionQualifiers.push(qualifier); - } - } - - if (isTopLevelFrame(dst)) { - for (let type of frameTransitions.topFrame.types) { - if (frameTransitionData[type]) { - transitionType = type; - } - } - - for (let qualifier of tabTransitions.topFrame.qualifiers) { - if (tabTransitionData[qualifier]) { - transitionQualifiers.push(qualifier); - } - } - - for (let type of tabTransitions.topFrame.types) { - if (tabTransitionData[type]) { - transitionType = type; - } - } - - // If transitionType is not defined, defaults it to "link". - if (!transitionType) { - transitionType = defaultTransitionTypes.topFrame; - } - } else { - // If it is sub-frame, transitionType defaults it to "auto_subframe", - // "manual_subframe" is set only in case of a recent user interaction. - transitionType = tabTransitionData.link ? - "manual_subframe" : defaultTransitionTypes.subFrame; - } - - // Fill the transition properties in the webNavigation event object. - dst.transitionType = transitionType; - dst.transitionQualifiers = transitionQualifiers; - } -} - -// Similar to WebRequestEventManager but for WebNavigation. -function WebNavigationEventManager(context, eventName) { - let name = `webNavigation.${eventName}`; - let register = (callback, urlFilters) => { - // Don't create a MatchURLFilters instance if the listener does not include any filter. - let filters = urlFilters ? - new MatchURLFilters(urlFilters.url) : null; - - let listener = data => { - if (!data.browser) { - return; - } - - let data2 = { - url: data.url, - timeStamp: Date.now(), - frameId: ExtensionManagement.getFrameId(data.windowId), - parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId), - }; - - if (eventName == "onErrorOccurred") { - data2.error = data.error; - } - - // Fills in tabId typically. - extensions.emit("fill-browser-data", data.browser, data2); - if (data2.tabId < 0) { - return; - } - - fillTransitionProperties(eventName, data, data2); - - context.runSafe(callback, data2); - }; - - WebNavigation[eventName].addListener(listener, filters); - return () => { - WebNavigation[eventName].removeListener(listener); - }; - }; - - return SingletonEventManager.call(this, context, name, register); -} - -WebNavigationEventManager.prototype = Object.create(SingletonEventManager.prototype); - -function convertGetFrameResult(tabId, data) { - return { - errorOccurred: data.errorOccurred, - url: data.url, - tabId, - frameId: ExtensionManagement.getFrameId(data.windowId), - parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId), - }; -} - -extensions.registerSchemaAPI("webNavigation", "addon_parent", context => { - return { - webNavigation: { - onTabReplaced: ignoreEvent(context, "webNavigation.onTabReplaced"), - onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(), - onCommitted: new WebNavigationEventManager(context, "onCommitted").api(), - onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(), - onCompleted: new WebNavigationEventManager(context, "onCompleted").api(), - onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(), - onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(), - onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(), - onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"), - getAllFrames(details) { - let tab = TabManager.getTab(details.tabId, context); - - let {innerWindowID, messageManager} = tab.linkedBrowser; - let recipient = {innerWindowID}; - - return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient}) - .then((results) => results.map(convertGetFrameResult.bind(null, details.tabId))); - }, - getFrame(details) { - let tab = TabManager.getTab(details.tabId, context); - - let recipient = { - innerWindowID: tab.linkedBrowser.innerWindowID, - }; - - let mm = tab.linkedBrowser.messageManager; - return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, {recipient}) - .then((result) => { - return result ? - convertGetFrameResult(details.tabId, result) : - Promise.reject({message: `No frame found with frameId: ${details.frameId}`}); - }); - }, - }, - }; -}); diff --git a/toolkit/components/webextensions/ext-webRequest.js b/toolkit/components/webextensions/ext-webRequest.js deleted file mode 100644 index f92330131..000000000 --- a/toolkit/components/webextensions/ext-webRequest.js +++ /dev/null @@ -1,115 +0,0 @@ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "WebRequest", - "resource://gre/modules/WebRequest.jsm"); - -Cu.import("resource://gre/modules/ExtensionManagement.jsm"); -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -var { - SingletonEventManager, -} = ExtensionUtils; - -// EventManager-like class specifically for WebRequest. Inherits from -// SingletonEventManager. Takes care of converting |details| parameter -// when invoking listeners. -function WebRequestEventManager(context, eventName) { - let name = `webRequest.${eventName}`; - let register = (callback, filter, info) => { - let listener = data => { - // Prevent listening in on requests originating from system principal to - // prevent tinkering with OCSP, app and addon updates, etc. - if (data.isSystemPrincipal) { - return; - } - - let data2 = { - requestId: data.requestId, - url: data.url, - originUrl: data.originUrl, - method: data.method, - type: data.type, - timeStamp: Date.now(), - frameId: data.type == "main_frame" ? 0 : ExtensionManagement.getFrameId(data.windowId), - parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId), - }; - - const maybeCached = ["onResponseStarted", "onBeforeRedirect", "onCompleted", "onErrorOccurred"]; - if (maybeCached.includes(eventName)) { - data2.fromCache = !!data.fromCache; - } - - if ("ip" in data) { - data2.ip = data.ip; - } - - extensions.emit("fill-browser-data", data.browser, data2); - - let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl", - "requestBody"]; - for (let opt of optional) { - if (opt in data) { - data2[opt] = data[opt]; - } - } - - return context.runSafe(callback, data2); - }; - - let filter2 = {}; - filter2.urls = new MatchPattern(filter.urls); - if (filter.types) { - filter2.types = filter.types; - } - if (filter.tabId) { - filter2.tabId = filter.tabId; - } - if (filter.windowId) { - filter2.windowId = filter.windowId; - } - - let info2 = []; - if (info) { - for (let desc of info) { - if (desc == "blocking" && !context.extension.hasPermission("webRequestBlocking")) { - Cu.reportError("Using webRequest.addListener with the blocking option " + - "requires the 'webRequestBlocking' permission."); - } else { - info2.push(desc); - } - } - } - - WebRequest[eventName].addListener(listener, filter2, info2); - return () => { - WebRequest[eventName].removeListener(listener); - }; - }; - - return SingletonEventManager.call(this, context, name, register); -} - -WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype); - -extensions.registerSchemaAPI("webRequest", "addon_parent", context => { - return { - webRequest: { - onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(), - onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(), - onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(), - onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(), - onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(), - onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(), - onErrorOccurred: new WebRequestEventManager(context, "onErrorOccurred").api(), - onCompleted: new WebRequestEventManager(context, "onCompleted").api(), - handlerBehaviorChanged: function() { - // TODO: Flush all caches. - }, - }, - }; -}); diff --git a/toolkit/components/webextensions/extensions-toolkit.manifest b/toolkit/components/webextensions/extensions-toolkit.manifest deleted file mode 100644 index 4ec65a984..000000000 --- a/toolkit/components/webextensions/extensions-toolkit.manifest +++ /dev/null @@ -1,49 +0,0 @@ -# scripts -category webextension-scripts alarms chrome://extensions/content/ext-alarms.js -category webextension-scripts backgroundPage chrome://extensions/content/ext-backgroundPage.js -category webextension-scripts cookies chrome://extensions/content/ext-cookies.js -category webextension-scripts downloads chrome://extensions/content/ext-downloads.js -category webextension-scripts management chrome://extensions/content/ext-management.js -category webextension-scripts notifications chrome://extensions/content/ext-notifications.js -category webextension-scripts i18n chrome://extensions/content/ext-i18n.js -category webextension-scripts idle chrome://extensions/content/ext-idle.js -category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js -category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js -category webextension-scripts runtime chrome://extensions/content/ext-runtime.js -category webextension-scripts extension chrome://extensions/content/ext-extension.js -category webextension-scripts storage chrome://extensions/content/ext-storage.js -category webextension-scripts topSites chrome://extensions/content/ext-topSites.js - -# scripts specific for content process. -category webextension-scripts-content extension chrome://extensions/content/ext-c-extension.js -category webextension-scripts-content i18n chrome://extensions/content/ext-i18n.js -category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js -category webextension-scripts-content test chrome://extensions/content/ext-c-test.js -category webextension-scripts-content storage chrome://extensions/content/ext-c-storage.js - -# scripts that must run in the same process as addon code. -category webextension-scripts-addon backgroundPage chrome://extensions/content/ext-c-backgroundPage.js -category webextension-scripts-addon extension chrome://extensions/content/ext-c-extension.js -category webextension-scripts-addon i18n chrome://extensions/content/ext-i18n.js -category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js -category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js -category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js - -# schemas -category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json -category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json -category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json -category webextension-schemas events chrome://extensions/content/schemas/events.json -category webextension-schemas extension chrome://extensions/content/schemas/extension.json -category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json -category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json -category webextension-schemas idle chrome://extensions/content/schemas/idle.json -category webextension-schemas management chrome://extensions/content/schemas/management.json -category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json -category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json -category webextension-schemas runtime chrome://extensions/content/schemas/runtime.json -category webextension-schemas storage chrome://extensions/content/schemas/storage.json -category webextension-schemas test chrome://extensions/content/schemas/test.json -category webextension-schemas top_sites chrome://extensions/content/schemas/top_sites.json -category webextension-schemas web_navigation chrome://extensions/content/schemas/web_navigation.json -category webextension-schemas web_request chrome://extensions/content/schemas/web_request.json diff --git a/toolkit/components/webextensions/jar.mn b/toolkit/components/webextensions/jar.mn deleted file mode 100644 index 6d343e1b7..000000000 --- a/toolkit/components/webextensions/jar.mn +++ /dev/null @@ -1,26 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -toolkit.jar: -% content extensions %content/extensions/ - content/extensions/ext-alarms.js - content/extensions/ext-backgroundPage.js - content/extensions/ext-browser-content.js - content/extensions/ext-cookies.js - content/extensions/ext-downloads.js - content/extensions/ext-management.js - content/extensions/ext-notifications.js - content/extensions/ext-i18n.js - content/extensions/ext-idle.js - content/extensions/ext-webRequest.js - content/extensions/ext-webNavigation.js - content/extensions/ext-runtime.js - content/extensions/ext-extension.js - content/extensions/ext-storage.js - content/extensions/ext-topSites.js - content/extensions/ext-c-backgroundPage.js - content/extensions/ext-c-extension.js - content/extensions/ext-c-runtime.js - content/extensions/ext-c-storage.js - content/extensions/ext-c-test.js diff --git a/toolkit/components/webextensions/moz.build b/toolkit/components/webextensions/moz.build deleted file mode 100644 index f32f526f9..000000000 --- a/toolkit/components/webextensions/moz.build +++ /dev/null @@ -1,41 +0,0 @@ -# -*- 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/. - -EXTRA_JS_MODULES += [ - 'Extension.jsm', - 'ExtensionAPI.jsm', - 'ExtensionChild.jsm', - 'ExtensionCommon.jsm', - 'ExtensionContent.jsm', - 'ExtensionManagement.jsm', - 'ExtensionParent.jsm', - 'ExtensionStorage.jsm', - 'ExtensionUtils.jsm', - 'LegacyExtensionsUtils.jsm', - 'MessageChannel.jsm', - 'NativeMessaging.jsm', - 'Schemas.jsm', -] - -EXTRA_COMPONENTS += [ - 'extensions-toolkit.manifest', -] - -TESTING_JS_MODULES += [ - 'ExtensionTestCommon.jsm', - 'ExtensionXPCShellUtils.jsm', -] - -DIRS += ['schemas'] - -JAR_MANIFESTS += ['jar.mn'] - -MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini'] -MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini'] -XPCSHELL_TESTS_MANIFESTS += [ - 'test/xpcshell/native_messaging.ini', - 'test/xpcshell/xpcshell.ini', -] diff --git a/toolkit/components/webextensions/schemas/LICENSE b/toolkit/components/webextensions/schemas/LICENSE deleted file mode 100644 index 9314092fd..000000000 --- a/toolkit/components/webextensions/schemas/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/toolkit/components/webextensions/schemas/alarms.json b/toolkit/components/webextensions/schemas/alarms.json deleted file mode 100644 index 2a72a2842..000000000 --- a/toolkit/components/webextensions/schemas/alarms.json +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "alarms", - "permissions": ["alarms"], - "types": [ - { - "id": "Alarm", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of this alarm." - }, - "scheduledTime": { - "type": "number", - "description": "Time when the alarm is scheduled to fire, in milliseconds past the epoch." - }, - "periodInMinutes": { - "type": "number", - "optional": true, - "description": "When present, signals that the alarm triggers periodically after so many minutes." - } - } - } - ], - "functions": [ - { - "name": "create", - "type": "function", - "description": "Creates an alarm. After the delay is expired, the onAlarm event is fired. If there is another alarm with the same name (or no name if none is specified), it will be cancelled and replaced by this alarm.", - "parameters": [ - { - "type": "string", - "name": "name", - "optional": true, - "description": "Optional name to identify this alarm. Defaults to the empty string." - }, - { - "type": "object", - "name": "alarmInfo", - "description": "Details about the alarm. The alarm first fires either at 'when' milliseconds past the epoch (if 'when' is provided), after 'delayInMinutes' minutes from the current time (if 'delayInMinutes' is provided instead), or after 'periodInMinutes' minutes from the current time (if only 'periodInMinutes' is provided). Users should never provide both 'when' and 'delayInMinutes'. If 'periodInMinutes' is provided, then the alarm recurs repeatedly after that many minutes.", - "properties": { - "when": {"type": "number", "optional": true, - "description": "Time when the alarm is scheduled to first fire, in milliseconds past the epoch."}, - "delayInMinutes": {"type": "number", "optional": true, - "description": "Number of minutes from the current time after which the alarm should first fire."}, - "periodInMinutes": {"type": "number", "optional": true, - "description": "Number of minutes after which the alarm should recur repeatedly."} - } - } - ] - }, - { - "name": "get", - "type": "function", - "description": "Retrieves details about the specified alarm.", - "async": "callback", - "parameters": [ - { - "type": "string", - "name": "name", - "optional": true, - "description": "The name of the alarm to get. Defaults to the empty string." - }, - { - "type": "function", - "name": "callback", - "parameters": [ - { "name": "alarm", "$ref": "Alarm" } - ] - } - ] - }, - { - "name": "getAll", - "type": "function", - "description": "Gets an array of all the alarms.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { "name": "alarms", "type": "array", "items": { "$ref": "Alarm" } } - ] - } - ] - }, - { - "name": "clear", - "type": "function", - "description": "Clears the alarm with the given name.", - "async": "callback", - "parameters": [ - { - "type": "string", - "name": "name", - "optional": true, - "description": "The name of the alarm to clear. Defaults to the empty string." - }, - { - "type": "function", - "name": "callback", - "parameters": [ - { "name": "wasCleared", "type": "boolean", "description": "Whether an alarm of the given name was found to clear." } - ] - } - ] - }, - { - "name": "clearAll", - "type": "function", - "description": "Clears all alarms.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { "name": "wasCleared", "type": "boolean", "description": "Whether any alarm was found to clear." } - ] - } - ] - } - ], - "events": [ - { - "name": "onAlarm", - "type": "function", - "description": "Fired when an alarm has expired. Useful for transient background pages.", - "parameters": [ - { - "name": "name", - "$ref": "Alarm", - "description": "The alarm that has expired." - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/cookies.json b/toolkit/components/webextensions/schemas/cookies.json deleted file mode 100644 index a7de6eb42..000000000 --- a/toolkit/components/webextensions/schemas/cookies.json +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [{ - "type": "string", - "enum": [ - "cookies" - ] - }] - } - ] - }, - { - "namespace": "cookies", - "description": "Use the <code>browser.cookies</code> API to query and modify cookies, and to be notified when they change.", - "permissions": ["cookies"], - "types": [ - { - "id": "Cookie", - "type": "object", - "description": "Represents information about an HTTP cookie.", - "properties": { - "name": {"type": "string", "description": "The name of the cookie."}, - "value": {"type": "string", "description": "The value of the cookie."}, - "domain": {"type": "string", "description": "The domain of the cookie (e.g. \"www.google.com\", \"example.com\")."}, - "hostOnly": {"type": "boolean", "description": "True if the cookie is a host-only cookie (i.e. a request's host must exactly match the domain of the cookie)."}, - "path": {"type": "string", "description": "The path of the cookie."}, - "secure": {"type": "boolean", "description": "True if the cookie is marked as Secure (i.e. its scope is limited to secure channels, typically HTTPS)."}, - "httpOnly": {"type": "boolean", "description": "True if the cookie is marked as HttpOnly (i.e. the cookie is inaccessible to client-side scripts)."}, - "session": {"type": "boolean", "description": "True if the cookie is a session cookie, as opposed to a persistent cookie with an expiration date."}, - "expirationDate": {"type": "number", "optional": true, "description": "The expiration date of the cookie as the number of seconds since the UNIX epoch. Not provided for session cookies."}, - "storeId": {"type": "string", "description": "The ID of the cookie store containing this cookie, as provided in getAllCookieStores()."} - } - }, - { - "id": "CookieStore", - "type": "object", - "description": "Represents a cookie store in the browser. An incognito mode window, for instance, uses a separate cookie store from a non-incognito window.", - "properties": { - "id": {"type": "string", "description": "The unique identifier for the cookie store."}, - "tabIds": {"type": "array", "items": {"type": "integer"}, "description": "Identifiers of all the browser tabs that share this cookie store."} - } - }, - { - "id": "OnChangedCause", - "type": "string", - "enum": ["evicted", "expired", "explicit", "expired_overwrite", "overwrite"], - "description": "The underlying reason behind the cookie's change. If a cookie was inserted, or removed via an explicit call to $(ref:cookies.remove), \"cause\" will be \"explicit\". If a cookie was automatically removed due to expiry, \"cause\" will be \"expired\". If a cookie was removed due to being overwritten with an already-expired expiration date, \"cause\" will be set to \"expired_overwrite\". If a cookie was automatically removed due to garbage collection, \"cause\" will be \"evicted\". If a cookie was automatically removed due to a \"set\" call that overwrote it, \"cause\" will be \"overwrite\". Plan your response accordingly." - } - ], - "functions": [ - { - "name": "get", - "type": "function", - "description": "Retrieves information about a single cookie. If more than one cookie of the same name exists for the given URL, the one with the longest path will be returned. For cookies with the same path length, the cookie with the earliest creation time will be returned.", - "async": "callback", - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Details to identify the cookie being retrieved.", - "properties": { - "url": {"type": "string", "description": "The URL with which the cookie to retrieve is associated. This argument may be a full URL, in which case any data following the URL path (e.g. the query string) is simply ignored. If host permissions for this URL are not specified in the manifest file, the API call will fail."}, - "name": {"type": "string", "description": "The name of the cookie to retrieve."}, - "storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store in which to look for the cookie. By default, the current execution context's cookie store will be used."} - } - }, - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "cookie", "$ref": "Cookie", "optional": true, "description": "Contains details about the cookie. This parameter is null if no such cookie was found." - } - ] - } - ] - }, - { - "name": "getAll", - "type": "function", - "description": "Retrieves all cookies from a single cookie store that match the given information. The cookies returned will be sorted, with those with the longest path first. If multiple cookies have the same path length, those with the earliest creation time will be first.", - "async": "callback", - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Information to filter the cookies being retrieved.", - "properties": { - "url": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those that would match the given URL."}, - "name": {"type": "string", "optional": true, "description": "Filters the cookies by name."}, - "domain": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those whose domains match or are subdomains of this one."}, - "path": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those whose path exactly matches this string."}, - "secure": {"type": "boolean", "optional": true, "description": "Filters the cookies by their Secure property."}, - "session": {"type": "boolean", "optional": true, "description": "Filters out session vs. persistent cookies."}, - "storeId": {"type": "string", "optional": true, "description": "The cookie store to retrieve cookies from. If omitted, the current execution context's cookie store will be used."} - } - }, - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "cookies", "type": "array", "items": {"$ref": "Cookie"}, "description": "All the existing, unexpired cookies that match the given cookie info." - } - ] - } - ] - }, - { - "name": "set", - "type": "function", - "description": "Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.", - "async": "callback", - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Details about the cookie being set.", - "properties": { - "url": {"type": "string", "description": "The request-URI to associate with the setting of the cookie. This value can affect the default domain and path values of the created cookie. If host permissions for this URL are not specified in the manifest file, the API call will fail."}, - "name": {"type": "string", "optional": true, "description": "The name of the cookie. Empty by default if omitted."}, - "value": {"type": "string", "optional": true, "description": "The value of the cookie. Empty by default if omitted."}, - "domain": {"type": "string", "optional": true, "description": "The domain of the cookie. If omitted, the cookie becomes a host-only cookie."}, - "path": {"type": "string", "optional": true, "description": "The path of the cookie. Defaults to the path portion of the url parameter."}, - "secure": {"type": "boolean", "optional": true, "description": "Whether the cookie should be marked as Secure. Defaults to false."}, - "httpOnly": {"type": "boolean", "optional": true, "description": "Whether the cookie should be marked as HttpOnly. Defaults to false."}, - "expirationDate": {"type": "number", "optional": true, "description": "The expiration date of the cookie as the number of seconds since the UNIX epoch. If omitted, the cookie becomes a session cookie."}, - "storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store in which to set the cookie. By default, the cookie is set in the current execution context's cookie store."} - } - }, - { - "type": "function", - "name": "callback", - "optional": true, - "parameters": [ - { - "name": "cookie", "$ref": "Cookie", "optional": true, "description": "Contains details about the cookie that's been set. If setting failed for any reason, this will be \"null\", and $(ref:runtime.lastError) will be set." - } - ] - } - ] - }, - { - "name": "remove", - "type": "function", - "description": "Deletes a cookie by name.", - "async": "callback", - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Information to identify the cookie to remove.", - "properties": { - "url": {"type": "string", "description": "The URL associated with the cookie. If host permissions for this URL are not specified in the manifest file, the API call will fail."}, - "name": {"type": "string", "description": "The name of the cookie to remove."}, - "storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store to look in for the cookie. If unspecified, the cookie is looked for by default in the current execution context's cookie store."} - } - }, - { - "type": "function", - "name": "callback", - "optional": true, - "parameters": [ - { - "name": "details", - "type": "object", - "description": "Contains details about the cookie that's been removed. If removal failed for any reason, this will be \"null\", and $(ref:runtime.lastError) will be set.", - "optional": true, - "properties": { - "url": {"type": "string", "description": "The URL associated with the cookie that's been removed."}, - "name": {"type": "string", "description": "The name of the cookie that's been removed."}, - "storeId": {"type": "string", "description": "The ID of the cookie store from which the cookie was removed."} - } - } - ] - } - ] - }, - { - "name": "getAllCookieStores", - "type": "function", - "description": "Lists all existing cookie stores.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "cookieStores", "type": "array", "items": {"$ref": "CookieStore"}, "description": "All the existing cookie stores." - } - ] - } - ] - } - ], - "events": [ - { - "name": "onChanged", - "type": "function", - "description": "Fired when a cookie is set or removed. As a special case, note that updating a cookie's properties is implemented as a two step process: the cookie to be updated is first removed entirely, generating a notification with \"cause\" of \"overwrite\" . Afterwards, a new cookie is written with the updated values, generating a second notification with \"cause\" \"explicit\".", - "parameters": [ - { - "type": "object", - "name": "changeInfo", - "properties": { - "removed": {"type": "boolean", "description": "True if a cookie was removed."}, - "cookie": {"$ref": "Cookie", "description": "Information about the cookie that was set or removed."}, - "cause": {"$ref": "OnChangedCause", "description": "The underlying reason behind the cookie's change."} - } - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/downloads.json b/toolkit/components/webextensions/schemas/downloads.json deleted file mode 100644 index dcd43e4e1..000000000 --- a/toolkit/components/webextensions/schemas/downloads.json +++ /dev/null @@ -1,793 +0,0 @@ -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [{ - "type": "string", - "enum": [ - "downloads", - "downloads.open", - "downloads.shelf" - ] - }] - } - ] - }, - { - "namespace": "downloads", - "permissions": ["downloads"], - "types": [ - { - "id": "FilenameConflictAction", - "type": "string", - "enum": [ - "uniquify", - "overwrite", - "prompt" - ] - }, - { - "id": "InterruptReason", - "type": "string", - "enum": [ - "FILE_FAILED", - "FILE_ACCESS_DENIED", - "FILE_NO_SPACE", - "FILE_NAME_TOO_LONG", - "FILE_TOO_LARGE", - "FILE_VIRUS_INFECTED", - "FILE_TRANSIENT_ERROR", - "FILE_BLOCKED", - "FILE_SECURITY_CHECK_FAILED", - "FILE_TOO_SHORT", - "NETWORK_FAILED", - "NETWORK_TIMEOUT", - "NETWORK_DISCONNECTED", - "NETWORK_SERVER_DOWN", - "NETWORK_INVALID_REQUEST", - "SERVER_FAILED", - "SERVER_NO_RANGE", - "SERVER_BAD_CONTENT", - "SERVER_UNAUTHORIZED", - "SERVER_CERT_PROBLEM", - "SERVER_FORBIDDEN", - "USER_CANCELED", - "USER_SHUTDOWN", - "CRASH" - ] - }, - { - "id": "DangerType", - "type": "string", - "enum": [ - "file", - "url", - "content", - "uncommon", - "host", - "unwanted", - "safe", - "accepted" - ], - "description": "<dl><dt>file</dt><dd>The download's filename is suspicious.</dd><dt>url</dt><dd>The download's URL is known to be malicious.</dd><dt>content</dt><dd>The downloaded file is known to be malicious.</dd><dt>uncommon</dt><dd>The download's URL is not commonly downloaded and could be dangerous.</dd><dt>safe</dt><dd>The download presents no known danger to the user's computer.</dd></dl>These string constants will never change, however the set of DangerTypes may change." - }, - { - "id": "State", - "type": "string", - "enum": [ - "in_progress", - "interrupted", - "complete" - ], - "description": "<dl><dt>in_progress</dt><dd>The download is currently receiving data from the server.</dd><dt>interrupted</dt><dd>An error broke the connection with the file host.</dd><dt>complete</dt><dd>The download completed successfully.</dd></dl>These string constants will never change, however the set of States may change." - }, - { - "id": "DownloadItem", - "type": "object", - "properties": { - "id": { - "description": "An identifier that is persistent across browser sessions.", - "type": "integer" - }, - "url": { - "description": "Absolute URL.", - "type": "string" - }, - "referrer": { - "type": "string" - }, - "filename": { - "description": "Absolute local path.", - "type": "string" - }, - "incognito": { - "description": "False if this download is recorded in the history, true if it is not recorded.", - "type": "boolean" - }, - "danger": { - "$ref": "DangerType", - "description": "Indication of whether this download is thought to be safe or known to be suspicious." - }, - "mime": { - "description": "The file's MIME type.", - "type": "string" - }, - "startTime": { - "description": "Number of milliseconds between the unix epoch and when this download began.", - "type": "string" - }, - "endTime": { - "description": "Number of milliseconds between the unix epoch and when this download ended.", - "optional": true, - "type": "string" - }, - "estimatedEndTime": { - "type": "string", - "optional": true - }, - "state": { - "$ref": "State", - "description": "Indicates whether the download is progressing, interrupted, or complete." - }, - "paused": { - "description": "True if the download has stopped reading data from the host, but kept the connection open.", - "type": "boolean" - }, - "canResume": { - "type": "boolean" - }, - "error": { - "description": "Number indicating why a download was interrupted.", - "optional": true, - "$ref": "InterruptReason" - }, - "bytesReceived": { - "description": "Number of bytes received so far from the host, without considering file compression.", - "type": "number" - }, - "totalBytes": { - "description": "Number of bytes in the whole file, without considering file compression, or -1 if unknown.", - "type": "number" - }, - "fileSize": { - "description": "Number of bytes in the whole file post-decompression, or -1 if unknown.", - "type": "number" - }, - "exists": { - "type": "boolean" - }, - "byExtensionId": { - "type": "string", - "optional": true - }, - "byExtensionName": { - "type": "string", - "optional": true - } - } - }, - { - "id": "StringDelta", - "type": "object", - "properties": { - "current": { - "optional": true, - "type": "string" - }, - "previous": { - "optional": true, - "type": "string" - } - } - }, - { - "id": "DoubleDelta", - "type": "object", - "properties": { - "current": { - "optional": true, - "type": "number" - }, - "previous": { - "optional": true, - "type": "number" - } - } - }, - { - "id": "BooleanDelta", - "type": "object", - "properties": { - "current": { - "optional": true, - "type": "boolean" - }, - "previous": { - "optional": true, - "type": "boolean" - } - } - }, - { - "id": "DownloadTime", - "description": "A time specified as a Date object, a number or string representing milliseconds since the epoch, or an ISO 8601 string", - "choices": [ - { - "type": "string", - "pattern": "^[1-9]\\d*$" - }, - { - "$ref": "extensionTypes.Date" - } - ] - }, - { - "id": "DownloadQuery", - "description": "Parameters that combine to specify a predicate that can be used to select a set of downloads. Used for example in search() and erase()", - "type": "object", - "properties": { - "query": { - "description": "This array of search terms limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>filename</code> or <code>url</code> contain all of the search terms that do not begin with a dash '-' and none of the search terms that do begin with a dash.", - "optional": true, - "type": "array", - "items": { "type": "string" } - }, - "startedBefore": { - "description": "Limits results to downloads that started before the given ms since the epoch.", - "optional": true, - "$ref": "DownloadTime" - }, - "startedAfter": { - "description": "Limits results to downloads that started after the given ms since the epoch.", - "optional": true, - "$ref": "DownloadTime" - }, - "endedBefore": { - "description": "Limits results to downloads that ended before the given ms since the epoch.", - "optional": true, - "$ref": "DownloadTime" - }, - "endedAfter": { - "description": "Limits results to downloads that ended after the given ms since the epoch.", - "optional": true, - "$ref": "DownloadTime" - }, - "totalBytesGreater": { - "description": "Limits results to downloads whose totalBytes is greater than the given integer.", - "optional": true, - "type": "number" - }, - "totalBytesLess": { - "description": "Limits results to downloads whose totalBytes is less than the given integer.", - "optional": true, - "type": "number" - }, - "filenameRegex": { - "description": "Limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>filename</code> matches the given regular expression.", - "optional": true, - "type": "string" - }, - "urlRegex": { - "description": "Limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>url</code> matches the given regular expression.", - "optional": true, - "type": "string" - }, - "limit": { - "description": "Setting this integer limits the number of results. Otherwise, all matching <a href='#type-DownloadItem'>DownloadItems</a> will be returned.", - "optional": true, - "type": "integer" - }, - "orderBy": { - "description": "Setting elements of this array to <a href='#type-DownloadItem'>DownloadItem</a> properties in order to sort the search results. For example, setting <code>orderBy='startTime'</code> sorts the <a href='#type-DownloadItem'>DownloadItems</a> by their start time in ascending order. To specify descending order, prefix <code>orderBy</code> with a hyphen: '-startTime'.", - "optional": true, - "type": "array", - "items": { "type": "string" } - }, - "id": { - "type": "integer", - "optional": true - }, - "url": { - "description": "Absolute URL.", - "optional": true, - "type": "string" - }, - "filename": { - "description": "Absolute local path.", - "optional": true, - "type": "string" - }, - "danger": { - "$ref": "DangerType", - "description": "Indication of whether this download is thought to be safe or known to be suspicious.", - "optional": true - }, - "mime": { - "description": "The file's MIME type.", - "optional": true, - "type": "string" - }, - "startTime": { - "optional": true, - "type": "string" - }, - "endTime": { - "optional": true, - "type": "string" - }, - "state": { - "$ref": "State", - "description": "Indicates whether the download is progressing, interrupted, or complete.", - "optional": true - }, - "paused": { - "description": "True if the download has stopped reading data from the host, but kept the connection open.", - "optional": true, - "type": "boolean" - }, - "error": { - "description": "Why a download was interrupted.", - "optional": true, - "$ref": "InterruptReason" - }, - "bytesReceived": { - "description": "Number of bytes received so far from the host, without considering file compression.", - "optional": true, - "type": "number" - }, - "totalBytes": { - "description": "Number of bytes in the whole file, without considering file compression, or -1 if unknown.", - "optional": true, - "type": "number" - }, - "fileSize": { - "description": "Number of bytes in the whole file post-decompression, or -1 if unknown.", - "optional": true, - "type": "number" - }, - "exists": { - "type": "boolean", - "optional": true - } - } - } - ], - "functions": [ - { - "name": "download", - "type": "function", - "async": "callback", - "description": "Download a URL. If the URL uses the HTTP[S] protocol, then the request will include all cookies currently set for its hostname. If both <code>filename</code> and <code>saveAs</code> are specified, then the Save As dialog will be displayed, pre-populated with the specified <code>filename</code>. If the download started successfully, <code>callback</code> will be called with the new <a href='#type-DownloadItem'>DownloadItem</a>'s <code>downloadId</code>. If there was an error starting the download, then <code>callback</code> will be called with <code>downloadId=undefined</code> and <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain a descriptive string. The error strings are not guaranteed to remain backwards compatible between releases. You must not parse it.", - "parameters": [ - { - "description": "What to download and how.", - "name": "options", - "type": "object", - "properties": { - "url": { - "description": "The URL to download.", - "type": "string", - "format": "url" - }, - "filename": { - "description": "A file path relative to the Downloads directory to contain the downloaded file.", - "optional": true, - "type": "string" - }, - "conflictAction": { - "$ref": "FilenameConflictAction", - "optional": true - }, - "saveAs": { - "description": "Use a file-chooser to allow the user to select a filename.", - "optional": true, - "type": "boolean" - }, - "method": { - "description": "The HTTP method to use if the URL uses the HTTP[S] protocol.", - "enum": [ - "GET", - "POST" - ], - "optional": true, - "type": "string" - }, - "headers": { - "optional": true, - "type": "array", - "description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.", - "items": { - "type": "object", - "properties": { - "name": { - "description": "Name of the HTTP header.", - "type": "string" - }, - "value": { - "description": "Value of the HTTP header.", - "type": "string" - } - } - } - }, - "body": { - "description": "Post body.", - "optional": true, - "type": "string" - } - } - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [ - { - "name": "downloadId", - "type": "integer" - } - ] - } - ] - }, - { - "name": "search", - "type": "function", - "async": "callback", - "description": "Find <a href='#type-DownloadItem'>DownloadItems</a>. Set <code>query</code> to the empty object to get all <a href='#type-DownloadItem'>DownloadItems</a>. To get a specific <a href='#type-DownloadItem'>DownloadItem</a>, set only the <code>id</code> field.", - "parameters": [ - { - "name": "query", - "$ref": "DownloadQuery" - }, - { - "name": "callback", - "type": "function", - "parameters": [ - { - "items": { - "$ref": "DownloadItem" - }, - "name": "results", - "type": "array" - } - ] - } - ] - }, - { - "name": "pause", - "type": "function", - "async": "callback", - "description": "Pause the download. If the request was successful the download is in a paused state. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.", - "parameters": [ - { - "description": "The id of the download to pause.", - "name": "downloadId", - "type": "integer" - }, - { - "name": "callback", - "optional": true, - "parameters": [], - "type": "function" - } - ] - }, - { - "name": "resume", - "type": "function", - "async": "callback", - "description": "Resume a paused download. If the request was successful the download is in progress and unpaused. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.", - "parameters": [ - { - "description": "The id of the download to resume.", - "name": "downloadId", - "type": "integer" - }, - { - "name": "callback", - "optional": true, - "parameters": [], - "type": "function" - } - ] - }, - { - "name": "cancel", - "type": "function", - "async": "callback", - "description": "Cancel a download. When <code>callback</code> is run, the download is cancelled, completed, interrupted or doesn't exist anymore.", - "parameters": [ - { - "description": "The id of the download to cancel.", - "name": "downloadId", - "type": "integer" - }, - { - "name": "callback", - "optional": true, - "parameters": [], - "type": "function" - } - ] - }, - { - "name": "getFileIcon", - "type": "function", - "async": "callback", - "description": "Retrieve an icon for the specified download. For new downloads, file icons are available after the <a href='#event-onCreated'>onCreated</a> event has been received. The image returned by this function while a download is in progress may be different from the image returned after the download is complete. Icon retrieval is done by querying the underlying operating system or toolkit depending on the platform. The icon that is returned will therefore depend on a number of factors including state of the download, platform, registered file types and visual theme. If a file icon cannot be determined, <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain an error message.", - "parameters": [ - { - "description": "The identifier for the download.", - "name": "downloadId", - "type": "integer" - }, - { - "name": "options", - "optional": true, - "properties": { - "size": { - "description": "The size of the icon. The returned icon will be square with dimensions size * size pixels. The default size for the icon is 32x32 pixels.", - "optional": true, - "minimum": 1, - "maximum": 127, - "type": "integer" - } - }, - "type": "object" - }, - { - "name": "callback", - "parameters": [ - { - "name": "iconURL", - "optional": true, - "type": "string" - } - ], - "type": "function" - } - ] - }, - { - "name": "open", - "type": "function", - "async": "callback", - "description": "Open the downloaded file.", - "permissions": ["downloads.open"], - "parameters": [ - { - "name": "downloadId", - "type": "integer" - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [] - } - ] - }, - { - "name": "show", - "type": "function", - "description": "Show the downloaded file in its folder in a file manager.", - "async": "callback", - "parameters": [ - { - "name": "downloadId", - "type": "integer" - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [ - { - "name": "success", - "type": "boolean" - } - ] - } - ] - }, - { - "name": "showDefaultFolder", - "type": "function", - "parameters": [] - }, - { - "name": "erase", - "type": "function", - "async": "callback", - "description": "Erase matching <a href='#type-DownloadItem'>DownloadItems</a> from history", - "parameters": [ - { - "name": "query", - "$ref": "DownloadQuery" - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [ - { - "items": { - "type": "integer" - }, - "name": "erasedIds", - "type": "array" - } - ] - } - ] - }, - { - "name": "removeFile", - "async": "callback", - "type": "function", - "parameters": [ - { - "name": "downloadId", - "type": "integer" - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [ ] - } - ] - }, - { - "description": "Prompt the user to either accept or cancel a dangerous download. <code>acceptDanger()</code> does not automatically accept dangerous downloads.", - "name": "acceptDanger", - "unsupported": true, - "parameters": [ - { - "name": "downloadId", - "type": "integer" - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [ ] - } - ], - "type": "function" - }, - { - "description": "Initiate dragging the file to another application.", - "name": "drag", - "unsupported": true, - "parameters": [ - { - "name": "downloadId", - "type": "integer" - } - ], - "type": "function" - }, - { - "name": "setShelfEnabled", - "type": "function", - "unsupported": true, - "parameters": [ - { - "name": "enabled", - "type": "boolean" - } - ] - } - ], - "events": [ - { - "description": "This event fires with the <a href='#type-DownloadItem'>DownloadItem</a> object when a download begins.", - "name": "onCreated", - "parameters": [ - { - "$ref": "DownloadItem", - "name": "downloadItem" - } - ], - "type": "function" - }, - { - "description": "Fires with the <code>downloadId</code> when a download is erased from history.", - "name": "onErased", - "parameters": [ - { - "name": "downloadId", - "description": "The <code>id</code> of the <a href='#type-DownloadItem'>DownloadItem</a> that was erased.", - "type": "integer" - } - ], - "type": "function" - }, - { - "name": "onChanged", - "description": "When any of a <a href='#type-DownloadItem'>DownloadItem</a>'s properties except <code>bytesReceived</code> changes, this event fires with the <code>downloadId</code> and an object containing the properties that changed.", - "parameters": [ - { - "name": "downloadDelta", - "type": "object", - "properties": { - "id": { - "description": "The <code>id</code> of the <a href='#type-DownloadItem'>DownloadItem</a> that changed.", - "type": "integer" - }, - "url": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>url</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "filename": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>filename</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "danger": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>danger</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "mime": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>mime</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "startTime": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>startTime</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "endTime": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>endTime</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "state": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>state</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "canResume": { - "optional": true, - "$ref": "BooleanDelta" - }, - "paused": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>paused</code>.", - "optional": true, - "$ref": "BooleanDelta" - }, - "error": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>error</code>.", - "optional": true, - "$ref": "StringDelta" - }, - "totalBytes": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>totalBytes</code>.", - "optional": true, - "$ref": "DoubleDelta" - }, - "fileSize": { - "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>fileSize</code>.", - "optional": true, - "$ref": "DoubleDelta" - }, - "exists": { - "optional": true, - "$ref": "BooleanDelta" - } - } - } - ], - "type": "function" - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/events.json b/toolkit/components/webextensions/schemas/events.json deleted file mode 100644 index ea3cbb5d2..000000000 --- a/toolkit/components/webextensions/schemas/events.json +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "events", - "description": "The <code>chrome.events</code> namespace contains common types used by APIs dispatching events to notify you when something interesting happens.", - "types": [ - { - "id": "Rule", - "type": "object", - "description": "Description of a declarative rule for handling events.", - "properties": { - "id": { - "type": "string", - "optional": true, - "description": "Optional identifier that allows referencing this rule." - }, - "tags": { - "type": "array", - "items": {"type": "string"}, - "optional": true, - "description": "Tags can be used to annotate rules and perform operations on sets of rules." - }, - "conditions": { - "type": "array", - "items": {"type": "any"}, - "description": "List of conditions that can trigger the actions." - }, - "actions": { - "type": "array", - "items": {"type": "any"}, - "description": "List of actions that are triggered if one of the condtions is fulfilled." - }, - "priority": { - "type": "integer", - "optional": true, - "description": "Optional priority of this rule. Defaults to 100." - } - } - }, - { - "id": "Event", - "type": "object", - "description": "An object which allows the addition and removal of listeners for a Chrome event.", - "functions": [ - { - "name": "addListener", - "type": "function", - "description": "Registers an event listener <em>callback</em> to an event.", - "parameters": [ - { - "name": "callback", - "type": "function", - "description": "Called when an event occurs. The parameters of this function depend on the type of event." - } - ] - }, - { - "name": "removeListener", - "type": "function", - "description": "Deregisters an event listener <em>callback</em> from an event.", - "parameters": [ - { - "name": "callback", - "type": "function", - "description": "Listener that shall be unregistered." - } - ] - }, - { - "name": "hasListener", - "type": "function", - "parameters": [ - { - "name": "callback", - "type": "function", - "description": "Listener whose registration status shall be tested." - } - ], - "returns": { - "type": "boolean", - "description": "True if <em>callback</em> is registered to the event." - } - }, - { - "name": "hasListeners", - "type": "function", - "parameters": [], - "returns": { - "type": "boolean", - "description": "True if any event listeners are registered to the event." - } - }, - { - "name": "addRules", - "unsupported": true, - "type": "function", - "description": "Registers rules to handle events.", - "parameters": [ - { - "name": "eventName", - "type": "string", - "description": "Name of the event this function affects." - }, - { - "name": "webViewInstanceId", - "type": "integer", - "description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call." - }, - { - "name": "rules", - "type": "array", - "items": {"$ref": "Rule"}, - "description": "Rules to be registered. These do not replace previously registered rules." - }, - { - "name": "callback", - "optional": true, - "type": "function", - "parameters": [ - { - "name": "rules", - "type": "array", - "items": {"$ref": "Rule"}, - "description": "Rules that were registered, the optional parameters are filled with values." - } - ], - "description": "Called with registered rules." - } - ] - }, - { - "name": "getRules", - "unsupported": true, - "type": "function", - "description": "Returns currently registered rules.", - "parameters": [ - { - "name": "eventName", - "type": "string", - "description": "Name of the event this function affects." - }, - { - "name": "webViewInstanceId", - "type": "integer", - "description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call." - }, - { - "name": "ruleIdentifiers", - "optional": true, - "type": "array", - "items": {"type": "string"}, - "description": "If an array is passed, only rules with identifiers contained in this array are returned." - }, - { - "name": "callback", - "type": "function", - "parameters": [ - { - "name": "rules", - "type": "array", - "items": {"$ref": "Rule"}, - "description": "Rules that were registered, the optional parameters are filled with values." - } - ], - "description": "Called with registered rules." - } - ] - }, - { - "name": "removeRules", - "unsupported": true, - "type": "function", - "description": "Unregisters currently registered rules.", - "parameters": [ - { - "name": "eventName", - "type": "string", - "description": "Name of the event this function affects." - }, - { - "name": "webViewInstanceId", - "type": "integer", - "description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call." - }, - { - "name": "ruleIdentifiers", - "optional": true, - "type": "array", - "items": {"type": "string"}, - "description": "If an array is passed, only rules with identifiers contained in this array are unregistered." - }, - { - "name": "callback", - "optional": true, - "type": "function", - "parameters": [], - "description": "Called when rules were unregistered." - } - ] - } - ] - }, - { - "id": "UrlFilter", - "type": "object", - "description": "Filters URLs for various criteria. See <a href='events#filtered'>event filtering</a>. All criteria are case sensitive.", - "properties": { - "hostContains": { - "type": "string", - "description": "Matches if the host name of the URL contains a specified string. To test whether a host name component has a prefix 'foo', use hostContains: '.foo'. This matches 'www.foobar.com' and 'foo.com', because an implicit dot is added at the beginning of the host name. Similarly, hostContains can be used to match against component suffix ('foo.') and to exactly match against components ('.foo.'). Suffix- and exact-matching for the last components need to be done separately using hostSuffix, because no implicit dot is added at the end of the host name.", - "optional": true - }, - "hostEquals": { - "type": "string", - "description": "Matches if the host name of the URL is equal to a specified string.", - "optional": true - }, - "hostPrefix": { - "type": "string", - "description": "Matches if the host name of the URL starts with a specified string.", - "optional": true - }, - "hostSuffix": { - "type": "string", - "description": "Matches if the host name of the URL ends with a specified string.", - "optional": true - }, - "pathContains": { - "type": "string", - "description": "Matches if the path segment of the URL contains a specified string.", - "optional": true - }, - "pathEquals": { - "type": "string", - "description": "Matches if the path segment of the URL is equal to a specified string.", - "optional": true - }, - "pathPrefix": { - "type": "string", - "description": "Matches if the path segment of the URL starts with a specified string.", - "optional": true - }, - "pathSuffix": { - "type": "string", - "description": "Matches if the path segment of the URL ends with a specified string.", - "optional": true - }, - "queryContains": { - "type": "string", - "description": "Matches if the query segment of the URL contains a specified string.", - "optional": true - }, - "queryEquals": { - "type": "string", - "description": "Matches if the query segment of the URL is equal to a specified string.", - "optional": true - }, - "queryPrefix": { - "type": "string", - "description": "Matches if the query segment of the URL starts with a specified string.", - "optional": true - }, - "querySuffix": { - "type": "string", - "description": "Matches if the query segment of the URL ends with a specified string.", - "optional": true - }, - "urlContains": { - "type": "string", - "description": "Matches if the URL (without fragment identifier) contains a specified string. Port numbers are stripped from the URL if they match the default port number.", - "optional": true - }, - "urlEquals": { - "type": "string", - "description": "Matches if the URL (without fragment identifier) is equal to a specified string. Port numbers are stripped from the URL if they match the default port number.", - "optional": true - }, - "urlMatches": { - "type": "string", - "description": "Matches if the URL (without fragment identifier) matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the <a href=\"https://github.com/google/re2/blob/master/doc/syntax.txt\">RE2 syntax</a>.", - "optional": true - }, - "originAndPathMatches": { - "type": "string", - "description": "Matches if the URL without query segment and fragment identifier matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the <a href=\"https://github.com/google/re2/blob/master/doc/syntax.txt\">RE2 syntax</a>.", - "optional": true - }, - "urlPrefix": { - "type": "string", - "description": "Matches if the URL (without fragment identifier) starts with a specified string. Port numbers are stripped from the URL if they match the default port number.", - "optional": true - }, - "urlSuffix": { - "type": "string", - "description": "Matches if the URL (without fragment identifier) ends with a specified string. Port numbers are stripped from the URL if they match the default port number.", - "optional": true - }, - "schemes": { - "type": "array", - "description": "Matches if the scheme of the URL is equal to any of the schemes specified in the array.", - "optional": true, - "items": { "type": "string" } - }, - "ports": { - "type": "array", - "description": "Matches if the port of the URL is contained in any of the specified port lists. For example <code>[80, 443, [1000, 1200]]</code> matches all requests on port 80, 443 and in the range 1000-1200.", - "optional": true, - "items": { - "choices": [ - {"type": "integer", "description": "A specific port."}, - {"type": "array", "minItems": 2, "maxItems": 2, "items": {"type": "integer"}, "description": "A pair of integers identiying the start and end (both inclusive) of a port range."} - ] - } - } - } - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/experiments.json b/toolkit/components/webextensions/schemas/experiments.json deleted file mode 100644 index c687173a9..000000000 --- a/toolkit/components/webextensions/schemas/experiments.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [ - { - "type": "string", - "pattern": "^experiments(\\.\\w+)+$" - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/extension.json b/toolkit/components/webextensions/schemas/extension.json deleted file mode 100644 index 5a1b6c935..000000000 --- a/toolkit/components/webextensions/schemas/extension.json +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "extension", - "allowedContexts": ["content"], - "description": "The <code>browser.extension</code> API has utilities that can be used by any extension page. It includes support for exchanging messages between an extension and its content scripts or between extensions, as described in detail in $(topic:messaging)[Message Passing].", - "properties": { - "lastError": { - "type": "object", - "optional": true, - "allowedContexts": ["content"], - "description": "Set for the lifetime of a callback if an ansychronous extension api has resulted in an error. If no error has occured lastError will be <var>undefined</var>.", - "properties": { - "message": { "type": "string", "description": "Description of the error that has taken place." } - }, - "additionalProperties": { - "type": "any" - } - }, - "inIncognitoContext": { - "type": "boolean", - "optional": true, - "allowedContexts": ["content"], - "description": "True for content scripts running inside incognito tabs, and for extension pages running inside an incognito process. The latter only applies to extensions with 'split' incognito_behavior." - } - }, - "types": [ - { - "id": "ViewType", - "type": "string", - "enum": ["tab", "notification", "popup"], - "description": "The type of extension view." - } - ], - "functions": [ - { - "name": "getURL", - "type": "function", - "allowedContexts": ["content"], - "description": "Converts a relative path within an extension install directory to a fully-qualified URL.", - "parameters": [ - { - "type": "string", - "name": "path", - "description": "A path to a resource within an extension expressed relative to its install directory." - } - ], - "returns": { - "type": "string", - "description": "The fully-qualified URL to the resource." - } - }, - { - "name": "getViews", - "type": "function", - "description": "Returns an array of the JavaScript 'window' objects for each of the pages running inside the current extension.", - "parameters": [ - { - "type": "object", - "name": "fetchProperties", - "optional": true, - "properties": { - "type": { - "$ref": "ViewType", - "optional": true, - "description": "The type of view to get. If omitted, returns all views (including background pages and tabs). Valid values: 'tab', 'notification', 'popup'." - }, - "windowId": { - "type": "integer", - "optional": true, - "description": "The window to restrict the search to. If omitted, returns all views." - } - } - } - ], - "returns": { - "type": "array", - "description": "Array of global objects", - "items": { - "name": "viewGlobals", - "type": "object", - "isInstanceOf": "Window", - "additionalProperties": { "type": "any" } - } - } - }, - { - "name": "getBackgroundPage", - "type": "function", - "description": "Returns the JavaScript 'window' object for the background page running inside the current extension. Returns null if the extension has no background page.", - "parameters": [], - "returns": { - "type": "object", - "optional": true, - "name": "backgroundPageGlobal", - "isInstanceOf": "Window", - "additionalProperties": { "type": "any" } - } - }, - { - "name": "isAllowedIncognitoAccess", - "type": "function", - "description": "Retrieves the state of the extension's access to Incognito-mode (as determined by the user-controlled 'Allowed in Incognito' checkbox.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "isAllowedAccess", - "type": "boolean", - "description": "True if the extension has access to Incognito mode, false otherwise." - } - ] - } - ] - }, - { - "name": "isAllowedFileSchemeAccess", - "type": "function", - "description": "Retrieves the state of the extension's access to the 'file://' scheme (as determined by the user-controlled 'Allow access to File URLs' checkbox.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "isAllowedAccess", - "type": "boolean", - "description": "True if the extension can access the 'file://' scheme, false otherwise." - } - ] - } - ] - }, - { - "name": "setUpdateUrlData", - "unsupported": true, - "type": "function", - "description": "Sets the value of the ap CGI parameter used in the extension's update URL. This value is ignored for extensions that are hosted in the browser vendor's store.", - "parameters": [ - {"type": "string", "name": "data", "maxLength": 1024} - ] - } - ], - "events": [ - { - "name": "onRequest", - "unsupported": true, - "deprecated": "Please use $(ref:runtime.onMessage).", - "type": "function", - "description": "Fired when a request is sent from either an extension process or a content script.", - "parameters": [ - {"name": "request", "type": "any", "optional": true, "description": "The request sent by the calling script."}, - {"name": "sender", "$ref": "runtime.MessageSender" }, - {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object, or undefined if there is no response. If you have more than one <code>onRequest</code> listener in the same document, then only one may send a response." } - ] - }, - { - "name": "onRequestExternal", - "unsupported": true, - "deprecated": "Please use $(ref:runtime.onMessageExternal).", - "type": "function", - "description": "Fired when a request is sent from another extension.", - "parameters": [ - {"name": "request", "type": "any", "optional": true, "description": "The request sent by the calling script."}, - {"name": "sender", "$ref": "runtime.MessageSender" }, - {"name": "sendResponse", "type": "function", "description": "Function to call when you have a response. The argument should be any JSON-ifiable object, or undefined if there is no response." } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/extension_types.json b/toolkit/components/webextensions/schemas/extension_types.json deleted file mode 100644 index 1a88e4e60..000000000 --- a/toolkit/components/webextensions/schemas/extension_types.json +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "extensionTypes", - "description": "The <code>browser.extensionTypes</code> API contains type declarations for WebExtensions.", - "types": [ - { - "id": "ImageFormat", - "type": "string", - "enum": ["jpeg", "png"], - "description": "The format of an image." - }, - { - "id": "ImageDetails", - "type": "object", - "description": "Details about the format and quality of an image.", - "properties": { - "format": { - "$ref": "ImageFormat", - "optional": true, - "description": "The format of the resulting image. Default is <code>\"jpeg\"</code>." - }, - "quality": { - "type": "integer", - "optional": true, - "minimum": 0, - "maximum": 100, - "description": "When format is <code>\"jpeg\"</code>, controls the quality of the resulting image. This value is ignored for PNG images. As quality is decreased, the resulting image will have more visual artifacts, and the number of bytes needed to store it will decrease." - } - } - }, - { - "id": "RunAt", - "type": "string", - "enum": ["document_start", "document_end", "document_idle"], - "description": "The soonest that the JavaScript or CSS will be injected into the tab." - }, - { - "id": "InjectDetails", - "type": "object", - "description": "Details of the script or CSS to inject. Either the code or the file property must be set, but both may not be set at the same time.", - "properties": { - "code": {"type": "string", "optional": true, "description": "JavaScript or CSS code to inject.<br><br><b>Warning:</b><br>Be careful using the <code>code</code> parameter. Incorrect use of it may open your extension to <a href=\"https://en.wikipedia.org/wiki/Cross-site_scripting\">cross site scripting</a> attacks."}, - "file": {"type": "string", "optional": true, "description": "JavaScript or CSS file to inject."}, - "allFrames": {"type": "boolean", "optional": true, "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."}, - "matchAboutBlank": {"type": "boolean", "optional": true, "description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."}, - "frameId": { - "type": "integer", - "minimum": 0, - "optional": true, - "description": "The ID of the frame to inject the script into. This may not be used in combination with <code>allFrames</code>." - }, - "runAt": { - "$ref": "RunAt", - "optional": true, - "description": "The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\"." - } - } - }, - { - "id": "Date", - "choices": [ - { - "type": "string", - "format": "date" - }, - { - "type": "integer", - "minimum": 0 - }, - { - "type": "object", - "isInstanceOf": "Date", - "additionalProperties": { "type": "any" } - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/i18n.json b/toolkit/components/webextensions/schemas/i18n.json deleted file mode 100644 index 12dc45dfc..000000000 --- a/toolkit/components/webextensions/schemas/i18n.json +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "WebExtensionManifest", - "properties": { - "default_locale": { - "type": "string", - "optional": "true" - } - } - } - ] - }, - { - "namespace": "i18n", - "allowedContexts": ["content"], - "defaultContexts": ["content"], - "description": "Use the <code>browser.i18n</code> infrastructure to implement internationalization across your whole app or extension.", - "types": [ - { - "id": "LanguageCode", - "type": "string", - "description": "An ISO language code such as <code>en</code> or <code>fr</code>. For a complete list of languages supported by this method, see <a href='http://src.chromium.org/viewvc/chrome/trunk/src/third_party/cld/languages/internal/languages.cc'>kLanguageInfoTable</a>. For an unknown language, <code>und</code> will be returned, which means that [percentage] of the text is unknown to CLD" - } - ], - "functions": [ - { - "name": "getAcceptLanguages", - "type": "function", - "description": "Gets the accept-languages of the browser. This is different from the locale used by the browser; to get the locale, use $(ref:i18n.getUILanguage).", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - {"name": "languages", "type": "array", "items": {"$ref": "LanguageCode"}, "description": "Array of LanguageCode"} - ] - } - ] - }, - { - "name": "getMessage", - "type": "function", - "description": "Gets the localized string for the specified message. If the message is missing, this method returns an empty string (''). If the format of the <code>getMessage()</code> call is wrong — for example, <em>messageName</em> is not a string or the <em>substitutions</em> array has more than 9 elements — this method returns <code>undefined</code>.", - "parameters": [ - { - "type": "string", - "name": "messageName", - "description": "The name of the message, as specified in the <code>$(topic:i18n-messages)[messages.json]</code> file." - }, - { - "type": "any", - "name": "substitutions", - "optional": true, - "description": "Substitution strings, if the message requires any." - } - ], - "returns": { - "type": "string", - "description": "Message localized for current locale." - } - }, - { - "name": "getUILanguage", - "type": "function", - "description": "Gets the browser UI language of the browser. This is different from $(ref:i18n.getAcceptLanguages) which returns the preferred user languages.", - "parameters": [], - "returns": { - "type": "string", - "description": "The browser UI language code such as en-US or fr-FR." - } - }, - { - "name": "detectLanguage", - "type": "function", - "description": "Detects the language of the provided text using CLD.", - "async": "callback", - "parameters": [ - { - "type": "string", - "name": "text", - "description": "User input string to be translated." - }, - { - "type": "function", - "name": "callback", - "parameters": [ - { - "type": "object", - "name": "result", - "description": "LanguageDetectionResult object that holds detected langugae reliability and array of DetectedLanguage", - "properties": { - "isReliable": { "type": "boolean", "description": "CLD detected language reliability" }, - "languages": - { - "type": "array", - "description": "array of detectedLanguage", - "items": - { - "type": "object", - "description": "DetectedLanguage object that holds detected ISO language code and its percentage in the input string", - "properties": - { - "language": - { - "$ref": "LanguageCode" - }, - "percentage": - { - "type": "integer", - "description": "The percentage of the detected language" - } - } - } - } - } - } - ] - } - ] - } - ], - "events": [] - } -] diff --git a/toolkit/components/webextensions/schemas/idle.json b/toolkit/components/webextensions/schemas/idle.json deleted file mode 100644 index e0b3b951e..000000000 --- a/toolkit/components/webextensions/schemas/idle.json +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "idle", - "description": "Use the <code>browser.idle</code> API to detect when the machine's idle state changes.", - "permissions": ["idle"], - "types": [ - { - "id": "IdleState", - "type": "string", - "enum": ["active", "idle"] - } - ], - "functions": [ - { - "name": "queryState", - "type": "function", - "description": "Returns \"idle\" if the user has not generated any input for a specified number of seconds, or \"active\" otherwise.", - "async": "callback", - "parameters": [ - { - "name": "detectionIntervalInSeconds", - "type": "integer", - "minimum": 15, - "description": "The system is considered idle if detectionIntervalInSeconds seconds have elapsed since the last user input detected." - }, - { - "name": "callback", - "type": "function", - "parameters": [ - { - "name": "newState", - "$ref": "IdleState" - } - ] - } - ] - }, - { - "name": "setDetectionInterval", - "type": "function", - "description": "Sets the interval, in seconds, used to determine when the system is in an idle state for onStateChanged events. The default interval is 60 seconds.", - "parameters": [ - { - "name": "intervalInSeconds", - "type": "integer", - "minimum": 15, - "description": "Threshold, in seconds, used to determine when the system is in an idle state." - } - ] - } - ], - "events": [ - { - "name": "onStateChanged", - "type": "function", - "description": "Fired when the system changes to an active or idle state. The event fires with \"idle\" if the the user has not generated any input for a specified number of seconds, and \"active\" when the user generates input on an idle system.", - "parameters": [ - { - "name": "newState", - "$ref": "IdleState" - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/jar.mn b/toolkit/components/webextensions/schemas/jar.mn deleted file mode 100644 index 0bdf35b0d..000000000 --- a/toolkit/components/webextensions/schemas/jar.mn +++ /dev/null @@ -1,25 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -toolkit.jar: -% content extensions %content/extensions/ - content/extensions/schemas/alarms.json - content/extensions/schemas/cookies.json - content/extensions/schemas/downloads.json - content/extensions/schemas/events.json - content/extensions/schemas/experiments.json - content/extensions/schemas/extension.json - content/extensions/schemas/extension_types.json - content/extensions/schemas/i18n.json - content/extensions/schemas/idle.json - content/extensions/schemas/management.json - content/extensions/schemas/manifest.json - content/extensions/schemas/native_host_manifest.json - content/extensions/schemas/notifications.json - content/extensions/schemas/runtime.json - content/extensions/schemas/storage.json - content/extensions/schemas/test.json - content/extensions/schemas/top_sites.json - content/extensions/schemas/web_navigation.json - content/extensions/schemas/web_request.json diff --git a/toolkit/components/webextensions/schemas/management.json b/toolkit/components/webextensions/schemas/management.json deleted file mode 100644 index 413ff1d0d..000000000 --- a/toolkit/components/webextensions/schemas/management.json +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [{ - "type": "string", - "enum": [ - "management" - ] - }] - } - ] - }, - { - "namespace":"management", - "description": "The <code>browser.management</code> API provides ways to manage the list of extensions that are installed and running.", - "types": [ - { - "id": "IconInfo", - "description": "Information about an icon belonging to an extension.", - "type": "object", - "properties": { - "size": { - "type": "integer", - "description": "A number representing the width and height of the icon. Likely values include (but are not limited to) 128, 48, 24, and 16." - }, - "url": { - "type": "string", - "description": "The URL for this icon image. To display a grayscale version of the icon (to indicate that an extension is disabled, for example), append <code>?grayscale=true</code> to the URL." - } - } - }, - { - "id": "ExtensionDisabledReason", - "description": "A reason the item is disabled.", - "type": "string", - "enum": ["unknown", "permissions_increase"] - }, - { - "id": "ExtensionType", - "description": "The type of this extension. Will always be 'extension'.", - "type": "string", - "enum": ["extension"] - }, - { - "id": "ExtensionInstallType", - "description": "How the extension was installed. One of<br><var>development</var>: The extension was loaded unpacked in developer mode,<br><var>normal</var>: The extension was installed normally via an .xpi file,<br><var>sideload</var>: The extension was installed by other software on the machine,<br><var>other</var>: The extension was installed by other means.", - "type": "string", - "enum": ["development", "normal", "sideload", "other"] - }, - { - "id": "ExtensionInfo", - "description": "Information about an installed extension.", - "type": "object", - "properties": { - "id": { - "description": "The extension's unique identifier.", - "type": "string" - }, - "name": { - "description": "The name of this extension.", - "type": "string" - }, - "shortName": { - "description": "A short version of the name of this extension.", - "type": "string" - }, - "description": { - "description": "The description of this extension.", - "type": "string" - }, - "version": { - "description": "The <a href='manifest/version'>version</a> of this extension.", - "type": "string" - }, - "versionName": { - "description": "The <a href='manifest/version#version_name'>version name</a> of this extension if the manifest specified one.", - "type": "string", - "optional": true - }, - "mayDisable": { - "description": "Whether this extension can be disabled or uninstalled by the user.", - "type": "boolean" - }, - "enabled": { - "description": "Whether it is currently enabled or disabled.", - "type": "boolean" - }, - "disabledReason": { - "description": "A reason the item is disabled.", - "$ref": "ExtensionDisabledReason", - "optional": true - }, - "type": { - "description": "The type of this extension. Will always return 'extension'.", - "$ref": "ExtensionType" - }, - "homepageUrl": { - "description": "The URL of the homepage of this extension.", - "type": "string", - "optional": true - }, - "updateUrl": { - "description": "The update URL of this extension.", - "type": "string", - "optional": true - }, - "optionsUrl": { - "description": "The url for the item's options page, if it has one.", - "type": "string" - }, - "icons": { - "description": "A list of icon information. Note that this just reflects what was declared in the manifest, and the actual image at that url may be larger or smaller than what was declared, so you might consider using explicit width and height attributes on img tags referencing these images. See the <a href='manifest/icons'>manifest documentation on icons</a> for more details.", - "type": "array", - "optional": true, - "items": { - "$ref": "IconInfo" - } - }, - "permissions": { - "description": "Returns a list of API based permissions.", - "type": "array", - "items" : { - "type": "string" - } - }, - "hostPermissions": { - "description": "Returns a list of host based permissions.", - "type": "array", - "items" : { - "type": "string" - } - }, - "installType": { - "description": "How the extension was installed.", - "$ref": "ExtensionInstallType" - } - } - } - ], - "functions": [ - { - "name": "getAll", - "type": "function", - "permissions": ["management"], - "unsupported": true, - "description": "Returns a list of information about installed extensions.", - "async": "callback", - "parameters": [ - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [ - { - "type": "array", - "name": "result", - "items": { - "$ref": "ExtensionInfo" - } - } - ] - } - ] - }, - { - "name": "get", - "type": "function", - "permissions": ["management"], - "unsupported": true, - "description": "Returns information about the installed extension that has the given ID.", - "async": "callback", - "parameters": [ - { - "name": "id", - "$ref": "manifest.ExtensionID", - "description": "The ID from an item of $(ref:management.ExtensionInfo)." - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [ - { - "name": "result", - "$ref": "ExtensionInfo" - } - ] - } - ] - }, - { - "name": "getSelf", - "type": "function", - "description": "Returns information about the calling extension. Note: This function can be used without requesting the 'management' permission in the manifest.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "optional": true, - "parameters": [ - { - "name": "result", - "$ref": "ExtensionInfo" - } - ] - } - ] - }, - { - "name": "uninstallSelf", - "type": "function", - "description": "Uninstalls the calling extension. Note: This function can be used without requesting the 'management' permission in the manifest.", - "async": "callback", - "parameters": [ - { - "type": "object", - "name": "options", - "optional": true, - "properties": { - "showConfirmDialog": { - "type": "boolean", - "optional": true, - "description": "Whether or not a confirm-uninstall dialog should prompt the user. Defaults to false." - }, - "dialogMessage": { - "type": "string", - "optional": true, - "description": "The message to display to a user when being asked to confirm removal of the extension." - } - } - }, - { - "name": "callback", - "type": "function", - "optional": true, - "parameters": [] - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/manifest.json b/toolkit/components/webextensions/schemas/manifest.json deleted file mode 100644 index 09e6b56fb..000000000 --- a/toolkit/components/webextensions/schemas/manifest.json +++ /dev/null @@ -1,377 +0,0 @@ -[ - { - "namespace": "manifest", - "types": [ - { - "id": "WebExtensionManifest", - "type": "object", - "description": "Represents a WebExtension manifest.json file", - "properties": { - "manifest_version": { - "type": "integer", - "minimum": 2, - "maximum": 2 - }, - - "minimum_chrome_version":{ - "type": "string", - "optional": true - }, - - "applications": { - "type": "object", - "optional": true, - "properties": { - "gecko": { - "$ref": "FirefoxSpecificProperties", - "optional": true - } - } - }, - - "browser_specific_settings": { - "type": "object", - "optional": true, - "properties": { - "gecko": { - "$ref": "FirefoxSpecificProperties", - "optional": true - } - } - }, - - "name": { - "type": "string", - "optional": false, - "preprocess": "localize" - }, - - "short_name": { - "type": "string", - "optional": true, - "preprocess": "localize" - }, - - "description": { - "type": "string", - "optional": true, - "preprocess": "localize" - }, - - "author": { - "type": "string", - "optional": true, - "preprocess": "localize", - "onError": "warn" - }, - - "version": { - "type": "string", - "optional": false - }, - - "homepage_url": { - "type": "string", - "format": "url", - "optional": true, - "preprocess": "localize" - }, - - "icons": { - "type": "object", - "optional": true, - "patternProperties": { - "^[1-9]\\d*$": { "type": "string" } - } - }, - - "incognito": { - "type": "string", - "enum": ["spanning"], - "optional": true, - "onError": "warn" - }, - - "background": { - "choices": [ - { - "type": "object", - "properties": { - "page": { "$ref": "ExtensionURL" }, - "persistent": { - "optional": true, - "$ref": "PersistentBackgroundProperty" - } - }, - "additionalProperties": { "$ref": "UnrecognizedProperty" } - }, - { - "type": "object", - "properties": { - "scripts": { - "type": "array", - "items": { "$ref": "ExtensionURL" } - }, - "persistent": { - "optional": true, - "$ref": "PersistentBackgroundProperty" - } - }, - "additionalProperties": { "$ref": "UnrecognizedProperty" } - } - ], - "optional": true - }, - - "options_ui": { - "type": "object", - - "optional": true, - - "properties": { - "page": { "$ref": "ExtensionURL" }, - "browser_style": { - "type": "boolean", - "optional": true - }, - "chrome_style": { - "type": "boolean", - "optional": true - }, - "open_in_tab": { - "type": "boolean", - "optional": true - } - }, - - "additionalProperties": { - "type": "any", - "deprecated": "An unexpected property was found in the WebExtension manifest" - } - }, - - "content_scripts": { - "type": "array", - "optional": true, - "items": { "$ref": "ContentScript" } - }, - - "content_security_policy": { - "type": "string", - "optional": true, - "format": "contentSecurityPolicy", - "onError": "warn" - }, - - "permissions": { - "type": "array", - "items": { - "choices": [ - { "$ref": "Permission" }, - { - "type": "string", - "deprecated": "Unknown permission ${value}" - } - ] - }, - "optional": true - }, - - "web_accessible_resources": { - "type": "array", - "items": { "type": "string" }, - "optional": true - }, - - "developer": { - "type": "object", - "optional": true, - "properties": { - "name": { - "type": "string", - "optional": true, - "preprocess": "localize" - }, - "url": { - "type": "string", - "optional": true, - "preprocess": "localize" - } - } - } - - }, - - "additionalProperties": { "$ref": "UnrecognizedProperty" } - }, - { - "id": "Permission", - "choices": [ - { - "type": "string", - "enum": [ - "alarms", - "clipboardWrite", - "idle", - "notifications", - "storage" - ] - }, - { "$ref": "MatchPattern" } - ] - }, - { - "id": "ExtensionURL", - "type": "string", - "format": "strictRelativeUrl" - }, - { - "id": "ExtensionID", - "choices": [ - { - "type": "string", - "pattern": "(?i)^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$" - }, - { - "type": "string", - "pattern": "(?i)^[a-z0-9-._]*@[a-z0-9-._]+$" - } - ] - }, - { - "id": "FirefoxSpecificProperties", - "type": "object", - "properties": { - "id": { - "$ref": "ExtensionID", - "optional": true - }, - - "update_url": { - "type": "string", - "format": "url", - "optional": true - }, - - "strict_min_version": { - "type": "string", - "optional": true - }, - - "strict_max_version": { - "type": "string", - "optional": true - } - } - }, - { - "id": "MatchPattern", - "choices": [ - { - "type": "string", - "enum": ["<all_urls>"] - }, - { - "type": "string", - "pattern": "^(https?|file|ftp|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+)/.*$" - }, - { - "type": "string", - "pattern": "^file:///.*$" - } - ] - }, - { - "id": "ContentScript", - "type": "object", - "description": "Details of the script or CSS to inject. Either the code or the file property must be set, but both may not be set at the same time. Based on InjectDetails, but using underscore rather than camel case naming conventions.", - "additionalProperties": { "$ref": "UnrecognizedProperty" }, - "properties": { - "matches": { - "type": "array", - "optional": false, - "minItems": 1, - "items": { "$ref": "MatchPattern" } - }, - "exclude_matches": { - "type": "array", - "optional": true, - "minItems": 1, - "items": { "$ref": "MatchPattern" } - }, - "include_globs": { - "type": "array", - "optional": true, - "items": { "type": "string" } - }, - "exclude_globs": { - "type": "array", - "optional": true, - "items": { "type": "string" } - }, - "css": { - "type": "array", - "optional": true, - "description": "The list of CSS files to inject", - "items": { "$ref": "ExtensionURL" } - }, - "js": { - "type": "array", - "optional": true, - "description": "The list of CSS files to inject", - "items": { "$ref": "ExtensionURL" } - }, - "all_frames": {"type": "boolean", "optional": true, "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."}, - "match_about_blank": {"type": "boolean", "optional": true, "description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."}, - "run_at": { - "$ref": "extensionTypes.RunAt", - "optional": true, - "description": "The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\"." - } - } - }, - { - "id": "IconPath", - "choices": [ - { - "type": "object", - "patternProperties": { - "^[1-9]\\d*$": { "$ref": "ExtensionURL" } - }, - "additionalProperties": false - }, - { "$ref": "ExtensionURL" } - ] - }, - { - "id": "IconImageData", - "choices": [ - { - "type": "object", - "patternProperties": { - "^[1-9]\\d*$": { "$ref": "ImageData" } - }, - "additionalProperties": false - }, - { "$ref": "ImageData" } - ] - }, - { - "id": "ImageData", - "type": "object", - "isInstanceOf": "ImageData", - "postprocess": "convertImageDataToURL" - }, - { - "id": "UnrecognizedProperty", - "type": "any", - "deprecated": "An unexpected property was found in the WebExtension manifest." - }, - { - "id": "PersistentBackgroundProperty", - "type": "boolean", - "deprecated": "Event pages are not currently supported. This will run as a persistent background page." - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/moz.build b/toolkit/components/webextensions/schemas/moz.build deleted file mode 100644 index aac3a838c..000000000 --- a/toolkit/components/webextensions/schemas/moz.build +++ /dev/null @@ -1,7 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -JAR_MANIFESTS += ['jar.mn'] diff --git a/toolkit/components/webextensions/schemas/native_host_manifest.json b/toolkit/components/webextensions/schemas/native_host_manifest.json deleted file mode 100644 index 4ad2ea7f1..000000000 --- a/toolkit/components/webextensions/schemas/native_host_manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "namespace": "manifest", - "types": [ - { - "id": "NativeHostManifest", - "type": "object", - "description": "Represents a native host manifest file", - "properties": { - "name": { - "type": "string", - "pattern": "^\\w+(\\.\\w+)*$" - }, - "description": { - "type": "string" - }, - "path": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "stdio" - ] - }, - "allowed_extensions": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "manifest.ExtensionID" - } - } - } - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/notifications.json b/toolkit/components/webextensions/schemas/notifications.json deleted file mode 100644 index 12878e8c8..000000000 --- a/toolkit/components/webextensions/schemas/notifications.json +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "notifications", - "permissions": ["notifications"], - "types": [ - { - "id": "TemplateType", - "type": "string", - "enum": [ - "basic", - "image", - "list", - "progress" - ] - }, - { - "id": "PermissionLevel", - "type": "string", - "enum": [ - "granted", - "denied" - ] - }, - { - "id": "NotificationItem", - "type": "object", - "properties": { - "title": { - "description": "Title of one item of a list notification.", - "type": "string" - }, - "message": { - "description": "Additional details about this item.", - "type": "string" - } - } - }, - { - "id": "CreateNotificationOptions", - "type": "object", - "properties": { - "type": { - "description": "Which type of notification to display.", - "$ref": "TemplateType" - }, - "iconUrl": { - "optional": true, - "description": "A URL to the sender's avatar, app icon, or a thumbnail for image notifications.", - "type": "string" - }, - "appIconMaskUrl": { - "optional": true, - "description": "A URL to the app icon mask.", - "type": "string" - }, - "title": { - "description": "Title of the notification (e.g. sender name for email).", - "type": "string" - }, - "message": { - "description": "Main notification content.", - "type": "string" - }, - "contextMessage": { - "optional": true, - "description": "Alternate notification content with a lower-weight font.", - "type": "string" - }, - "priority": { - "optional": true, - "description": "Priority ranges from -2 to 2. -2 is lowest priority. 2 is highest. Zero is default.", - "type": "integer", - "minimum": -2, - "maximum": 2 - }, - "eventTime": { - "optional": true, - "description": "A timestamp associated with the notification, in milliseconds past the epoch.", - "type": "number" - }, - "buttons": { - "unsupported": true, - "optional": true, - "description": "Text and icons for up to two notification action buttons.", - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "iconUrl": { - "optional": true, - "type": "string" - } - } - } - }, - "imageUrl": { - "optional": true, - "description": "A URL to the image thumbnail for image-type notifications.", - "type": "string" - }, - "items": { - "optional": true, - "description": "Items for multi-item notifications.", - "type": "array", - "items": { "$ref": "NotificationItem" } - }, - "progress": { - "optional": true, - "description": "Current progress ranges from 0 to 100.", - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "isClickable": { - "optional": true, - "description": "Whether to show UI indicating that the app will visibly respond to clicks on the body of a notification.", - "type": "boolean" - } - } - }, - { - "id": "UpdateNotificationOptions", - "type": "object", - "properties": { - "type": { - "optional": true, - "description": "Which type of notification to display.", - "$ref": "TemplateType" - }, - "iconUrl": { - "optional": true, - "description": "A URL to the sender's avatar, app icon, or a thumbnail for image notifications.", - "type": "string" - }, - "appIconMaskUrl": { - "optional": true, - "description": "A URL to the app icon mask.", - "type": "string" - }, - "title": { - "optional": true, - "description": "Title of the notification (e.g. sender name for email).", - "type": "string" - }, - "message": { - "optional": true, - "description": "Main notification content.", - "type": "string" - }, - "contextMessage": { - "optional": true, - "description": "Alternate notification content with a lower-weight font.", - "type": "string" - }, - "priority": { - "optional": true, - "description": "Priority ranges from -2 to 2. -2 is lowest priority. 2 is highest. Zero is default.", - "type": "integer", - "minimum": -2, - "maximum": 2 - }, - "eventTime": { - "optional": true, - "description": "A timestamp associated with the notification, in milliseconds past the epoch.", - "type": "number" - }, - "buttons": { - "unsupported": true, - "optional": true, - "description": "Text and icons for up to two notification action buttons.", - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "iconUrl": { - "optional": true, - "type": "string" - } - } - } - }, - "imageUrl": { - "optional": true, - "description": "A URL to the image thumbnail for image-type notifications.", - "type": "string" - }, - "items": { - "optional": true, - "description": "Items for multi-item notifications.", - "type": "array", - "items": { "$ref": "NotificationItem" } - }, - "progress": { - "optional": true, - "description": "Current progress ranges from 0 to 100.", - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "isClickable": { - "optional": true, - "description": "Whether to show UI indicating that the app will visibly respond to clicks on the body of a notification.", - "type": "boolean" - } - } - } - ], - "functions": [ - { - "name": "create", - "type": "function", - "description": "Creates and displays a notification.", - "async": "callback", - "parameters": [ - { - "optional": true, - "type": "string", - "name": "notificationId", - "description": "Identifier of the notification. If it is empty, this method generates an id. If it matches an existing notification, this method first clears that notification before proceeding with the create operation." - }, - { - "$ref": "CreateNotificationOptions", - "name": "options", - "description": "Contents of the notification." - }, - { - "optional": true, - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "notificationId", - "type": "string", - "description": "The notification id (either supplied or generated) that represents the created notification." - } - ] - } - ] - }, - { - "name": "update", - "unsupported": true, - "type": "function", - "description": "Updates an existing notification.", - "async": "callback", - "parameters": [ - { - "type": "string", - "name": "notificationId", - "description": "The id of the notification to be updated." - }, - { - "$ref": "UpdateNotificationOptions", - "name": "options", - "description": "Contents of the notification to update to." - }, - { - "optional": true, - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "wasUpdated", - "type": "boolean", - "description": "Indicates whether a matching notification existed." - } - ] - } - ] - }, - { - "name": "clear", - "type": "function", - "description": "Clears an existing notification.", - "async": "callback", - "parameters": [ - { - "type": "string", - "name": "notificationId", - "description": "The id of the notification to be updated." - }, - { - "optional": true, - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "wasCleared", - "type": "boolean", - "description": "Indicates whether a matching notification existed." - } - ] - } - ] - }, - { - "name": "getAll", - "type": "function", - "description": "Retrieves all the notifications.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "notifications", - "type": "object", - "description": "The set of notifications currently in the system." - } - ] - } - ] - }, - { - "name": "getPermissionLevel", - "unsupported": true, - "type": "function", - "description": "Retrieves whether the user has enabled notifications from this app or extension.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "level", - "$ref": "PermissionLevel", - "description": "The current permission level." - } - ] - } - ] - } - ], - "events": [ - { - "name": "onClosed", - "type": "function", - "description": "Fired when the notification closed, either by the system or by user action.", - "parameters": [ - { - "type": "string", - "name": "notificationId", - "description": "The notificationId of the closed notification." - }, - { - "type": "boolean", - "name": "byUser", - "description": "True if the notification was closed by the user." - } - ] - }, - { - "name": "onClicked", - "type": "function", - "description": "Fired when the user clicked in a non-button area of the notification.", - "parameters": [ - { - "type": "string", - "name": "notificationId", - "description": "The notificationId of the clicked notification." - } - ] - }, - { - "name": "onButtonClicked", - "type": "function", - "description": "Fired when the user pressed a button in the notification.", - "parameters": [ - { - "type": "string", - "name": "notificationId", - "description": "The notificationId of the clicked notification." - }, - { - "type": "number", - "name": "buttonIndex", - "description": "The index of the button clicked by the user." - } - ] - }, - { - "name": "onPermissionLevelChanged", - "unsupported": true, - "type": "function", - "description": "Fired when the user changes the permission level.", - "parameters": [ - { - "$ref": "PermissionLevel", - "name": "level", - "description": "The new permission level." - } - ] - }, - { - "name": "onShowSettings", - "unsupported": true, - "type": "function", - "description": "Fired when the user clicked on a link for the app's notification settings.", - "parameters": [ - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/runtime.json b/toolkit/components/webextensions/schemas/runtime.json deleted file mode 100644 index 575df7d27..000000000 --- a/toolkit/components/webextensions/schemas/runtime.json +++ /dev/null @@ -1,590 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [{ - "type": "string", - "enum": [ - "nativeMessaging" - ] - }] - } - ] - }, - { - "namespace": "runtime", - "allowedContexts": ["content"], - "description": "Use the <code>browser.runtime</code> API to retrieve the background page, return details about the manifest, and listen for and respond to events in the app or extension lifecycle. You can also use this API to convert the relative path of URLs to fully-qualified URLs.", - "types": [ - { - "id": "Port", - "type": "object", - "allowedContexts": ["content"], - "description": "An object which allows two way communication with other pages.", - "properties": { - "name": {"type": "string"}, - "disconnect": { "type": "function" }, - "onDisconnect": { "$ref": "events.Event" }, - "onMessage": { "$ref": "events.Event" }, - "postMessage": {"type": "function"}, - "sender": { - "$ref": "MessageSender", - "optional": true, - "description": "This property will <b>only</b> be present on ports passed to onConnect/onConnectExternal listeners." - } - }, - "additionalProperties": { "type": "any"} - }, - { - "id": "MessageSender", - "type": "object", - "allowedContexts": ["content"], - "description": "An object containing information about the script context that sent a message or request.", - "properties": { - "tab": {"$ref": "tabs.Tab", "optional": true, "description": "The $(ref:tabs.Tab) which opened the connection, if any. This property will <strong>only</strong> be present when the connection was opened from a tab (including content scripts), and <strong>only</strong> if the receiver is an extension, not an app."}, - "frameId": {"type": "integer", "optional": true, "description": "The $(topic:frame_ids)[frame] that opened the connection. 0 for top-level frames, positive for child frames. This will only be set when <code>tab</code> is set."}, - "id": {"type": "string", "optional": true, "description": "The ID of the extension or app that opened the connection, if any."}, - "url": {"type": "string", "optional": true, "description": "The URL of the page or frame that opened the connection. If the sender is in an iframe, it will be iframe's URL not the URL of the page which hosts it."}, - "tlsChannelId": {"unsupported": true, "type": "string", "optional": true, "description": "The TLS channel ID of the page or frame that opened the connection, if requested by the extension or app, and if available."} - } - }, - { - "id": "PlatformOs", - "type": "string", - "allowedContexts": ["content"], - "description": "The operating system the browser is running on.", - "enum": ["mac", "win", "android", "cros", "linux", "openbsd"] - }, - { - "id": "PlatformArch", - "type": "string", - "enum": ["arm", "x86-32", "x86-64"], - "allowedContexts": ["content"], - "description": "The machine's processor architecture." - }, - { - "id": "PlatformInfo", - "type": "object", - "allowedContexts": ["content"], - "description": "An object containing information about the current platform.", - "properties": { - "os": { - "$ref": "PlatformOs", - "description": "The operating system the browser is running on." - }, - "arch": { - "$ref": "PlatformArch", - "description": "The machine's processor architecture." - }, - "nacl_arch" : { - "unsupported": true, - "description": "The native client architecture. This may be different from arch on some platforms.", - "$ref": "PlatformNaclArch" - } - } - }, - { - "id": "BrowserInfo", - "type": "object", - "description": "An object containing information about the current browser.", - "properties": { - "name": { - "type": "string", - "description": "The name of the browser, for example 'Firefox'." - }, - "vendor": { - "type": "string", - "description": "The name of the browser vendor, for example 'Mozilla'." - }, - "version": { - "type": "string", - "description": "The browser's version, for example '42.0.0' or '0.8.1pre'." - }, - "buildID": { - "type": "string", - "description": "The browser's build ID/date, for example '20160101'." - } - } - }, - { - "id": "RequestUpdateCheckStatus", - "type": "string", - "enum": ["throttled", "no_update", "update_available"], - "allowedContexts": ["content"], - "description": "Result of the update check." - }, - { - "id": "OnInstalledReason", - "type": "string", - "enum": ["install", "update", "browser_update"], - "allowedContexts": ["content"], - "description": "The reason that this event is being dispatched." - }, - { - "id": "OnRestartRequiredReason", - "type": "string", - "allowedContexts": ["content"], - "description": "The reason that the event is being dispatched. 'app_update' is used when the restart is needed because the application is updated to a newer version. 'os_update' is used when the restart is needed because the browser/OS is updated to a newer version. 'periodic' is used when the system runs for more than the permitted uptime set in the enterprise policy.", - "enum": ["app_update", "os_update", "periodic"] - } - ], - "properties": { - "lastError": { - "type": "object", - "optional": true, - "allowedContexts": ["content"], - "description": "This will be defined during an API method callback if there was an error", - "properties": { - "message": { - "optional": true, - "type": "string", - "description": "Details about the error which occurred." - } - }, - "additionalProperties": { - "type": "any" - } - }, - "id": { - "type": "string", - "allowedContexts": ["content"], - "description": "The ID of the extension/app." - } - }, - "functions": [ - { - "name": "getBackgroundPage", - "type": "function", - "description": "Retrieves the JavaScript 'window' object for the background page running inside the current extension/app. If the background page is an event page, the system will ensure it is loaded before calling the callback. If there is no background page, an error is set.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "backgroundPage", - "optional": true, - "type": "object", - "isInstanceOf": "Window", - "additionalProperties": { "type": "any" }, - "description": "The JavaScript 'window' object for the background page." - } - ] - } - ] - }, - { - "name": "openOptionsPage", - "type": "function", - "description": "<p>Open your Extension's options page, if possible.</p><p>The precise behavior may depend on your manifest's <code>$(topic:optionsV2)[options_ui]</code> or <code>$(topic:options)[options_page]</code> key, or what the browser happens to support at the time.</p><p>If your Extension does not declare an options page, or the browser failed to create one for some other reason, the callback will set $(ref:lastError).</p>", - "async": "callback", - "parameters": [{ - "type": "function", - "name": "callback", - "parameters": [], - "optional": true - }] - }, - { - "name": "getManifest", - "allowedContexts": ["content"], - "description": "Returns details about the app or extension from the manifest. The object returned is a serialization of the full $(topic:manifest)[manifest file].", - "type": "function", - "parameters": [], - "returns": { - "type": "object", - "properties": {}, - "additionalProperties": { "type": "any" }, - "description": "The manifest details." - } - }, - { - "name": "getURL", - "type": "function", - "allowedContexts": ["content"], - "description": "Converts a relative path within an app/extension install directory to a fully-qualified URL.", - "parameters": [ - { - "type": "string", - "name": "path", - "description": "A path to a resource within an app/extension expressed relative to its install directory." - } - ], - "returns": { - "type": "string", - "description": "The fully-qualified URL to the resource." - } - }, - { - "name": "setUninstallURL", - "type": "function", - "description": "Sets the URL to be visited upon uninstallation. This may be used to clean up server-side data, do analytics, and implement surveys. Maximum 255 characters.", - "async": "callback", - "parameters": [ - { - "type": "string", - "name": "url", - "maxLength": 255, - "description": "URL to be opened after the extension is uninstalled. This URL must have an http: or https: scheme. Set an empty string to not open a new tab upon uninstallation." - }, - { - "type": "function", - "name": "callback", - "optional": true, - "description": "Called when the uninstall URL is set. If the given URL is invalid, $(ref:runtime.lastError) will be set.", - "parameters": [] - } - ] - }, - { - "name": "reload", - "description": "Reloads the app or extension.", - "type": "function", - "parameters": [] - }, - { - "name": "requestUpdateCheck", - "unsupported": true, - "type": "function", - "description": "Requests an update check for this app/extension.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "status", - "$ref": "RequestUpdateCheckStatus", - "description": "Result of the update check." - }, - { - "name": "details", - "type": "object", - "optional": true, - "properties": { - "version": { - "type": "string", - "description": "The version of the available update." - } - }, - "description": "If an update is available, this contains more information about the available update." - } - ] - } - ] - }, - { - "name": "restart", - "unsupported": true, - "description": "Restart the device when the app runs in kiosk mode. Otherwise, it's no-op.", - "type": "function", - "parameters": [] - }, - { - "name": "connect", - "type": "function", - "allowedContexts": ["content"], - "description": "Attempts to connect to connect listeners within an extension/app (such as the background page), or other extensions/apps. This is useful for content scripts connecting to their extension processes, inter-app/extension communication, and $(topic:manifest/externally_connectable)[web messaging]. Note that this does not connect to any listeners in a content script. Extensions may connect to content scripts embedded in tabs via $(ref:tabs.connect).", - "parameters": [ - {"type": "string", "name": "extensionId", "optional": true, "description": "The ID of the extension or app to connect to. If omitted, a connection will be attempted with your own extension. Required if sending messages from a web page for $(topic:manifest/externally_connectable)[web messaging]."}, - { - "type": "object", - "name": "connectInfo", - "properties": { - "name": { "type": "string", "optional": true, "description": "Will be passed into onConnect for processes that are listening for the connection event." }, - "includeTlsChannelId": { "type": "boolean", "optional": true, "description": "Whether the TLS channel ID will be passed into onConnectExternal for processes that are listening for the connection event." } - }, - "optional": true - } - ], - "returns": { - "$ref": "Port", - "description": "Port through which messages can be sent and received. The port's $(ref:runtime.Port onDisconnect) event is fired if the extension/app does not exist. " - } - }, - { - "name": "connectNative", - "type": "function", - "description": "Connects to a native application in the host machine.", - "permissions": ["nativeMessaging"], - "parameters": [ - { - "type": "string", - "name": "application", - "description": "The name of the registered application to connect to." - } - ], - "returns": { - "$ref": "Port", - "description": "Port through which messages can be sent and received with the application" - } - }, - { - "name": "sendMessage", - "type": "function", - "allowAmbiguousOptionalArguments": true, - "allowedContexts": ["content"], - "description": "Sends a single message to event listeners within your extension/app or a different extension/app. Similar to $(ref:runtime.connect) but only sends a single message, with an optional response. If sending to your extension, the $(ref:runtime.onMessage) event will be fired in each page, or $(ref:runtime.onMessageExternal), if a different extension. Note that extensions cannot send messages to content scripts using this method. To send messages to content scripts, use $(ref:tabs.sendMessage).", - "async": "responseCallback", - "parameters": [ - {"type": "string", "name": "extensionId", "optional": true, "description": "The ID of the extension/app to send the message to. If omitted, the message will be sent to your own extension/app. Required if sending messages from a web page for $(topic:manifest/externally_connectable)[web messaging]."}, - { "type": "any", "name": "message" }, - { - "type": "object", - "name": "options", - "properties": { - "includeTlsChannelId": { "type": "boolean", "optional": true, "description": "Whether the TLS channel ID will be passed into onMessageExternal for processes that are listening for the connection event." } - }, - "optional": true - }, - { - "type": "function", - "name": "responseCallback", - "optional": true, - "parameters": [ - { - "name": "response", - "type": "any", - "description": "The JSON response object sent by the handler of the message. If an error occurs while connecting to the extension, the callback will be called with no arguments and $(ref:runtime.lastError) will be set to the error message." - } - ] - } - ] - }, - { - "name": "sendNativeMessage", - "type": "function", - "description": "Send a single message to a native application.", - "permissions": ["nativeMessaging"], - "async": "responseCallback", - "parameters": [ - { - "name": "application", - "description": "The name of the native messaging host.", - "type": "string" - }, - { - "name": "message", - "description": "The message that will be passed to the native messaging host.", - "type": "any" - }, - { - "type": "function", - "name": "responseCallback", - "optional": true, - "parameters": [ - { - "name": "response", - "type": "any", - "description": "The response message sent by the native messaging host. If an error occurs while connecting to the native messaging host, the callback will be called with no arguments and $(ref:runtime.lastError) will be set to the error message." - } - ] - } - ] - }, - { - "name": "getBrowserInfo", - "type": "function", - "description": "Returns information about the current browser.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "description": "Called with results", - "parameters": [ - { - "name": "browserInfo", - "$ref": "BrowserInfo" - } - ] - } - ] - }, - { - "name": "getPlatformInfo", - "type": "function", - "description": "Returns information about the current platform.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "description": "Called with results", - "parameters": [ - { - "name": "platformInfo", - "$ref": "PlatformInfo" - } - ] - } - ] - }, - { - "name": "getPackageDirectoryEntry", - "unsupported": true, - "type": "function", - "description": "Returns a DirectoryEntry for the package directory.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "directoryEntry", - "type": "object", - "additionalProperties": { "type": "any" }, - "isInstanceOf": "DirectoryEntry" - } - ] - } - ] - } - ], - "events": [ - { - "name": "onStartup", - "type": "function", - "description": "Fired when a profile that has this extension installed first starts up. This event is not fired for incognito profiles." - }, - { - "name": "onInstalled", - "type": "function", - "description": "Fired when the extension is first installed, when the extension is updated to a new version, and when the browser is updated to a new version.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "reason": { - "$ref": "OnInstalledReason", - "description": "The reason that this event is being dispatched." - }, - "previousVersion": { - "type": "string", - "optional": true, - "unsupported": true, - "description": "Indicates the previous version of the extension, which has just been updated. This is present only if 'reason' is 'update'." - }, - "id": { - "type": "string", - "optional": true, - "unsupported": true, - "description": "Indicates the ID of the imported shared module extension which updated. This is present only if 'reason' is 'shared_module_update'." - } - } - } - ] - }, - { - "name": "onSuspend", - "unsupported": true, - "type": "function", - "description": "Sent to the event page just before it is unloaded. This gives the extension opportunity to do some clean up. Note that since the page is unloading, any asynchronous operations started while handling this event are not guaranteed to complete. If more activity for the event page occurs before it gets unloaded the onSuspendCanceled event will be sent and the page won't be unloaded. " - }, - { - "name": "onSuspendCanceled", - "unsupported": true, - "type": "function", - "description": "Sent after onSuspend to indicate that the app won't be unloaded after all." - }, - { - "name": "onUpdateAvailable", - "type": "function", - "description": "Fired when an update is available, but isn't installed immediately because the app is currently running. If you do nothing, the update will be installed the next time the background page gets unloaded, if you want it to be installed sooner you can explicitly call $(ref:runtime.reload). If your extension is using a persistent background page, the background page of course never gets unloaded, so unless you call $(ref:runtime.reload) manually in response to this event the update will not get installed until the next time the browser itself restarts. If no handlers are listening for this event, and your extension has a persistent background page, it behaves as if $(ref:runtime.reload) is called in response to this event.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "version": { - "type": "string", - "description": "The version number of the available update." - } - }, - "additionalProperties": { "type": "any" }, - "description": "The manifest details of the available update." - } - ] - }, - { - "name": "onBrowserUpdateAvailable", - "unsupported": true, - "type": "function", - "description": "Fired when an update for the browser is available, but isn't installed immediately because a browser restart is required.", - "deprecated": "Please use $(ref:runtime.onRestartRequired).", - "parameters": [] - }, - { - "name": "onConnect", - "type": "function", - "allowedContexts": ["content"], - "description": "Fired when a connection is made from either an extension process or a content script.", - "parameters": [ - {"$ref": "Port", "name": "port"} - ] - }, - { - "name": "onConnectExternal", - "type": "function", - "description": "Fired when a connection is made from another extension.", - "parameters": [ - {"$ref": "Port", "name": "port"} - ] - }, - { - "name": "onMessage", - "type": "function", - "allowedContexts": ["content"], - "description": "Fired when a message is sent from either an extension process or a content script.", - "parameters": [ - {"name": "message", "type": "any", "optional": true, "description": "The message sent by the calling script."}, - {"name": "sender", "$ref": "MessageSender" }, - {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)." } - ], - "returns": { - "type": "boolean", - "optional": true, - "description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns." - } - }, - { - "name": "onMessageExternal", - "type": "function", - "description": "Fired when a message is sent from another extension/app. Cannot be used in a content script.", - "parameters": [ - {"name": "message", "type": "any", "optional": true, "description": "The message sent by the calling script."}, - {"name": "sender", "$ref": "MessageSender" }, - {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)." } - ], - "returns": { - "type": "boolean", - "optional": true, - "description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns." - } - }, - { - "name": "onRestartRequired", - "unsupported": true, - "type": "function", - "description": "Fired when an app or the device that it runs on needs to be restarted. The app should close all its windows at its earliest convenient time to let the restart to happen. If the app does nothing, a restart will be enforced after a 24-hour grace period has passed. Currently, this event is only fired for Chrome OS kiosk apps.", - "parameters": [ - { - "$ref": "OnRestartRequiredReason", - "name": "reason", - "description": "The reason that the event is being dispatched." - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/storage.json b/toolkit/components/webextensions/schemas/storage.json deleted file mode 100644 index a54a20942..000000000 --- a/toolkit/components/webextensions/schemas/storage.json +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "storage", - "allowedContexts": ["content"], - "defaultContexts": ["content"], - "description": "Use the <code>browser.storage</code> API to store, retrieve, and track changes to user data.", - "permissions": ["storage"], - "types": [ - { - "id": "StorageChange", - "type": "object", - "properties": { - "oldValue": { - "type": "any", - "description": "The old value of the item, if there was an old value.", - "optional": true - }, - "newValue": { - "type": "any", - "description": "The new value of the item, if there is a new value.", - "optional": true - } - } - }, - { - "id": "StorageArea", - "type": "object", - "functions": [ - { - "name": "get", - "type": "function", - "description": "Gets one or more items from storage.", - "async": "callback", - "parameters": [ - { - "name": "keys", - "choices": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } }, - { - "type": "object", - "description": "Storage items to return in the callback, where the values are replaced with those from storage if they exist.", - "additionalProperties": { "type": "any" } - } - ], - "description": "A single key to get, list of keys to get, or a dictionary specifying default values (see description of the object). An empty list or object will return an empty result object. Pass in <code>null</code> to get the entire contents of storage.", - "optional": true - }, - { - "name": "callback", - "type": "function", - "description": "Callback with storage items, or on failure (in which case $(ref:runtime.lastError) will be set).", - "parameters": [ - { - "name": "items", - "type": "object", - "additionalProperties": { "type": "any" }, - "description": "Object with items in their key-value mappings." - } - ] - } - ] - }, - { - "name": "getBytesInUse", - "unsupported": true, - "type": "function", - "description": "Gets the amount of space (in bytes) being used by one or more items.", - "async": "callback", - "parameters": [ - { - "name": "keys", - "choices": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ], - "description": "A single key or list of keys to get the total usage for. An empty list will return 0. Pass in <code>null</code> to get the total usage of all of storage.", - "optional": true - }, - { - "name": "callback", - "type": "function", - "description": "Callback with the amount of space being used by storage, or on failure (in which case $(ref:runtime.lastError) will be set).", - "parameters": [ - { - "name": "bytesInUse", - "type": "integer", - "description": "Amount of space being used in storage, in bytes." - } - ] - } - ] - }, - { - "name": "set", - "type": "function", - "description": "Sets multiple items.", - "async": "callback", - "parameters": [ - { - "name": "items", - "type": "object", - "additionalProperties": { "type": "any" }, - "description": "<p>An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected.</p><p>Primitive values such as numbers will serialize as expected. Values with a <code>typeof</code> <code>\"object\"</code> and <code>\"function\"</code> will typically serialize to <code>{}</code>, with the exception of <code>Array</code> (serializes as expected), <code>Date</code>, and <code>Regex</code> (serialize using their <code>String</code> representation).</p>" - }, - { - "name": "callback", - "type": "function", - "description": "Callback on success, or on failure (in which case $(ref:runtime.lastError) will be set).", - "parameters": [], - "optional": true - } - ] - }, - { - "name": "remove", - "type": "function", - "description": "Removes one or more items from storage.", - "async": "callback", - "parameters": [ - { - "name": "keys", - "choices": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ], - "description": "A single key or a list of keys for items to remove." - }, - { - "name": "callback", - "type": "function", - "description": "Callback on success, or on failure (in which case $(ref:runtime.lastError) will be set).", - "parameters": [], - "optional": true - } - ] - }, - { - "name": "clear", - "type": "function", - "description": "Removes all items from storage.", - "async": "callback", - "parameters": [ - { - "name": "callback", - "type": "function", - "description": "Callback on success, or on failure (in which case $(ref:runtime.lastError) will be set).", - "parameters": [], - "optional": true - } - ] - } - ] - } - ], - "events": [ - { - "name": "onChanged", - "type": "function", - "description": "Fired when one or more items change.", - "parameters": [ - { - "name": "changes", - "type": "object", - "additionalProperties": { "$ref": "StorageChange" }, - "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item." - }, - { - "name": "areaName", - "type": "string", - "description": "The name of the storage area (<code>\"sync\"</code>, <code>\"local\"</code> or <code>\"managed\"</code>) the changes are for." - } - ] - } - ], - "properties": { - "sync": { - "$ref": "StorageArea", - "description": "Items in the <code>sync</code> storage area are synced by the browser.", - "properties": { - "QUOTA_BYTES": { - "value": 102400, - "description": "The maximum total amount (in bytes) of data that can be stored in sync storage, as measured by the JSON stringification of every value plus every key's length. Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError)." - }, - "QUOTA_BYTES_PER_ITEM": { - "value": 8192, - "description": "The maximum size (in bytes) of each individual item in sync storage, as measured by the JSON stringification of its value plus its key length. Updates containing items larger than this limit will fail immediately and set $(ref:runtime.lastError)." - }, - "MAX_ITEMS": { - "value": 512, - "description": "The maximum number of items that can be stored in sync storage. Updates that would cause this limit to be exceeded will fail immediately and set $(ref:runtime.lastError)." - }, - "MAX_WRITE_OPERATIONS_PER_HOUR": { - "value": 1800, - "description": "<p>The maximum number of <code>set</code>, <code>remove</code>, or <code>clear</code> operations that can be performed each hour. This is 1 every 2 seconds, a lower ceiling than the short term higher writes-per-minute limit.</p><p>Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError).</p>" - }, - "MAX_WRITE_OPERATIONS_PER_MINUTE": { - "value": 120, - "description": "<p>The maximum number of <code>set</code>, <code>remove</code>, or <code>clear</code> operations that can be performed each minute. This is 2 per second, providing higher throughput than writes-per-hour over a shorter period of time.</p><p>Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError).</p>" - }, - "MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE": { - "value": 1000000, - "deprecated": "The storage.sync API no longer has a sustained write operation quota.", - "description": "" - } - } - }, - "local": { - "$ref": "StorageArea", - "description": "Items in the <code>local</code> storage area are local to each machine.", - "properties": { - "QUOTA_BYTES": { - "value": 5242880, - "description": "The maximum amount (in bytes) of data that can be stored in local storage, as measured by the JSON stringification of every value plus every key's length. This value will be ignored if the extension has the <code>unlimitedStorage</code> permission. Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError)." - } - } - }, - "managed": { - "unsupported": true, - "$ref": "StorageArea", - "description": "Items in the <code>managed</code> storage area are set by the domain administrator, and are read-only for the extension; trying to modify this namespace results in an error." - } - } - } -] diff --git a/toolkit/components/webextensions/schemas/test.json b/toolkit/components/webextensions/schemas/test.json deleted file mode 100644 index 25a62a96b..000000000 --- a/toolkit/components/webextensions/schemas/test.json +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "test", - "allowedContexts": ["content"], - "defaultContexts": ["content"], - "description": "none", - "functions": [ - { - "name": "notifyFail", - "type": "function", - "description": "Notifies the browser process that test code running in the extension failed. This is only used for internal unit testing.", - "parameters": [ - {"type": "string", "name": "message"} - ] - }, - { - "name": "notifyPass", - "type": "function", - "description": "Notifies the browser process that test code running in the extension passed. This is only used for internal unit testing.", - "parameters": [ - {"type": "string", "name": "message", "optional": true} - ] - }, - { - "name": "log", - "type": "function", - "description": "Logs a message during internal unit testing.", - "parameters": [ - {"type": "string", "name": "message"} - ] - }, - { - "name": "sendMessage", - "type": "function", - "description": "Sends a string message to the browser process, generating a Notification that C++ test code can wait for.", - "allowAmbiguousOptionalArguments": true, - "parameters": [ - {"type": "any", "name": "arg1", "optional": true}, - {"type": "any", "name": "arg2", "optional": true} - ] - }, - { - "name": "fail", - "type": "function", - "parameters": [ - {"type": "any", "name": "message", "optional": true} - ] - }, - { - "name": "succeed", - "type": "function", - "parameters": [ - {"type": "any", "name": "message", "optional": true} - ] - }, - { - "name": "assertTrue", - "type": "function", - "allowAmbiguousOptionalArguments": true, - "parameters": [ - {"name": "test", "type": "any", "optional": true}, - {"type": "string", "name": "message", "optional": true} - ] - }, - { - "name": "assertFalse", - "type": "function", - "allowAmbiguousOptionalArguments": true, - "parameters": [ - {"name": "test", "type": "any", "optional": true}, - {"type": "string", "name": "message", "optional": true} - ] - }, - { - "name": "assertBool", - "type": "function", - "unsupported": true, - "parameters": [ - { - "name": "test", - "choices": [ - {"type": "string"}, - {"type": "boolean"} - ] - }, - {"type": "boolean", "name": "expected"}, - {"type": "string", "name": "message", "optional": true} - ] - }, - { - "name": "checkDeepEq", - "type": "function", - "unsupported": true, - "allowAmbiguousOptionalArguments": true, - "parameters": [ - {"type": "any", "name": "expected"}, - {"type": "any", "name": "actual"} - ] - }, - { - "name": "assertEq", - "type": "function", - "allowAmbiguousOptionalArguments": true, - "parameters": [ - {"type": "any", "name": "expected", "optional": true}, - {"type": "any", "name": "actual", "optional": true}, - {"type": "string", "name": "message", "optional": true} - ] - }, - { - "name": "assertNoLastError", - "type": "function", - "unsupported": true, - "parameters": [] - }, - { - "name": "assertLastError", - "type": "function", - "unsupported": true, - "parameters": [ - {"type": "string", "name": "expectedError"} - ] - }, - { - "name": "assertRejects", - "type": "function", - "async": true, - "parameters": [ - { - "name": "promise", - "$ref": "Promise" - }, - { - "name": "expectedError", - "$ref": "ExpectedError", - "optional": true - }, - { - "name": "message", - "type": "string", - "optional": true - } - ] - }, - { - "name": "assertThrows", - "type": "function", - "parameters": [ - { - "name": "func", - "type": "function" - }, - { - "name": "expectedError", - "$ref": "ExpectedError", - "optional": true - }, - { - "name": "message", - "type": "string", - "optional": true - } - ] - } - ], - "types": [ - { - "id": "ExpectedError", - "choices": [ - {"type": "string"}, - {"type": "object", "isInstanceOf": "RegExp", "additionalProperties": true}, - {"type": "function"} - ] - }, - { - "id": "Promise", - "choices": [ - { - "type": "object", - "properties": { - "then": {"type": "function"} - }, - "additionalProperties": true - }, - { - "type": "object", - "isInstanceOf": "Promise", - "additionalProperties": true - } - ] - } - ], - "events": [ - { - "name": "onMessage", - "type": "function", - "description": "Used to test sending messages to extensions.", - "parameters": [ - { - "type": "string", - "name": "message" - }, - { - "type": "any", - "name": "argument" - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/top_sites.json b/toolkit/components/webextensions/schemas/top_sites.json deleted file mode 100644 index fbfbc4b62..000000000 --- a/toolkit/components/webextensions/schemas/top_sites.json +++ /dev/null @@ -1,66 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [{ - "type": "string", - "enum": [ - "topSites" - ] - }] - } - ] - }, - { - "namespace": "topSites", - "description": "Use the chrome.topSites API to access the top sites that are displayed on the new tab page. ", - "permissions": ["topSites"], - "types": [ - { - "id": "MostVisitedURL", - "type": "object", - "description": "An object encapsulating a most visited URL, such as the URLs on the new tab page.", - "properties": { - "url": { - "type": "string", - "description": "The most visited URL." - }, - "title": { - "type": "string", - "optional": true, - "description": "The title of the page." - } - } - } - ], - "functions": [ - { - "name": "get", - "type": "function", - "description": "Gets a list of top sites.", - "async": "callback", - "parameters": [ - { - "name": "callback", - "type": "function", - "parameters": [ - { - "name": "results", - "type": "array", - "items": { - "$ref": "MostVisitedURL" - } - } - ] - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/web_navigation.json b/toolkit/components/webextensions/schemas/web_navigation.json deleted file mode 100644 index 1e13b181a..000000000 --- a/toolkit/components/webextensions/schemas/web_navigation.json +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [{ - "type": "string", - "enum": [ - "webNavigation" - ] - }] - } - ] - }, - { - "namespace": "webNavigation", - "description": "Use the <code>browser.webNavigation</code> API to receive notifications about the status of navigation requests in-flight.", - "permissions": ["webNavigation"], - "types": [ - { - "id": "TransitionType", - "type": "string", - "enum": ["link", "typed", "auto_bookmark", "auto_subframe", "manual_subframe", "generated", "start_page", "form_submit", "reload", "keyword", "keyword_generated"], - "description": "Cause of the navigation. The same transition types as defined in the history API are used. These are the same transition types as defined in the $(topic:transition_types)[history API] except with <code>\"start_page\"</code> in place of <code>\"auto_toplevel\"</code> (for backwards compatibility)." - }, - { - "id": "TransitionQualifier", - "type": "string", - "enum": ["client_redirect", "server_redirect", "forward_back", "from_address_bar"] - }, - { - "id": "EventUrlFilters", - "type": "object", - "properties": { - "url": { - "type": "array", - "minItems": 1, - "items": { "$ref": "events.UrlFilter" } - } - } - } - ], - "functions": [ - { - "name": "getFrame", - "type": "function", - "description": "Retrieves information about the given frame. A frame refers to an <iframe> or a <frame> of a web page and is identified by a tab ID and a frame ID.", - "async": "callback", - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Information about the frame to retrieve information about.", - "properties": { - "tabId": { "type": "integer", "minimum": 0, "description": "The ID of the tab in which the frame is." }, - "processId": {"optional": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": { "type": "integer", "minimum": 0, "description": "The ID of the frame in the given tab." } - } - }, - { - "type": "function", - "name": "callback", - "parameters": [ - { - "type": "object", - "name": "details", - "optional": true, - "description": "Information about the requested frame, null if the specified frame ID and/or tab ID are invalid.", - "properties": { - "errorOccurred": { - "unsupported": true, - "type": "boolean", - "description": "True if the last navigation in this frame was interrupted by an error, i.e. the onErrorOccurred event fired." - }, - "url": { - "type": "string", - "description": "The URL currently associated with this frame, if the frame identified by the frameId existed at one point in the given tab. The fact that an URL is associated with a given frameId does not imply that the corresponding frame still exists." - }, - "parentFrameId": { - "type": "integer", - "description": "ID of frame that wraps the frame. Set to -1 of no parent frame exists." - } - } - } - ] - } - ] - }, - { - "name": "getAllFrames", - "type": "function", - "description": "Retrieves information about all frames of a given tab.", - "async": "callback", - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Information about the tab to retrieve all frames from.", - "properties": { - "tabId": { "type": "integer", "minimum": 0, "description": "The ID of the tab." } - } - }, - { - "type": "function", - "name": "callback", - "parameters": [ - { - "name": "details", - "type": "array", - "description": "A list of frames in the given tab, null if the specified tab ID is invalid.", - "optional": true, - "items": { - "type": "object", - "properties": { - "errorOccurred": { - "unsupported": true, - "type": "boolean", - "description": "True if the last navigation in this frame was interrupted by an error, i.e. the onErrorOccurred event fired." - }, - "processId": { - "unsupported": true, - "type": "integer", - "description": "The ID of the process runs the renderer for this tab." - }, - "frameId": { - "type": "integer", - "description": "The ID of the frame. 0 indicates that this is the main frame; a positive value indicates the ID of a subframe." - }, - "parentFrameId": { - "type": "integer", - "description": "ID of frame that wraps the frame. Set to -1 of no parent frame exists." - }, - "url": { - "type": "string", - "description": "The URL currently associated with this frame." - } - } - } - } - ] - } - ] - } - ], - "events": [ - { - "name": "onBeforeNavigate", - "type": "function", - "description": "Fired when a navigation is about to occur.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation is about to occur."}, - "url": {"type": "string"}, - "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique for a given tab and process."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame. Set to -1 of no parent frame exists."}, - "timeStamp": {"type": "number", "description": "The time when the browser was about to start the navigation, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - }, - { - "name": "onCommitted", - "type": "function", - "description": "Fired when a navigation is committed. The document (and the resources it refers to, such as images and subframes) might still be downloading, but at least part of the document has been received from the server and the browser has decided to switch to the new document.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."}, - "url": {"type": "string"}, - "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."}, - "transitionType": {"unsupported": true, "$ref": "TransitionType", "description": "Cause of the navigation."}, - "transitionQualifiers": {"unsupported": true, "type": "array", "description": "A list of transition qualifiers.", "items": {"$ref": "TransitionQualifier"}}, - "timeStamp": {"type": "number", "description": "The time when the navigation was committed, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - }, - { - "name": "onDOMContentLoaded", - "type": "function", - "description": "Fired when the page's DOM is fully constructed, but the referenced resources may not finish loading.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."}, - "url": {"type": "string"}, - "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."}, - "timeStamp": {"type": "number", "description": "The time when the page's DOM was fully constructed, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - }, - { - "name": "onCompleted", - "type": "function", - "description": "Fired when a document, including the resources it refers to, is completely loaded and initialized.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."}, - "url": {"type": "string"}, - "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."}, - "timeStamp": {"type": "number", "description": "The time when the document finished loading, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - }, - { - "name": "onErrorOccurred", - "type": "function", - "description": "Fired when an error occurs and the navigation is aborted. This can happen if either a network error occurred, or the user aborted the navigation.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."}, - "url": {"type": "string"}, - "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."}, - "error": {"unsupported": true, "type": "string", "description": "The error description."}, - "timeStamp": {"type": "number", "description": "The time when the error occurred, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - }, - { - "name": "onCreatedNavigationTarget", - "unsupported": true, - "type": "function", - "description": "Fired when a new window, or a new tab in an existing window, is created to host a navigation.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "sourceTabId": {"type": "integer", "description": "The ID of the tab in which the navigation is triggered."}, - "sourceProcessId": {"type": "integer", "description": "The ID of the process runs the renderer for the source tab."}, - "sourceFrameId": {"type": "integer", "description": "The ID of the frame with sourceTabId in which the navigation is triggered. 0 indicates the main frame."}, - "url": {"type": "string", "description": "The URL to be opened in the new window."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the url is opened"}, - "timeStamp": {"type": "number", "description": "The time when the browser was about to create a new view, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - }, - { - "name": "onReferenceFragmentUpdated", - "type": "function", - "description": "Fired when the reference fragment of a frame was updated. All future events for that frame will use the updated URL.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."}, - "url": {"type": "string"}, - "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."}, - "transitionType": {"unsupported": true, "$ref": "TransitionType", "description": "Cause of the navigation."}, - "transitionQualifiers": {"unsupported": true, "type": "array", "description": "A list of transition qualifiers.", "items": {"$ref": "TransitionQualifier"}}, - "timeStamp": {"type": "number", "description": "The time when the navigation was committed, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - }, - { - "name": "onTabReplaced", - "type": "function", - "description": "Fired when the contents of the tab is replaced by a different (usually previously pre-rendered) tab.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "replacedTabId": {"type": "integer", "description": "The ID of the tab that was replaced."}, - "tabId": {"type": "integer", "description": "The ID of the tab that replaced the old tab."}, - "timeStamp": {"type": "number", "description": "The time when the replacement happened, in milliseconds since the epoch."} - } - } - ] - }, - { - "name": "onHistoryStateUpdated", - "type": "function", - "description": "Fired when the frame's history was updated to a new URL. All future events for that frame will use the updated URL.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."}, - "url": {"type": "string"}, - "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."}, - "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."}, - "transitionType": {"unsupported": true, "$ref": "TransitionType", "description": "Cause of the navigation."}, - "transitionQualifiers": {"unsupported": true, "type": "array", "description": "A list of transition qualifiers.", "items": {"$ref": "TransitionQualifier"}}, - "timeStamp": {"type": "number", "description": "The time when the navigation was committed, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "name": "filters", - "optional": true, - "$ref": "EventUrlFilters", - "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event." - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/schemas/web_request.json b/toolkit/components/webextensions/schemas/web_request.json deleted file mode 100644 index 4035aea6e..000000000 --- a/toolkit/components/webextensions/schemas/web_request.json +++ /dev/null @@ -1,616 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -[ - { - "namespace": "manifest", - "types": [ - { - "$extend": "Permission", - "choices": [{ - "type": "string", - "enum": [ - "webRequest", - "webRequestBlocking" - ] - }] - } - ] - }, - { - "namespace": "webRequest", - "description": "Use the <code>browser.webRequest</code> API to observe and analyze traffic and to intercept, block, or modify requests in-flight.", - "permissions": ["webRequest"], - "properties": { - "MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES": { - "value": 20, - "description": "The maximum number of times that <code>handlerBehaviorChanged</code> can be called per 10 minute sustained interval. <code>handlerBehaviorChanged</code> is an expensive function call that shouldn't be called often." - } - }, - "types": [ - { - "id": "ResourceType", - "type": "string", - "enum": [ - "main_frame", - "sub_frame", - "stylesheet", - "script", - "image", - "object", - "xmlhttprequest", - "xbl", - "xslt", - "ping", - "beacon", - "xml_dtd", - "font", - "media", - "websocket", - "csp_report", - "imageset", - "web_manifest", - "other" - ] - }, - { - "id": "OnBeforeRequestOptions", - "type": "string", - "enum": ["blocking", "requestBody"] - }, - { - "id": "OnBeforeSendHeadersOptions", - "type": "string", - "enum": ["requestHeaders", "blocking"] - }, - { - "id": "OnSendHeadersOptions", - "type": "string", - "enum": ["requestHeaders"] - }, - { - "id": "OnHeadersReceivedOptions", - "type": "string", - "enum": ["blocking", "responseHeaders"] - }, - { - "id": "OnAuthRequiredOptions", - "type": "string", - "enum": ["responseHeaders", "blocking", "asyncBlocking"] - }, - { - "id": "OnResponseStartedOptions", - "type": "string", - "enum": ["responseHeaders"] - }, - { - "id": "OnBeforeRedirectOptions", - "type": "string", - "enum": ["responseHeaders"] - }, - { - "id": "OnCompletedOptions", - "type": "string", - "enum": ["responseHeaders"] - }, - { - "id": "RequestFilter", - "type": "object", - "description": "An object describing filters to apply to webRequest events.", - "properties": { - "urls": { - "type": "array", - "description": "A list of URLs or URL patterns. Requests that cannot match any of the URLs will be filtered out.", - "items": { "type": "string" } - }, - "types": { - "type": "array", - "optional": true, - "description": "A list of request types. Requests that cannot match any of the types will be filtered out.", - "items": { "$ref": "ResourceType" } - }, - "tabId": { "type": "integer", "optional": true }, - "windowId": { "type": "integer", "optional": true } - } - }, - { - "id": "HttpHeaders", - "type": "array", - "description": "An array of HTTP headers. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>.", - "items": { - "type": "object", - "properties": { - "name": {"type": "string", "description": "Name of the HTTP header."}, - "value": {"type": "string", "optional": true, "description": "Value of the HTTP header if it can be represented by UTF-8."}, - "binaryValue": { - "type": "array", - "optional": true, - "description": "Value of the HTTP header if it cannot be represented by UTF-8, stored as individual byte values (0..255).", - "items": {"type": "integer"} - } - } - } - }, - { - "id": "BlockingResponse", - "type": "object", - "description": "Returns value for event handlers that have the 'blocking' extraInfoSpec applied. Allows the event handler to modify network requests.", - "properties": { - "cancel": { - "type": "boolean", - "optional": true, - "description": "If true, the request is cancelled. Used in onBeforeRequest, this prevents the request from being sent." - }, - "redirectUrl": { - "type": "string", - "optional": true, - "description": "Only used as a response to the onBeforeRequest and onHeadersReceived events. If set, the original request is prevented from being sent/completed and is instead redirected to the given URL. Redirections to non-HTTP schemes such as data: are allowed. Redirects initiated by a redirect action use the original request method for the redirect, with one exception: If the redirect is initiated at the onHeadersReceived stage, then the redirect will be issued using the GET method." - }, - "requestHeaders": { - "$ref": "HttpHeaders", - "optional": true, - "description": "Only used as a response to the onBeforeSendHeaders event. If set, the request is made with these request headers instead." - }, - "responseHeaders": { - "$ref": "HttpHeaders", - "optional": true, - "description": "Only used as a response to the onHeadersReceived event. If set, the server is assumed to have responded with these response headers instead. Only return <code>responseHeaders</code> if you really want to modify the headers in order to limit the number of conflicts (only one extension may modify <code>responseHeaders</code> for each request)." - }, - "authCredentials": { - "type": "object", - "description": "Only used as a response to the onAuthRequired event. If set, the request is made using the supplied credentials.", - "optional": true, - "properties": { - "username": {"type": "string"}, - "password": {"type": "string"} - } - } - } - }, - { - "id": "UploadData", - "type": "object", - "properties": { - "bytes": { - "type": "any", - "optional": true, - "description": "An ArrayBuffer with a copy of the data." - }, - "file": { - "type": "string", - "optional": true, - "description": "A string with the file's path and name." - } - }, - "description": "Contains data uploaded in a URL request." - } - ], - "functions": [ - { - "name": "handlerBehaviorChanged", - "type": "function", - "description": "Needs to be called when the behavior of the webRequest handlers has changed to prevent incorrect handling due to caching. This function call is expensive. Don't call it often.", - "async": "callback", - "parameters": [ - { - "type": "function", - "name": "callback", - "optional": true, - "parameters": [] - } - ] - } - ], - "events": [ - { - "name": "onBeforeRequest", - "type": "function", - "description": "Fired when a request is about to occur.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "requestBody": { - "type": "object", - "optional": true, - "description": "Contains the HTTP request body data. Only provided if extraInfoSpec contains 'requestBody'.", - "properties": { - "error": {"type": "string", "optional": true, "description": "Errors when obtaining request body data."}, - "formData": { - "type": "object", - "optional": true, - "description": "If the request method is POST and the body is a sequence of key-value pairs encoded in UTF8, encoded as either multipart/form-data, or application/x-www-form-urlencoded, this dictionary is present and for each key contains the list of all values for that key. If the data is of another media type, or if it is malformed, the dictionary is not present. An example value of this dictionary is {'key': ['value1', 'value2']}.", - "properties": {}, - "additionalProperties": { - "type": "array", - "items": { "type": "string" } - } - }, - "raw" : { - "type": "array", - "optional": true, - "items": {"$ref": "UploadData"}, - "description": "If the request method is PUT or POST, and the body is not already parsed in formData, then the unparsed request body elements are contained in this array." - } - } - }, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnBeforeRequestOptions" - } - } - ], - "returns": { - "$ref": "BlockingResponse", - "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.", - "optional": true - } - }, - { - "name": "onBeforeSendHeaders", - "type": "function", - "description": "Fired before sending an HTTP request, once the request headers are available. This may occur after a TCP connection is made to the server, but before any HTTP data is sent. ", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that are going to be sent out with this request."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnBeforeSendHeadersOptions" - } - } - ], - "returns": { - "$ref": "BlockingResponse", - "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.", - "optional": true - } - }, - { - "name": "onSendHeaders", - "type": "function", - "description": "Fired just before a request is going to be sent to the server (modifications of previous onBeforeSendHeaders callbacks are visible by the time onSendHeaders is fired).", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that have been sent out with this request."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnSendHeadersOptions" - } - } - ] - }, - { - "name": "onHeadersReceived", - "type": "function", - "description": "Fired when HTTP response headers of a request have been received.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line)."}, - "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that have been received with this response."}, - "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnHeadersReceivedOptions" - } - } - ], - "returns": { - "$ref": "BlockingResponse", - "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.", - "optional": true - } - }, - { - "name": "onAuthRequired", - "unsupported": true, - "type": "function", - "description": "Fired when an authentication failure is received. The listener has three options: it can provide authentication credentials, it can cancel the request and display the error page, or it can take no action on the challenge. If bad user credentials are provided, this may be called multiple times for the same request.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "scheme": {"type": "string", "description": "The authentication scheme, e.g. Basic or Digest."}, - "realm": {"type": "string", "description": "The authentication realm provided by the server, if there is one.", "optional": true}, - "challenger": {"type": "object", "description": "The server requesting authentication.", "properties": {"host": {"type": "string"}, "port": {"type": "integer"}}}, - "isProxy": {"type": "boolean", "description": "True for Proxy-Authenticate, false for WWW-Authenticate."}, - "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this response."}, - "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."}, - "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."} - } - }, - { - "type": "function", - "optional": true, - "name": "callback", - "parameters": [ - {"name": "response", "$ref": "BlockingResponse"} - ] - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnAuthRequiredOptions" - } - } - ], - "returns": { - "$ref": "BlockingResponse", - "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.", - "optional": true - } - }, - { - "name": "onResponseStarted", - "type": "function", - "description": "Fired when the first byte of the response body is received. For HTTP requests, this means that the status line and response headers are available.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."}, - "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."}, - "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}, - "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this response."}, - "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnResponseStartedOptions" - } - } - ] - }, - { - "name": "onBeforeRedirect", - "type": "function", - "description": "Fired when a server-initiated redirect is about to occur.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."}, - "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."}, - "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}, - "redirectUrl": {"type": "string", "description": "The new URL."}, - "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this redirect."}, - "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnBeforeRedirectOptions" - } - } - ] - }, - { - "name": "onCompleted", - "type": "function", - "description": "Fired when a request is completed.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."}, - "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."}, - "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}, - "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this response."}, - "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - }, - { - "type": "array", - "optional": true, - "name": "extraInfoSpec", - "description": "Array of extra information that should be passed to the listener function.", - "items": { - "$ref": "OnCompletedOptions" - } - } - ] - }, - { - "name": "onErrorOccurred", - "type": "function", - "description": "Fired when an error occurs.", - "parameters": [ - { - "type": "object", - "name": "details", - "properties": { - "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."}, - "url": {"type": "string"}, - "method": {"type": "string", "description": "Standard HTTP method."}, - "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."}, - "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."}, - "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."}, - "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."}, - "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}, - "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."}, - "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."}, - "error": {"type": "string", "description": "The error description. This string is <em>not</em> guaranteed to remain backwards compatible between releases. You must not parse and act based upon its content."} - } - } - ], - "extraParameters": [ - { - "$ref": "RequestFilter", - "name": "filter", - "description": "A set of filters that restricts the events that will be sent to this listener." - } - ] - } - ] - } -] diff --git a/toolkit/components/webextensions/test/mochitest/.eslintrc.js b/toolkit/components/webextensions/test/mochitest/.eslintrc.js deleted file mode 100644 index 53938410b..000000000 --- a/toolkit/components/webextensions/test/mochitest/.eslintrc.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; - -module.exports = { // eslint-disable-line no-undef - "extends": "../../../../../testing/mochitest/mochitest.eslintrc.js", - - "env": { - "webextensions": true, - }, - - "globals": { - "ChromeWorker": false, - "onmessage": true, - "sendAsyncMessage": false, - - "waitForLoad": true, - "promiseConsoleOutput": true, - - "ExtensionTestUtils": false, - "NetUtil": true, - "webrequest_test": false, - "XPCOMUtils": true, - - // head_webrequest.js symbols - "addStylesheet": true, - "addLink": true, - "addImage": true, - "addScript": true, - "addFrame": true, - "makeExtension": false, - }, - - "rules": { - "no-shadow": 0, - }, -}; diff --git a/toolkit/components/webextensions/test/mochitest/chrome.ini b/toolkit/components/webextensions/test/mochitest/chrome.ini deleted file mode 100644 index 26585cad7..000000000 --- a/toolkit/components/webextensions/test/mochitest/chrome.ini +++ /dev/null @@ -1,35 +0,0 @@ -[DEFAULT] -support-files = - chrome_head.js - head.js - head_cookies.js - file_sample.html - webrequest_chromeworker.js - webrequest_test.jsm -tags = webextensions - -[test_chrome_ext_background_debug_global.html] -skip-if = (os == 'android') # android doesn't have devtools -[test_chrome_ext_background_page.html] -skip-if = (toolkit == 'android') # android doesn't have devtools -[test_chrome_ext_eventpage_warning.html] -[test_chrome_ext_contentscript_unrecognizedprop_warning.html] -skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android. -[test_chrome_ext_hybrid_addons.html] -[test_chrome_ext_trustworthy_origin.html] -[test_chrome_ext_webnavigation_resolved_urls.html] -skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android. -[test_chrome_ext_shutdown_cleanup.html] -[test_chrome_native_messaging_paths.html] -skip-if = os != "mac" && os != "linux" -[test_ext_cookies_expiry.html] -[test_ext_cookies_permissions_bad.html] -[test_ext_cookies_permissions_good.html] -[test_ext_cookies_containers.html] -[test_ext_jsversion.html] -[test_ext_schema.html] -[test_chrome_ext_storage_cleanup.html] -[test_chrome_ext_idle.html] -[test_chrome_ext_downloads_saveAs.html] -[test_chrome_ext_webrequest_background_events.html] -skip-if = os == 'android' # webrequest api unsupported (bug 1258975). diff --git a/toolkit/components/webextensions/test/mochitest/chrome_head.js b/toolkit/components/webextensions/test/mochitest/chrome_head.js deleted file mode 100644 index da2f53a02..000000000 --- a/toolkit/components/webextensions/test/mochitest/chrome_head.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -const { - classes: Cc, - interfaces: Ci, - utils: Cu, - results: Cr, -} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); - diff --git a/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page1.html b/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page1.html deleted file mode 100644 index 663ebc611..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page1.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<body> - -<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe> - -<form> -</form> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page2.html b/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page2.html deleted file mode 100644 index cc1acc83d..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page2.html +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<body> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page3.html b/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page3.html deleted file mode 100644 index a0a26a2e9..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_WebNavigation_page3.html +++ /dev/null @@ -1,9 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<body> - -<a id="elt" href="file_WebNavigation_page3.html#ref">click me</a> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_WebRequest_page3.html b/toolkit/components/webextensions/test/mochitest/file_WebRequest_page3.html deleted file mode 100644 index 5807dd439..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_WebRequest_page3.html +++ /dev/null @@ -1,11 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> -<script> -"use strict"; -window.close(); -</script> -</head> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_csp.html b/toolkit/components/webextensions/test/mochitest/file_csp.html deleted file mode 100644 index 206e44390..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_csp.html +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> -</head> -<body> - -<div id="test">Sample text</div> -<img id="bad-image" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" /> -<script id="bad-script" type="text/javascript" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_script_bad.js"></script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_csp.html^headers^ b/toolkit/components/webextensions/test/mochitest/file_csp.html^headers^ deleted file mode 100644 index 4c6fa3c26..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_csp.html^headers^ +++ /dev/null @@ -1 +0,0 @@ -Content-Security-Policy: default-src 'self' diff --git a/toolkit/components/webextensions/test/mochitest/file_ext_test_api_injection.js b/toolkit/components/webextensions/test/mochitest/file_ext_test_api_injection.js deleted file mode 100644 index 06dfae65e..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_ext_test_api_injection.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -var {interfaces: Ci} = Components; - -Components.utils.import("resource://gre/modules/Services.jsm"); - -Services.console.registerListener(function listener(message) { - if (/WebExt Privilege Escalation/.test(message.message)) { - Services.console.unregisterListener(listener); - sendAsyncMessage("console-message", {message: message.message}); - } -}); diff --git a/toolkit/components/webextensions/test/mochitest/file_image_bad.png b/toolkit/components/webextensions/test/mochitest/file_image_bad.png Binary files differdeleted file mode 100644 index 4c3be5084..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_image_bad.png +++ /dev/null diff --git a/toolkit/components/webextensions/test/mochitest/file_image_good.png b/toolkit/components/webextensions/test/mochitest/file_image_good.png Binary files differdeleted file mode 100644 index 769c63634..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_image_good.png +++ /dev/null diff --git a/toolkit/components/webextensions/test/mochitest/file_image_redirect.png b/toolkit/components/webextensions/test/mochitest/file_image_redirect.png Binary files differdeleted file mode 100644 index 4c3be5084..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_image_redirect.png +++ /dev/null diff --git a/toolkit/components/webextensions/test/mochitest/file_mixed.html b/toolkit/components/webextensions/test/mochitest/file_mixed.html deleted file mode 100644 index f3c7dda58..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_mixed.html +++ /dev/null @@ -1,13 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> -</head> -<body> - -<div id="test">Sample text</div> -<img id="bad-image" src="http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" /> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_permission_xhr.html b/toolkit/components/webextensions/test/mochitest/file_permission_xhr.html deleted file mode 100644 index 22a55f90d..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_permission_xhr.html +++ /dev/null @@ -1,55 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> -</head> -<body> - -<script> -"use strict"; - -/* globals privilegedFetch, privilegedXHR */ -/* eslint-disable mozilla/balanced-listeners */ - -addEventListener("message", function rcv(event) { - removeEventListener("message", rcv, false); - - function assertTrue(condition, description) { - postMessage({msg: "assertTrue", condition, description}, "*"); - } - - function passListener() { - assertTrue(true, "Content XHR has no elevated privileges"); - postMessage({"msg": "finish"}, "*"); - } - - function failListener() { - assertTrue(false, "Content XHR has no elevated privileges"); - postMessage({"msg": "finish"}, "*"); - } - - try { - new privilegedXHR(); - assertTrue(false, "Content should not have access to privileged XHR constructor"); - } catch (e) { - assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged XHR constructor"); - } - - try { - new privilegedFetch(); - assertTrue(false, "Content should not have access to privileged fetch() constructor"); - } catch (e) { - assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged fetch() constructor"); - } - - let req = new XMLHttpRequest(); - req.addEventListener("load", failListener); - req.addEventListener("error", passListener); - req.open("GET", "http://example.org/example.txt"); - req.send(); -}, false); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_privilege_escalation.html b/toolkit/components/webextensions/test/mochitest/file_privilege_escalation.html deleted file mode 100644 index 258f7058d..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_privilege_escalation.html +++ /dev/null @@ -1,13 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> -</head> -<body> - <script type="text/javascript"> - "use strict"; - throw new Error(`WebExt Privilege Escalation: typeof(browser) = ${typeof(browser)}`); - </script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_sample.html b/toolkit/components/webextensions/test/mochitest/file_sample.html deleted file mode 100644 index a20e49a1f..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_sample.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> -</head> -<body> - -<div id="test">Sample text</div> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_script_bad.js b/toolkit/components/webextensions/test/mochitest/file_script_bad.js deleted file mode 100644 index c425122c7..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_script_bad.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -window.failure = true; diff --git a/toolkit/components/webextensions/test/mochitest/file_script_good.js b/toolkit/components/webextensions/test/mochitest/file_script_good.js deleted file mode 100644 index 1848edf68..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_script_good.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -window.success = window.success ? window.success + 1 : 1; diff --git a/toolkit/components/webextensions/test/mochitest/file_script_redirect.js b/toolkit/components/webextensions/test/mochitest/file_script_redirect.js deleted file mode 100644 index c89a196c2..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_script_redirect.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; - -window.failure = true; - diff --git a/toolkit/components/webextensions/test/mochitest/file_script_xhr.js b/toolkit/components/webextensions/test/mochitest/file_script_xhr.js deleted file mode 100644 index 07f80eb2e..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_script_xhr.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -var request = new XMLHttpRequest(); -request.open("get", "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/xhr_resource", false); -request.send(); diff --git a/toolkit/components/webextensions/test/mochitest/file_style_bad.css b/toolkit/components/webextensions/test/mochitest/file_style_bad.css deleted file mode 100644 index 8dbc8dc7a..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_style_bad.css +++ /dev/null @@ -1,3 +0,0 @@ -#test { - color: green !important; -} diff --git a/toolkit/components/webextensions/test/mochitest/file_style_good.css b/toolkit/components/webextensions/test/mochitest/file_style_good.css deleted file mode 100644 index 46f9774b5..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_style_good.css +++ /dev/null @@ -1,3 +0,0 @@ -#test { - color: red; -} diff --git a/toolkit/components/webextensions/test/mochitest/file_style_redirect.css b/toolkit/components/webextensions/test/mochitest/file_style_redirect.css deleted file mode 100644 index 8dbc8dc7a..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_style_redirect.css +++ /dev/null @@ -1,3 +0,0 @@ -#test { - color: green !important; -} diff --git a/toolkit/components/webextensions/test/mochitest/file_teardown_test.js b/toolkit/components/webextensions/test/mochitest/file_teardown_test.js deleted file mode 100644 index 7246012ad..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_teardown_test.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; - -/* globals addMessageListener */ -let {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {}); -let events = []; -function record(type, extensionContext) { - let eventType = type == "proxy-context-load" ? "load" : "unload"; - let url = extensionContext.uri.spec; - let extensionId = extensionContext.extension.id; - events.push({eventType, url, extensionId}); -} - -Management.on("proxy-context-load", record); -Management.on("proxy-context-unload", record); -addMessageListener("cleanup", () => { - Management.off("proxy-context-load", record); - Management.off("proxy-context-unload", record); -}); - -addMessageListener("get-context-events", extensionId => { - sendAsyncMessage("context-events", events); - events = []; -}); -sendAsyncMessage("chromescript-startup"); diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect.html b/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect.html deleted file mode 100644 index cba3043f7..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect.html +++ /dev/null @@ -1,9 +0,0 @@ -<!DOCTYPE HTML> - -<html> - <head> - <meta http-equiv="refresh" content="1;dummy_page.html"> - </head> - <body> - </body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html b/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html deleted file mode 100644 index c5b436979..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html +++ /dev/null @@ -1,8 +0,0 @@ -<!DOCTYPE HTML> - -<html> - <head> - </head> - <body> - </body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ b/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ deleted file mode 100644 index 574a392a1..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ +++ /dev/null @@ -1 +0,0 @@ -Refresh: 1;url=dummy_page.html diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_frameClientRedirect.html b/toolkit/components/webextensions/test/mochitest/file_webNavigation_frameClientRedirect.html deleted file mode 100644 index d360bcbb1..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_frameClientRedirect.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<body> - -<iframe src="file_webNavigation_clientRedirect.html" width="200" height="200"></iframe> - -<form> -</form> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_frameRedirect.html b/toolkit/components/webextensions/test/mochitest/file_webNavigation_frameRedirect.html deleted file mode 100644 index 06dbd4374..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_frameRedirect.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<body> - -<iframe src="redirection.sjs" width="200" height="200"></iframe> - -<form> -</form> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe.html b/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe.html deleted file mode 100644 index 307990714..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<body> - -<iframe src="file_webNavigation_manualSubframe_page1.html" width="200" height="200"></iframe> - -<form> -</form> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe_page1.html b/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe_page1.html deleted file mode 100644 index 55bb7aa6a..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe_page1.html +++ /dev/null @@ -1,8 +0,0 @@ -<!DOCTYPE html> - -<html> - <body> - <h1>page1</h1> - <a href="file_webNavigation_manualSubframe_page2.html">page2</a> - </body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe_page2.html b/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe_page2.html deleted file mode 100644 index 8f589f8bb..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_webNavigation_manualSubframe_page2.html +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE html> - -<html> - <body> - <h1>page2</h1> - </body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/file_with_about_blank.html b/toolkit/components/webextensions/test/mochitest/file_with_about_blank.html deleted file mode 100644 index af51c2e52..000000000 --- a/toolkit/components/webextensions/test/mochitest/file_with_about_blank.html +++ /dev/null @@ -1,10 +0,0 @@ -<!doctype html> -<html> -<head> - <meta charset="utf-8"> -</head> -<body> - <iframe id="a_b" src="about:blank"></iframe> - <iframe srcdoc="galactica actual" src="adama"></iframe> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/head.js b/toolkit/components/webextensions/test/mochitest/head.js deleted file mode 100644 index 1b1a29472..000000000 --- a/toolkit/components/webextensions/test/mochitest/head.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict"; - -/* exported waitForLoad */ - -function waitForLoad(win) { - return new Promise(resolve => { - win.addEventListener("load", function listener() { - win.removeEventListener("load", listener, true); - resolve(); - }, true); - }); -} - diff --git a/toolkit/components/webextensions/test/mochitest/head_cookies.js b/toolkit/components/webextensions/test/mochitest/head_cookies.js deleted file mode 100644 index 9f6966551..000000000 --- a/toolkit/components/webextensions/test/mochitest/head_cookies.js +++ /dev/null @@ -1,167 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -/* exported testCookies */ - -function* testCookies(options) { - // Changing the options object is a bit of a hack, but it allows us to easily - // pass an expiration date to the background script. - options.expiry = Date.now() / 1000 + 3600; - - async function background(backgroundOptions) { - // Ask the parent scope to change some cookies we may or may not have - // permission for. - let awaitChanges = new Promise(resolve => { - browser.test.onMessage.addListener(msg => { - browser.test.assertEq("cookies-changed", msg, "browser.test.onMessage"); - resolve(); - }); - }); - - let changed = []; - browser.cookies.onChanged.addListener(event => { - changed.push(`${event.cookie.name}:${event.cause}`); - }); - browser.test.sendMessage("change-cookies"); - - - // Try to access some cookies in various ways. - let {url, domain, secure} = backgroundOptions; - - let failures = 0; - let tallyFailure = error => { - failures++; - }; - - try { - await awaitChanges; - - let cookie = await browser.cookies.get({url, name: "foo"}); - browser.test.assertEq(backgroundOptions.shouldPass, cookie != null, "should pass == get cookie"); - - let cookies = await browser.cookies.getAll({domain}); - if (backgroundOptions.shouldPass) { - browser.test.assertEq(2, cookies.length, "expected number of cookies"); - } else { - browser.test.assertEq(0, cookies.length, "expected number of cookies"); - } - - await Promise.all([ - browser.cookies.set({url, domain, secure, name: "foo", "value": "baz", expirationDate: backgroundOptions.expiry}).catch(tallyFailure), - browser.cookies.set({url, domain, secure, name: "bar", "value": "quux", expirationDate: backgroundOptions.expiry}).catch(tallyFailure), - browser.cookies.remove({url, name: "deleted"}), - ]); - - if (backgroundOptions.shouldPass) { - // The order of eviction events isn't guaranteed, so just check that - // it's there somewhere. - let evicted = changed.indexOf("evicted:evicted"); - if (evicted < 0) { - browser.test.fail("got no eviction event"); - } else { - browser.test.succeed("got eviction event"); - changed.splice(evicted, 1); - } - - browser.test.assertEq("x:explicit,x:overwrite,x:explicit,x:explicit,foo:overwrite,foo:explicit,bar:explicit,deleted:explicit", - changed.join(","), "expected changes"); - } else { - browser.test.assertEq("", changed.join(","), "expected no changes"); - } - - if (!(backgroundOptions.shouldPass || backgroundOptions.shouldWrite)) { - browser.test.assertEq(2, failures, "Expected failures"); - } else { - browser.test.assertEq(0, failures, "Expected no failures"); - } - - browser.test.notifyPass("cookie-permissions"); - } catch (error) { - browser.test.fail(`Error: ${error} :: ${error.stack}`); - browser.test.notifyFail("cookie-permissions"); - } - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "permissions": options.permissions, - }, - - background: `(${background})(${JSON.stringify(options)})`, - }); - - - let cookieSvc = SpecialPowers.Services.cookies; - - let domain = options.domain.replace(/^\.?/, "."); - - // This will be evicted after we add a fourth cookie. - cookieSvc.add(domain, "/", "evicted", "bar", options.secure, false, false, options.expiry); - // This will be modified by the background script. - cookieSvc.add(domain, "/", "foo", "bar", options.secure, false, false, options.expiry); - // This will be deleted by the background script. - cookieSvc.add(domain, "/", "deleted", "bar", options.secure, false, false, options.expiry); - - - yield extension.startup(); - - yield extension.awaitMessage("change-cookies"); - cookieSvc.add(domain, "/", "x", "y", options.secure, false, false, options.expiry); - cookieSvc.add(domain, "/", "x", "z", options.secure, false, false, options.expiry); - cookieSvc.remove(domain, "x", "/", false, {}); - extension.sendMessage("cookies-changed"); - - yield extension.awaitFinish("cookie-permissions"); - yield extension.unload(); - - - function getCookies(host) { - let cookies = []; - let enum_ = cookieSvc.getCookiesFromHost(host, {}); - while (enum_.hasMoreElements()) { - cookies.push(enum_.getNext().QueryInterface(SpecialPowers.Ci.nsICookie2)); - } - return cookies.sort((a, b) => String.localeCompare(a.name, b.name)); - } - - let cookies = getCookies(options.domain); - info(`Cookies: ${cookies.map(c => `${c.name}=${c.value}`)}`); - - if (options.shouldPass) { - is(cookies.length, 2, "expected two cookies for host"); - - is(cookies[0].name, "bar", "correct cookie name"); - is(cookies[0].value, "quux", "correct cookie value"); - - is(cookies[1].name, "foo", "correct cookie name"); - is(cookies[1].value, "baz", "correct cookie value"); - } else if (options.shouldWrite) { - // Note: |shouldWrite| applies only when |shouldPass| is false. - // This is necessary because, unfortunately, websites (and therefore web - // extensions) are allowed to write some cookies which they're not allowed - // to read. - is(cookies.length, 3, "expected three cookies for host"); - - is(cookies[0].name, "bar", "correct cookie name"); - is(cookies[0].value, "quux", "correct cookie value"); - - is(cookies[1].name, "deleted", "correct cookie name"); - - is(cookies[2].name, "foo", "correct cookie name"); - is(cookies[2].value, "baz", "correct cookie value"); - } else { - is(cookies.length, 2, "expected two cookies for host"); - - is(cookies[0].name, "deleted", "correct second cookie name"); - - is(cookies[1].name, "foo", "correct cookie name"); - is(cookies[1].value, "bar", "correct cookie value"); - } - - for (let cookie of cookies) { - cookieSvc.remove(cookie.host, cookie.name, "/", false, {}); - } - // Make sure we don't silently poison subsequent tests if something goes wrong. - is(getCookies(options.domain).length, 0, "cookies cleared"); -} diff --git a/toolkit/components/webextensions/test/mochitest/head_webrequest.js b/toolkit/components/webextensions/test/mochitest/head_webrequest.js deleted file mode 100644 index 96924e505..000000000 --- a/toolkit/components/webextensions/test/mochitest/head_webrequest.js +++ /dev/null @@ -1,331 +0,0 @@ -"use strict"; - -let commonEvents = { - "onBeforeRequest": [{urls: ["<all_urls>"]}, ["blocking"]], - "onBeforeSendHeaders": [{urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]], - "onSendHeaders": [{urls: ["<all_urls>"]}, ["requestHeaders"]], - "onBeforeRedirect": [{urls: ["<all_urls>"]}], - "onHeadersReceived": [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]], - "onResponseStarted": [{urls: ["<all_urls>"]}], - "onCompleted": [{urls: ["<all_urls>"]}, ["responseHeaders"]], - "onErrorOccurred": [{urls: ["<all_urls>"]}], -}; - -function background(events) { - let expect; - let ignore; - let defaultOrigin; - - browser.test.onMessage.addListener((msg, expected) => { - if (msg !== "set-expected") { - return; - } - expect = expected.expect; - defaultOrigin = expected.origin; - ignore = expected.ignore; - let promises = []; - // Initialize some stuff we'll need in the tests. - for (let entry of Object.values(expect)) { - // a place for the test infrastructure to store some state. - entry.test = {}; - // Each entry in expected gets a Promise that will be resolved in the - // last event for that entry. This will either be onCompleted, or the - // last entry if an events list was provided. - promises.push(new Promise(resolve => { entry.test.resolve = resolve; })); - // If events was left undefined, we're expecting all normal events we're - // listening for, exclude onBeforeRedirect and onErrorOccurred - if (entry.events === undefined) { - entry.events = Object.keys(events).filter(name => name != "onErrorOccurred" && name != "onBeforeRedirect"); - } - if (entry.optional_events === undefined) { - entry.optional_events = []; - } - } - // When every expected entry has finished our test is done. - Promise.all(promises).then(() => { - browser.test.sendMessage("done"); - }); - browser.test.sendMessage("continue"); - }); - - // Retrieve the per-file/test expected values. - function getExpected(details) { - let url = new URL(details.url); - let filename; - if (url.protocol == "data:") { - // pathname is everything after protocol. - filename = url.pathname; - } else { - filename = url.pathname.split("/").pop(); - } - if (ignore && ignore.includes(filename)) { - return; - } - let expected = expect[filename]; - if (!expected) { - browser.test.fail(`unexpected request ${filename}`); - return; - } - // Save filename for redirect verification. - expected.test.filename = filename; - return expected; - } - - // Process any test header modifications that can happen in request or response phases. - // If a test includes headers, it needs a complete header object, no undefined - // objects even if empty: - // request: { - // add: {"HeaderName": "value",}, - // modify: {"HeaderName": "value",}, - // remove: ["HeaderName",], - // }, - // response: { - // add: {"HeaderName": "value",}, - // modify: {"HeaderName": "value",}, - // remove: ["HeaderName",], - // }, - function processHeaders(phase, expected, details) { - // This should only happen once per phase [request|response]. - browser.test.assertFalse(!!expected.test[phase], `First processing of headers for ${phase}`); - expected.test[phase] = true; - - let headers = details[`${phase}Headers`]; - browser.test.assertTrue(Array.isArray(headers), `${phase}Headers array present`); - - let {add, modify, remove} = expected.headers[phase]; - - for (let name in add) { - browser.test.assertTrue(!headers.find(h => h.name === name), `header ${name} to be added not present yet in ${phase}Headers`); - let header = {name: name}; - if (name.endsWith("-binary")) { - header.binaryValue = Array.from(add[name], c => c.charCodeAt(0)); - } else { - header.value = add[name]; - } - headers.push(header); - } - - let modifiedAny = false; - for (let header of headers) { - if (header.name.toLowerCase() in modify) { - header.value = modify[header.name.toLowerCase()]; - modifiedAny = true; - } - } - browser.test.assertTrue(modifiedAny, `at least one ${phase}Headers element to modify`); - - let deletedAny = false; - for (let j = headers.length; j-- > 0;) { - if (remove.includes(headers[j].name.toLowerCase())) { - headers.splice(j, 1); - deletedAny = true; - } - } - browser.test.assertTrue(deletedAny, `at least one ${phase}Headers element to delete`); - - return headers; - } - - // phase is request or response. - function checkHeaders(phase, expected, details) { - if (!/^https?:/.test(details.url)) { - return; - } - - let headers = details[`${phase}Headers`]; - browser.test.assertTrue(Array.isArray(headers), `valid ${phase}Headers array`); - - let {add, modify, remove} = expected.headers[phase]; - for (let name in add) { - let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value; - browser.test.assertEq(value, add[name], `header ${name} correctly injected in ${phase}Headers`); - } - - for (let name in modify) { - let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value; - browser.test.assertEq(value, modify[name], `header ${name} matches modified value`); - } - - for (let name of remove) { - let found = headers.find(h => h.name.toLowerCase() === name.toLowerCase()); - browser.test.assertFalse(!!found, `deleted header ${name} still found in ${phase}Headers`); - } - } - - function getListener(name) { - return details => { - let result = {}; - browser.test.log(`${name} ${details.requestId} ${details.url}`); - let expected = getExpected(details); - if (!expected) { - return result; - } - let expectedEvent = expected.events[0] == name; - if (expectedEvent) { - expected.events.shift(); - } else { - expectedEvent = expected.optional_events[0] == name; - if (expectedEvent) { - expected.optional_events.shift(); - } - } - browser.test.assertTrue(expectedEvent, `received ${name}`); - browser.test.assertEq(expected.type, details.type, "resource type is correct"); - browser.test.assertEq(expected.origin || defaultOrigin, details.originUrl, "origin is correct"); - - if (name == "onBeforeRequest") { - // Save some values to test request consistency in later events. - browser.test.assertTrue(details.tabId !== undefined, `tabId ${details.tabId}`); - browser.test.assertTrue(details.requestId !== undefined, `requestId ${details.requestId}`); - // Validate requestId if it's already set, this happens with redirects. - if (expected.test.requestId !== undefined) { - browser.test.assertEq("string", typeof expected.test.requestId, `requestid ${expected.test.requestId} is string`); - browser.test.assertEq("string", typeof details.requestId, `requestid ${details.requestId} is string`); - browser.test.assertEq("number", typeof parseInt(details.requestId, 10), "parsed requestid is number"); - browser.test.assertNotEq(expected.test.requestId, details.requestId, - `last requestId ${expected.test.requestId} different from this one ${details.requestId}`); - } else { - // Save any values we want to validate in later events. - expected.test.requestId = details.requestId; - expected.test.tabId = details.tabId; - } - // Tests we don't need to do every event. - browser.test.assertTrue(details.type.toUpperCase() in browser.webRequest.ResourceType, `valid resource type ${details.type}`); - if (details.type == "main_frame") { - browser.test.assertEq(0, details.frameId, "frameId is zero when type is main_frame bug 1329299"); - } - } else { - // On events after onBeforeRequest, check the previous values. - browser.test.assertEq(expected.test.requestId, details.requestId, "correct requestId"); - browser.test.assertEq(expected.test.tabId, details.tabId, "correct tabId"); - } - if (name == "onBeforeSendHeaders") { - if (expected.headers && expected.headers.request) { - result.requestHeaders = processHeaders("request", expected, details); - } - if (expected.redirect) { - browser.test.log(`${name} redirect request`); - result.redirectUrl = details.url.replace(expected.test.filename, expected.redirect); - } - } - if (name == "onSendHeaders") { - if (expected.headers && expected.headers.request) { - checkHeaders("request", expected, details); - } - } - if (name == "onHeadersReceived") { - browser.test.assertEq(expected.status || 200, details.statusCode, - `expected HTTP status received for ${details.url}`); - if (expected.headers && expected.headers.response) { - result.responseHeaders = processHeaders("response", expected, details); - } - } - if (name == "onCompleted") { - // If we have already completed a GET request for this url, - // and it was found, we expect for the response to come fromCache. - // expected.cached may be undefined, force boolean. - let expectCached = !!expected.cached && details.method === "GET" && details.statusCode != 404; - browser.test.assertEq(expectCached, details.fromCache, "fromCache is correct"); - // We can only tell IPs for non-cached HTTP requests. - if (!details.fromCache && /^https?:/.test(details.url)) { - browser.test.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`); - } - if (expected.headers && expected.headers.response) { - checkHeaders("response", expected, details); - } - } - - if (expected.cancel && expected.cancel == name) { - browser.test.log(`${name} cancel request`); - browser.test.sendMessage("cancelled"); - result.cancel = true; - } - // If we've used up all the events for this test, resolve the promise. - // If something wrong happens and more events come through, there will be - // failures. - if (expected.events.length <= 0) { - expected.test.resolve(); - } - return result; - }; - } - - for (let [name, args] of Object.entries(events)) { - browser.test.log(`adding listener for ${name}`); - try { - browser.webRequest[name].addListener(getListener(name), ...args); - } catch (e) { - browser.test.assertTrue(/\brequestBody\b/.test(e.message), - "Request body is unsupported"); - - // RequestBody is disabled in release builds. - if (!/\brequestBody\b/.test(e.message)) { - throw e; - } - - args.splice(args.indexOf("requestBody"), 1); - browser.webRequest[name].addListener(getListener(name), ...args); - } - } -} - -/* exported makeExtension */ - -function makeExtension(events = commonEvents) { - return ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webRequest", - "webRequestBlocking", - "<all_urls>", - ], - }, - background: `(${background})(${JSON.stringify(events)})`, - }); -} - -/* exported addStylesheet */ - -function addStylesheet(file) { - let link = document.createElement("link"); - link.setAttribute("rel", "stylesheet"); - link.setAttribute("href", file); - document.body.appendChild(link); -} - -/* exported addLink */ - -function addLink(file) { - let a = document.createElement("a"); - a.setAttribute("href", file); - a.setAttribute("target", "_blank"); - document.body.appendChild(a); - return a; -} - -/* exported addImage */ - -function addImage(file) { - let img = document.createElement("img"); - img.setAttribute("src", file); - document.body.appendChild(img); -} - -/* exported addScript */ - -function addScript(file) { - let script = document.createElement("script"); - script.setAttribute("type", "text/javascript"); - script.setAttribute("src", file); - document.getElementsByTagName("head").item(0).appendChild(script); -} - -/* exported addFrame */ - -function addFrame(file) { - let frame = document.createElement("iframe"); - frame.setAttribute("width", "200"); - frame.setAttribute("height", "200"); - frame.setAttribute("src", file); - document.body.appendChild(frame); -} diff --git a/toolkit/components/webextensions/test/mochitest/mochitest.ini b/toolkit/components/webextensions/test/mochitest/mochitest.ini deleted file mode 100644 index 1f61060ad..000000000 --- a/toolkit/components/webextensions/test/mochitest/mochitest.ini +++ /dev/null @@ -1,115 +0,0 @@ -[DEFAULT] -support-files = - head.js - file_mixed.html - head_webrequest.js - file_csp.html - file_csp.html^headers^ - file_WebRequest_page3.html - file_webNavigation_clientRedirect.html - file_webNavigation_clientRedirect_httpHeaders.html - file_webNavigation_clientRedirect_httpHeaders.html^headers^ - file_webNavigation_frameClientRedirect.html - file_webNavigation_frameRedirect.html - file_webNavigation_manualSubframe.html - file_webNavigation_manualSubframe_page1.html - file_webNavigation_manualSubframe_page2.html - file_WebNavigation_page1.html - file_WebNavigation_page2.html - file_WebNavigation_page3.html - file_with_about_blank.html - file_image_good.png - file_image_bad.png - file_image_redirect.png - file_style_good.css - file_style_bad.css - file_style_redirect.css - file_script_good.js - file_script_bad.js - file_script_redirect.js - file_script_xhr.js - file_sample.html - redirection.sjs - file_privilege_escalation.html - file_ext_test_api_injection.js - file_permission_xhr.html - file_teardown_test.js - return_headers.sjs - webrequest_worker.js -tags = webextensions - -[test_clipboard.html] -# skip-if = # disabled test case with_permission_allow_copy, see inline comment. -[test_ext_inIncognitoContext_window.html] -skip-if = os == 'android' # Android does not currently support windows. -[test_ext_geturl.html] -[test_ext_background_canvas.html] -[test_ext_content_security_policy.html] -[test_ext_contentscript.html] -[test_ext_contentscript_api_injection.html] -[test_ext_contentscript_async_loading.html] -[test_ext_contentscript_context.html] -[test_ext_contentscript_create_iframe.html] -[test_ext_contentscript_devtools_metadata.html] -[test_ext_contentscript_exporthelpers.html] -[test_ext_contentscript_css.html] -[test_ext_contentscript_about_blank.html] -[test_ext_contentscript_permission.html] -skip-if = os == 'android' # Android does not support tabs API. Bug 1260250 -[test_ext_contentscript_teardown.html] -skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250 -[test_ext_exclude_include_globs.html] -[test_ext_external_messaging.html] -[test_ext_i18n_css.html] -[test_ext_generate.html] -[test_ext_notifications.html] -[test_ext_permission_xhr.html] -[test_ext_runtime_connect.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_runtime_connect_twoway.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_runtime_connect2.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_runtime_disconnect.html] -[test_ext_runtime_id.html] -[test_ext_sandbox_var.html] -[test_ext_sendmessage_reply.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_sendmessage_reply2.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_sendmessage_doublereply.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_sendmessage_no_receiver.html] -[test_ext_storage_content.html] -[test_ext_storage_tab.html] -skip-if = os == 'android' # Android does not currently support tabs. -[test_ext_test.html] -[test_ext_cookies.html] -skip-if = os == 'android' # Bug 1258975 on android. -[test_ext_background_api_injection.html] -[test_ext_background_generated_url.html] -[test_ext_background_teardown.html] -[test_ext_tab_teardown.html] -skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250 -[test_ext_unload_frame.html] -[test_ext_i18n.html] -skip-if = (os == 'android') # Bug 1258975 on android. -[test_ext_listener_proxies.html] -[test_ext_web_accessible_resources.html] -skip-if = (os == 'android') # Bug 1258975 on android. -[test_ext_webrequest_background_events.html] -skip-if = os == 'android' # webrequest api unsupported (bug 1258975). -[test_ext_webrequest_basic.html] -skip-if = os == 'android' # webrequest api unsupported (bug 1258975). -[test_ext_webrequest_suspend.html] -skip-if = os == 'android' # webrequest api unsupported (bug 1258975). -[test_ext_webrequest_upload.html] -skip-if = release_or_beta || os == 'android' # webrequest api unsupported (bug 1258975). -[test_ext_webnavigation.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_webnavigation_filters.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_window_postMessage.html] -[test_ext_subframes_privileges.html] -skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975). -[test_ext_xhr_capabilities.html] diff --git a/toolkit/components/webextensions/test/mochitest/redirection.sjs b/toolkit/components/webextensions/test/mochitest/redirection.sjs deleted file mode 100644 index 370ecd213..000000000 --- a/toolkit/components/webextensions/test/mochitest/redirection.sjs +++ /dev/null @@ -1,4 +0,0 @@ -function handleRequest(aRequest, aResponse) { - aResponse.setStatusLine(aRequest.httpVersion, 302); - aResponse.setHeader("Location", "./dummy_page.html"); -} diff --git a/toolkit/components/webextensions/test/mochitest/return_headers.sjs b/toolkit/components/webextensions/test/mochitest/return_headers.sjs deleted file mode 100644 index 54e2e5fb4..000000000 --- a/toolkit/components/webextensions/test/mochitest/return_headers.sjs +++ /dev/null @@ -1,20 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set ft=javascript sts=2 sw=2 et tw=80: */ -"use strict"; - -/* exported handleRequest */ - -function handleRequest(request, response) { - response.setHeader("Content-Type", "text/plain", false); - - let headers = {}; - // Why on earth is this a nsISimpleEnumerator... - let enumerator = request.headers; - while (enumerator.hasMoreElements()) { - let header = enumerator.getNext().data; - headers[header.toLowerCase()] = request.getHeader(header); - } - - response.write(JSON.stringify(headers)); -} - diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_background_debug_global.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_background_debug_global.html deleted file mode 100644 index 0edf5ea86..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_background_debug_global.html +++ /dev/null @@ -1,166 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); - -const { - XPIProvider, -} = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm"); - -/** - * This test is asserting that ext-backgroundPage.js successfully sets its - * debug global in the AddonWrapper provided by XPIProvider.jsm - * - * It does _not_ test any functionality in devtools and does not guarantee - * debugging is actually working correctly end-to-end. - */ - -function background() { - window.testThing = "test!"; - browser.test.notifyPass("background script ran"); -} - -const ID = "debug@tests.mozilla.org"; -let extensionData = { - useAddonManager: "temporary", - background, - manifest: { - applications: {gecko: {id: ID}}, - }, -}; - -add_task(function* () { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitFinish("background script ran"); - - yield new Promise(function(resolve) { - window.BrowserToolboxProcess.emit("connectionchange", "opened", { - setAddonOptions(id, options) { - if (id === ID) { - let context = Cu.waiveXrays(options.global); - ok(context.chrome, "global context has a chrome object"); - ok(context.browser, "global context has a browser object"); - is("test!", context.testThing, "global context is the background script context"); - resolve(); - } - }, - }); - }); - - let addon = yield new Promise((resolve, reject) => { - AddonManager.getAddonByID(ID, addon => addon ? resolve(addon) : reject()); - }); - - ok(addon, `Got the addon wrapper for ${addon.id}`); - - function waitForDebugGlobalChanges(times, initialAddonInstanceID) { - return new Promise((resolve) => { - AddonManager.addAddonListener({ - count: 0, - notNullGlobalsCount: 0, - undefinedPrivateWrappersCount: 0, - lastAddonInstanceID: initialAddonInstanceID, - onPropertyChanged(newAddon, changedPropNames) { - if (newAddon.id != addon.id || - !changedPropNames.includes("debugGlobal")) { - return; - } - - ok(!(newAddon.setDebugGlobal) && !(newAddon.getDebugGlobal), - "The addon wrapper should not be a PrivateWrapper"); - - let activeAddon = XPIProvider.activeAddons.get(addon.id); - - let addonInstanceID; - - if (!activeAddon) { - // The addon has been disable, the preferred global should be null - addonInstanceID = this.lastAddonInstanceID; - delete this.lastAddonInstanceID; - } else { - addonInstanceID = activeAddon.instanceID; - this.lastAddonInstanceID = addonInstanceID; - } - - ok(addonInstanceID, `Got the addon instanceID for ${addon.id}`); - - AddonManager.getAddonByInstanceID(addonInstanceID).then((privateWrapper) => { - this.count += 1; - - if (!privateWrapper) { - // The addon has been uninstalled - this.undefinedPrivateWrappersCount += 1; - } else { - ok((privateWrapper.getDebugGlobal), "Got the addon PrivateWrapper"); - - if (privateWrapper.getDebugGlobal()) { - this.notNullGlobalsCount += 1; - } - } - - if (this.count == times) { - AddonManager.removeAddonListener(this); - resolve({ - counters: { - count: this.count, - notNullGlobalsCount: this.notNullGlobalsCount, - undefinedPrivateWrappersCount: this.undefinedPrivateWrappersCount, - }, - lastAddonInstanceID: this.lastAddonInstanceID, - }); - } - }); - }, - }); - }); - } - - // two calls expected, one for the shutdown and one for the startup - // of the background page. - let waitForDebugGlobalChangesOnReload = waitForDebugGlobalChanges(2); - - info("Addon reload..."); - yield addon.reload(); - - info("Addon completed startup after reload"); - - let { - counters: reloadCounters, - lastAddonInstanceID, - } = yield waitForDebugGlobalChangesOnReload; - - isDeeply(reloadCounters, {count: 2, notNullGlobalsCount: 1, undefinedPrivateWrappersCount: 0}, - "Got the expected number of onPropertyChanged calls on reload"); - - // one more call expected for the shutdown. - let waitForDebugGlobalChangesOnShutdown = waitForDebugGlobalChanges(1, lastAddonInstanceID); - - info("extension unloading..."); - yield extension.unload(); - info("extension unloaded"); - - let {counters: unloadCounters} = yield waitForDebugGlobalChangesOnShutdown; - - isDeeply(unloadCounters, {count: 1, notNullGlobalsCount: 0, undefinedPrivateWrappersCount: 1}, - "Got the expected number of onPropertyChanged calls on shutdown"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_background_page.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_background_page.html deleted file mode 100644 index 471c5339d..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_background_page.html +++ /dev/null @@ -1,84 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -Cu.import("resource://testing-common/TestUtils.jsm"); - -/* eslint-disable mozilla/balanced-listeners */ - -add_task(function* testAlertNotShownInBackgroundWindow() { - ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), - "Alerts should not be present at the start of the test."); - - let consoleOpened = TestUtils.topicObserved("web-console-created"); - - - let extension = ExtensionTestUtils.loadExtension({ - background: function() { - browser.test.log("background script executed"); - - alert("I am an alert in the background."); - - browser.test.notifyPass("alertCalled"); - }, - }); - - yield extension.startup(); - - info("startup complete loaded"); - - yield extension.awaitFinish("alertCalled"); - - - let alertWindows = Services.wm.getEnumerator("alert:alert"); - ok(!alertWindows.hasMoreElements(), "Should not show alert"); - - - // Make sure the message we output to the console is seen. - // This message is in ext-backgroundPage.js - let events = Cc["@mozilla.org/consoleAPI-storage;1"] - .getService(Ci.nsIConsoleAPIStorage).getEvents(); - - // This is the warning that is output after the first `alert()` call is made. - let alertWarningEvent = events[events.length - 2]; - is(alertWarningEvent.arguments[0], "alert() is not supported in background windows; please use console.log instead."); - - // This is the actual alert text that should be present in the console - // instead of as an `alert`. - let alertEvent = events[events.length - 1]; - is(alertEvent.arguments[0], "I am an alert in the background."); - - - // Wait for the browser console window to open. - yield consoleOpened; - - let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); - require("devtools/client/framework/devtools-browser"); - let {HUDService} = require("devtools/client/webconsole/hudservice"); - - // And then double check that we have an actual browser console. - let haveConsole = !!HUDService.getBrowserConsole(); - ok(haveConsole, "Expected browser console to be open"); - - if (haveConsole) { - yield HUDService.toggleBrowserConsole(); - } - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html deleted file mode 100644 index e08121a8f..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html +++ /dev/null @@ -1,80 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script unrecognized property on manifest</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest"; - -add_task(function* test_contentscript() { - function background() { - browser.runtime.onMessage.addListener(async (msg) => { - if (msg == "loaded") { - // NOTE: we're removing the tab from here because doing a win.close() - // from the chrome test code is raising a "TypeError: can't access - // dead object" exception. - let tabs = await browser.tabs.query({active: true, currentWindow: true}); - await browser.tabs.remove(tabs[0].id); - - browser.test.notifyPass("content-script-loaded"); - } - }); - } - - function contentScript() { - chrome.runtime.sendMessage("loaded"); - } - - let extensionData = { - manifest: { - content_scripts: [ - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - "unrecognized_property": "with-a-random-value", - }, - ], - }, - background, - - files: { - "content_script.js": contentScript, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - SimpleTest.waitForExplicitFinish(); - let waitForConsole = new Promise(resolve => { - SimpleTest.monitorConsole(resolve, [{ - message: /Reading manifest: Error processing content_scripts.*.unrecognized_property: An unexpected property was found/, - }]); - }); - - yield extension.startup(); - - window.open(`${BASE}/file_sample.html`); - - yield Promise.all([extension.awaitFinish("content-script-loaded")]); - info("test page loaded"); - - yield extension.unload(); - - SimpleTest.endMonitorConsole(); - yield waitForConsole; -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_downloads_saveAs.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_downloads_saveAs.html deleted file mode 100644 index c1aaae035..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_downloads_saveAs.html +++ /dev/null @@ -1,68 +0,0 @@ -<!doctype html> -<html> -<head> - <title>Test downloads.download() saveAs option</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_downloads_saveAs() { - function background() { - const url = URL.createObjectURL(new Blob(["file content"])); - browser.test.onMessage.addListener(async () => { - try { - let id = await browser.downloads.download({url, saveAs: true}); - browser.downloads.onChanged.addListener(delta => { - if (delta.state.current === "complete") { - browser.test.sendMessage("done", {ok: true, id}); - } - }); - } catch ({message}) { - browser.test.sendMessage("done", {ok: false, message}); - } - }); - browser.test.sendMessage("ready"); - } - - const {MockFilePicker} = SpecialPowers; - const manifest = {background, manifest: {permissions: ["downloads"]}}; - const extension = ExtensionTestUtils.loadExtension(manifest); - - MockFilePicker.init(window); - MockFilePicker.useAnyFile(); - const [file] = MockFilePicker.returnFiles; - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - extension.sendMessage("download"); - let result = yield extension.awaitMessage("done"); - - ok(result.ok, "downloads.download() works with saveAs"); - is(file.fileSize, 12, "downloaded file is the correct size"); - file.remove(false); - - // Test the user canceling the save dialog. - MockFilePicker.returnValue = MockFilePicker.returnCancel; - - extension.sendMessage("download"); - result = yield extension.awaitMessage("done"); - - ok(!result.ok, "download rejected if the user cancels the dialog"); - is(result.message, "Download canceled by the user", "with the correct message"); - ok(!file.exists(), "file was not downloaded"); - - yield extension.unload(); - MockFilePicker.cleanup(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_eventpage_warning.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_eventpage_warning.html deleted file mode 100644 index ecea8237e..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_eventpage_warning.html +++ /dev/null @@ -1,106 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for WebExtension EventPage Warning</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function createEventPageExtension(eventPage) { - function eventPageScript() { - browser.test.log("running event page as background script"); - browser.test.sendMessage("running", 1); - } - - return ExtensionTestUtils.loadExtension({ - manifest: { - "background": eventPage, - }, - files: { - "event-page-script.js": eventPageScript, - "event-page.html": `<html><head> - <meta charset="utf-8"> - <script src="event-page-script.js"><\/script> - </head></html>`, - }, - }); -} - -add_task(function* test_eventpages() { - // Used in other tests to prevent the monitorConsole to grip. - SimpleTest.waitForExplicitFinish(); - - let testCases = [ - { - message: "testing event page running as a background page", - eventPage: { - "page": "event-page.html", - "persistent": false, - }, - }, - { - message: "testing event page scripts running as a background page", - eventPage: { - "scripts": ["event-page-script.js"], - "persistent": false, - }, - }, - ]; - - for (let {message, eventPage} of testCases) { - info(message); - - // Wait for the expected logged warnings from the manifest validation. - let waitForConsole = new Promise(resolve => { - SimpleTest.monitorConsole(resolve, [{message: /Event pages are not currently supported./}]); - }); - - let extension = createEventPageExtension(eventPage); - - info("load complete"); - let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); - is(x, 1, "got correct value from extension"); - info("test complete"); - yield extension.unload(); - info("extension unloaded successfully"); - - SimpleTest.endMonitorConsole(); - yield waitForConsole; - - waitForConsole = new Promise(resolve => { - SimpleTest.monitorConsole(resolve, [{ - message: /Reading manifest: Error processing background.nonExistentProp: An unexpected property was found/, - }]); - }); - - info("testing additional unrecognized properties on background page"); - - extension = createEventPageExtension({ - "scripts": ["event-page-script.js"], - "nonExistentProp": true, - }); - - info("load complete"); - [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); - is(x, 1, "got correct value from extension"); - info("test complete"); - yield extension.unload(); - info("extension unloaded successfully"); - - SimpleTest.endMonitorConsole(); - yield waitForConsole; - } -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_hybrid_addons.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_hybrid_addons.html deleted file mode 100644 index a74c551f0..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_hybrid_addons.html +++ /dev/null @@ -1,141 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for hybrid addons: SDK or bootstrap.js + embedded WebExtension</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/** - * This test contains additional tests that ensure that an SDK hybrid addon - * which is using the new module loader can embed a webextension correctly: - * - * while the other tests related to the "Embedded WebExtension" are focused - * on unit testing a specific component, these tests are testing that a complete - * hybrid SDK addon works as expected. - * - * NOTE: this tests are also the only ones which tests an SDK hybrid addon that - * uses the new module loader (the one actually used in production by real world - * addons these days), while the Addon SDK "embedded-webextension" test addon - * uses the old deprecated module loader (as all the other Addon SDK test addons). - */ - -function generateClassicExtensionFiles({id, files}) { - // The addon install.rdf file, as it would be generated by jpm from the addon - // package.json metadata. - files["install.rdf"] = `<?xml version="1.0" encoding="utf-8"?> - <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:em="http://www.mozilla.org/2004/em-rdf#"> - <Description about="urn:mozilla:install-manifest"> - <em:id>${id}</em:id> - <em:type>2</em:type> - <em:bootstrap>true</em:bootstrap> - <em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension> - <em:unpack>false</em:unpack> - <em:version>0.1.0</em:version> - <em:name>Fake Hybrid Addon</em:name> - <em:description>A fake hybrid addon</em:description> - - <!-- Firefox --> - <em:targetApplication> - <Description> - <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> - <em:minVersion>51.0a1</em:minVersion> - <em:maxVersion>*</em:maxVersion> - </Description> - </em:targetApplication> - - <!-- Fennec --> - <em:targetApplication> - <Description> - <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id> - <em:minVersion>51.0a1</em:minVersion> - <em:maxVersion>*</em:maxVersion> - </Description> - </em:targetApplication> - </Description> - </RDF>`; - - // The addon package.json file. - files["package.json"] = `{ - "id": "${id}", - "name": "hybrid-addon", - "version": "0.1.0", - "description": "A fake hybrid addon", - "main": "index.js", - "engines": { - "firefox": ">= 51.0a1", - "fennec": ">= 51.0a1" - }, - "license": "MPL-2.0", - "hasEmbeddedWebExtension": true - }`; - - // The bootstrap file that jpm bundle in any SDK addon built with it. - files["bootstrap.js"] = ` - const { utils: Cu } = Components; - const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", ""); - const COMMONJS_URI = "resource://gre/modules/commonjs"; - const { require } = Cu.import(COMMONJS_URI + "/toolkit/require.js", {}); - const { Bootstrap } = require(COMMONJS_URI + "/sdk/addon/bootstrap.js"); - var { startup, shutdown, install, uninstall } = new Bootstrap(rootURI); - `; - - return files; -} - -add_task(function* test_sdk_hybrid_addon_with_jpm_module_loader() { - function backgroundScript() { - browser.runtime.sendMessage("background message", (reply) => { - browser.test.assertEq("sdk received message: background message", reply, - "Got the expected reply from the SDK context"); - browser.test.notifyPass("sdk.webext-api.onmessage"); - }); - } - - async function sdkMainScript() { - /* globals require */ - const webext = require("sdk/webextension"); - let {browser} = await webext.startup(); - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - sendReply(`sdk received message: ${msg}`); - }); - } - - let id = "fake@sdk.hybrid.addon"; - let extension = ExtensionTestUtils.loadExtension({ - useAddonManager: "temporary", - files: generateClassicExtensionFiles({ - id, - files: { - "index.js": sdkMainScript, - "webextension/manifest.json": { - name: "embedded webextension name", - manifest_version: 2, - version: "0.1.0", - background: { - scripts: ["bg.js"], - }, - }, - "webextension/bg.js": backgroundScript, - }, - }), - }, id); - - extension.startup(); - - yield extension.awaitFinish("sdk.webext-api.onmessage"); - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_idle.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_idle.html deleted file mode 100644 index 3c3063e67..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_idle.html +++ /dev/null @@ -1,64 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -const idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService); - -add_task(function* testWithRealIdleService() { - function background() { - browser.test.onMessage.addListener((msg, ...args) => { - let detectionInterval = args[0]; - if (msg == "addListener") { - browser.idle.queryState(detectionInterval).then(status => { - browser.test.assertEq("active", status, "Idle status is active"); - }); - browser.idle.setDetectionInterval(detectionInterval); - browser.idle.onStateChanged.addListener(newState => { - browser.test.assertEq("idle", newState, "listener fired with the expected state"); - browser.test.sendMessage("listenerFired"); - }); - } else if (msg == "checkState") { - browser.idle.queryState(detectionInterval).then(status => { - browser.test.assertEq("idle", status, "Idle status is idle"); - browser.test.notifyPass("idle"); - }); - } - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["idle"], - }, - }); - - yield extension.startup(); - let idleTime = idleService.idleTime; - let detectionInterval = Math.max(Math.ceil(idleTime / 1000) + 2, 15); - info(`idleTime: ${idleTime}, detectionInterval: ${detectionInterval}`); - extension.sendMessage("addListener", detectionInterval); - info("Listener added"); - yield extension.awaitMessage("listenerFired"); - info("Listener fired"); - extension.sendMessage("checkState", detectionInterval); - yield extension.awaitFinish("idle"); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html deleted file mode 100644 index e3098e6b1..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html +++ /dev/null @@ -1,50 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://testing-common/TestUtils.jsm"); - -const {GlobalManager} = Cu.import("resource://gre/modules/Extension.jsm"); - -/* eslint-disable mozilla/balanced-listeners */ - -add_task(function* testShutdownCleanup() { - is(GlobalManager.initialized, false, - "GlobalManager start as not initialized"); - - let extension = ExtensionTestUtils.loadExtension({ - background: function() { - browser.test.notifyPass("background page loaded"); - }, - }); - - yield extension.startup(); - - yield extension.awaitFinish("background page loaded"); - - is(GlobalManager.initialized, true, - "GlobalManager has been initialized once an extension is started"); - - yield extension.unload(); - - is(GlobalManager.initialized, false, - "GlobalManager has been uninitialized once all the webextensions have been stopped"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_storage_cleanup.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_storage_cleanup.html deleted file mode 100644 index 010769500..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_storage_cleanup.html +++ /dev/null @@ -1,164 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -// Test that storage used by a webextension (through localStorage, -// indexedDB, and browser.storage.local) gets cleaned up when the -// extension is uninstalled. -add_task(function* test_uninstall() { - function writeData() { - localStorage.setItem("hello", "world"); - - let idbPromise = new Promise((resolve, reject) => { - let req = indexedDB.open("test"); - req.onerror = e => { - reject(new Error(`indexedDB open failed with ${e.errorCode}`)); - }; - - req.onupgradeneeded = e => { - let db = e.target.result; - db.createObjectStore("store", {keyPath: "name"}); - }; - - req.onsuccess = e => { - let db = e.target.result; - let transaction = db.transaction("store", "readwrite"); - let addreq = transaction.objectStore("store") - .add({name: "hello", value: "world"}); - addreq.onerror = addreqError => { - reject(new Error(`add to indexedDB failed with ${addreqError.errorCode}`)); - }; - addreq.onsuccess = () => { - resolve(); - }; - }; - }); - - let browserStoragePromise = browser.storage.local.set({hello: "world"}); - - Promise.all([idbPromise, browserStoragePromise]).then(() => { - browser.test.sendMessage("finished"); - }); - } - - function readData() { - let matchLocalStorage = (localStorage.getItem("hello") == "world"); - - let idbPromise = new Promise((resolve, reject) => { - let req = indexedDB.open("test"); - req.onerror = e => { - reject(new Error(`indexedDB open failed with ${e.errorCode}`)); - }; - - req.onupgradeneeded = e => { - // no database, data is not present - resolve(false); - }; - - req.onsuccess = e => { - let db = e.target.result; - let transaction = db.transaction("store", "readwrite"); - let addreq = transaction.objectStore("store").get("hello"); - addreq.onerror = addreqError => { - reject(new Error(`read from indexedDB failed with ${addreqError.errorCode}`)); - }; - addreq.onsuccess = () => { - let match = (addreq.result.value == "world"); - resolve(match); - }; - }; - }); - - let browserStoragePromise = browser.storage.local.get("hello").then(result => { - return (Object.keys(result).length == 1 && result.hello == "world"); - }); - - Promise.all([idbPromise, browserStoragePromise]) - .then(([matchIDB, matchBrowserStorage]) => { - let result = {matchLocalStorage, matchIDB, matchBrowserStorage}; - browser.test.sendMessage("results", result); - }); - } - - const ID = "storage.cleanup@tests.mozilla.org"; - - // Use a test-only pref to leave the addonid->uuid mapping around after - // uninstall so that we can re-attach to the same storage. Also set - // the pref to prevent cleaning up storage on uninstall so we can test - // that the "keep uuid" logic works correctly. Do the storage flag in - // a separate prefEnv so we can pop it below, leaving the uuid flag set. - yield SpecialPowers.pushPrefEnv({ - set: [["extensions.webextensions.keepUuidOnUninstall", true]], - }); - yield SpecialPowers.pushPrefEnv({ - set: [["extensions.webextensions.keepStorageOnUninstall", true]], - }); - - let extension = ExtensionTestUtils.loadExtension({ - background: writeData, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["storage"], - }, - useAddonManager: "temporary", - }); - - yield extension.startup(); - yield extension.awaitMessage("finished"); - yield extension.unload(); - - // Check that we can still see data we wrote to storage but clear the - // "leave storage" flag so our storaged gets cleared on uninstall. - // This effectively tests the keepUuidOnUninstall logic, which ensures - // that when we read storage again and check that it is cleared, that - // it is actually a meaningful test! - yield SpecialPowers.popPrefEnv(); - extension = ExtensionTestUtils.loadExtension({ - background: readData, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["storage"], - }, - useAddonManager: "temporary", - }); - - yield extension.startup(); - let results = yield extension.awaitMessage("results"); - is(results.matchLocalStorage, true, "localStorage data is still present"); - is(results.matchIDB, true, "indexedDB data is still present"); - is(results.matchBrowserStorage, true, "browser.storage.local data is still present"); - - yield extension.unload(); - - // Read again. This time, our data should be gone. - extension = ExtensionTestUtils.loadExtension({ - background: readData, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["storage"], - }, - useAddonManager: "temporary", - }); - - yield extension.startup(); - results = yield extension.awaitMessage("results"); - is(results.matchLocalStorage, false, "localStorage data was cleared"); - is(results.matchIDB, false, "indexedDB data was cleared"); - is(results.matchBrowserStorage, false, "browser.storage.local data was cleared"); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_trustworthy_origin.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_trustworthy_origin.html deleted file mode 100644 index 573c08806..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_trustworthy_origin.html +++ /dev/null @@ -1,53 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/** - * This test is asserting that moz-extension: URLs are recognized as trustworthy local origins - */ - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager", - "@mozilla.org/contentsecuritymanager;1", - "nsIContentSecurityManager"); - -add_task(function* () { - function background() { - browser.test.sendMessage("ready", browser.runtime.getURL("/test.html")); - } - - let extensionData = { - background, - files: { - "test.html": `<html><head></head><body></body></html>`, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let url = yield extension.awaitMessage("ready"); - - let uri = NetUtil.newURI(url); - let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); - is(gContentSecurityManager.isOriginPotentiallyTrustworthy(principal), true); - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html deleted file mode 100644 index 768eb31fd..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html +++ /dev/null @@ -1,83 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for simple WebExtension</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* webnav_unresolved_uri_on_expected_URI_scheme() { - function background() { - let checkURLs; - - browser.webNavigation.onCompleted.addListener(async msg => { - if (checkURLs.length > 0) { - let expectedURL = checkURLs.shift(); - browser.test.assertEq(expectedURL, msg.url, "Got the expected URL"); - await browser.tabs.remove(msg.tabId); - browser.test.sendMessage("next"); - } - }); - - browser.test.onMessage.addListener((name, urls) => { - if (name == "checkURLs") { - checkURLs = urls; - } - }); - - browser.test.sendMessage("ready", browser.runtime.getURL("/tab.html")); - } - - let extensionData = { - manifest: { - permissions: [ - "webNavigation", - ], - }, - background, - files: { - "tab.html": `<!DOCTYPE html> - <html> - <head> - <meta charset="utf-8"> - </head> - </html> - `, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - let checkURLs = [ - "resource://gre/modules/Services.jsm", - "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", - "about:mozilla", - ]; - - let tabURL = yield extension.awaitMessage("ready"); - checkURLs.push(tabURL); - - extension.sendMessage("checkURLs", checkURLs); - - for (let url of checkURLs) { - window.open(url); - yield extension.awaitMessage("next"); - } - - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_webrequest_background_events.html b/toolkit/components/webextensions/test/mochitest/test_chrome_ext_webrequest_background_events.html deleted file mode 100644 index a13c4d475..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_ext_webrequest_background_events.html +++ /dev/null @@ -1,96 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for simple WebExtension</title> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -Cu.import(SimpleTest.getTestFileURL("webrequest_test.jsm")); -let {testFetch, testXHR} = webrequest_test; - -// Here we test that any requests originating from a system principal are not -// accessible through WebRequest. text_ext_webrequest_background_events tests -// non-system principal requests. - -let testExtension = { - manifest: { - permissions: [ - "webRequest", - "<all_urls>", - ], - }, - background() { - let eventNames = [ - "onBeforeRequest", - "onBeforeSendHeaders", - "onSendHeaders", - "onHeadersReceived", - "onResponseStarted", - "onCompleted", - ]; - - function listener(name, details) { - // If we get anything, we failed. Removing the system principal check - // in ext-webrequest triggers this failure. - browser.test.fail(`recieved ${name}`); - } - - for (let name of eventNames) { - browser.webRequest[name].addListener( - listener.bind(null, name), - {urls: ["https://example.com/*"]} - ); - } - }, -}; - -add_task(function* test_webRequest_chromeworker_events() { - let extension = ExtensionTestUtils.loadExtension(testExtension); - yield extension.startup(); - yield new Promise(resolve => { - let worker = new ChromeWorker("webrequest_chromeworker.js"); - worker.onmessage = event => { - ok("chrome worker fetch finished"); - resolve(); - }; - worker.postMessage("go"); - }); - yield extension.unload(); -}); - -add_task(function* test_webRequest_chromepage_events() { - let extension = ExtensionTestUtils.loadExtension(testExtension); - yield extension.startup(); - yield new Promise(resolve => { - fetch("https://example.com/example.txt").then(() => { - ok("test page loaded"); - resolve(); - }); - }); - yield extension.unload(); -}); - -add_task(function* test_webRequest_jsm_events() { - let extension = ExtensionTestUtils.loadExtension(testExtension); - yield extension.startup(); - yield testFetch("https://example.com/example.txt").then(() => { - ok("fetch page loaded"); - }); - yield testXHR("https://example.com/example.txt").then(() => { - ok("xhr page loaded"); - }); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_chrome_native_messaging_paths.html b/toolkit/components/webextensions/test/mochitest/test_chrome_native_messaging_paths.html deleted file mode 100644 index 29a148063..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_chrome_native_messaging_paths.html +++ /dev/null @@ -1,61 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/* global OS */ - -Cu.import("resource://gre/modules/osfile.jsm"); -Cu.import("resource://gre/modules/AppConstants.jsm"); - -// Test that the default paths searched for native host manifests -// are the ones we expect. -add_task(function* test_default_paths() { - let expectUser, expectGlobal; - switch (AppConstants.platform) { - case "macosx": { - expectUser = OS.Path.join(OS.Constants.Path.homeDir, - "Library/Application Support/Mozilla/NativeMessagingHosts"); - expectGlobal = "/Library/Application Support/Mozilla/NativeMessagingHosts"; - - break; - } - - case "linux": { - expectUser = OS.Path.join(OS.Constants.Path.homeDir, ".mozilla/native-messaging-hosts"); - - const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib"; - expectGlobal = OS.Path.join("/usr", libdir, "mozilla/native-messaging-hosts"); - break; - } - - default: - // Fixed filesystem paths are only defined for MacOS and Linux, - // there's nothing to test on other platforms. - ok(false, `This test does not apply on ${AppConstants.platform}`); - break; - } - - let userDir = Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path; - is(userDir, expectUser, "user-specific native messaging directory is correct"); - - let globalDir = Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path; - is(globalDir, expectGlobal, "system-wide native messaing directory is correct"); -}); - -</script> - -</body> -</html> - diff --git a/toolkit/components/webextensions/test/mochitest/test_clipboard.html b/toolkit/components/webextensions/test/mochitest/test_clipboard.html deleted file mode 100644 index 900ee5f10..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_clipboard.html +++ /dev/null @@ -1,140 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>clipboard permission test</title> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <script src="/tests/SimpleTest/SpawnTask.js"></script> - <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script src="head.js"></script> - <link rel="stylesheet" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -function doCopy(txt) { - let field = document.createElement("textarea"); - document.body.appendChild(field); - field.value = txt; - field.select(); - return document.execCommand("copy"); -} - -add_task(function* no_permission_deny_copy() { - function backgroundScript() { - browser.test.assertEq(false, doCopy("whatever"), - "copy should be denied without permission"); - browser.test.sendMessage("ready"); - } - let extensionData = { - background: `${doCopy};(${backgroundScript})();`, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitMessage("ready"); - - yield extension.unload(); -}); - -/** Selecting text in a bg page is not possible, skip test until it's fixed. -add_task(function* with_permission_allow_copy() { - function backgroundScript() { - browser.test.onMessage.addListener(txt => { - browser.test.assertEq(true, doCopy(txt), - "copy should be allowed with permission"); - }); - browser.test.sendMessage("ready"); - } - let extensionData = { - background: `${doCopy};(${backgroundScript})();`, - manifest: { - permissions: [ - "clipboardWrite", - ], - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitMessage("ready"); - - const DUMMY_STR = "dummy string to copy"; - yield new Promise(resolve => { - SimpleTest.waitForClipboard(DUMMY_STR, () => { - extension.sendMessage(DUMMY_STR); - }, resolve, resolve); - }); - - yield extension.unload(); -}); */ - -add_task(function* content_script_no_permission_deny_copy() { - function contentScript() { - browser.test.assertEq(false, doCopy("whatever"), - "copy should be denied without permission"); - browser.test.sendMessage("ready"); - } - let extensionData = { - manifest: { - content_scripts: [{ - js: ["contentscript.js"], - matches: ["http://mochi.test/*/file_sample.html"], - }], - }, - files: { - "contentscript.js": `${doCopy};(${contentScript})();`, - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - yield extension.awaitMessage("ready"); - win.close(); - - yield extension.unload(); -}); - -add_task(function* content_script_with_permission_allow_copy() { - function contentScript() { - browser.test.onMessage.addListener(txt => { - browser.test.assertEq(true, doCopy(txt), - "copy should be allowed with permission"); - }); - browser.test.sendMessage("ready"); - } - let extensionData = { - manifest: { - content_scripts: [{ - js: ["contentscript.js"], - matches: ["http://mochi.test/*/file_sample.html"], - }], - permissions: [ - "clipboardWrite", - ], - }, - files: { - "contentscript.js": `${doCopy};(${contentScript})();`, - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - yield extension.awaitMessage("ready"); - - const DUMMY_STR = "dummy string to copy in content script"; - yield new Promise(resolve => { - SimpleTest.waitForClipboard(DUMMY_STR, () => { - extension.sendMessage(DUMMY_STR); - }, resolve, resolve); - }); - - win.close(); - - yield extension.unload(); -}); -</script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_all_apis.js b/toolkit/components/webextensions/test/mochitest/test_ext_all_apis.js deleted file mode 100644 index 25d04b36b..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_all_apis.js +++ /dev/null @@ -1,160 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -// Tests whether not too many APIs are visible by default. -// This file is used by test_ext_all_apis.html in browser/ and mobile/android/, -// which may modify the following variables to add or remove expected APIs. -/* globals expectedContentApisTargetSpecific */ -/* globals expectedBackgroundApisTargetSpecific */ - -// Generates a list of expectations. -function generateExpectations(list) { - return list.reduce((allApis, path) => { - return allApis.concat(`browser.${path}`, `chrome.${path}`); - }, []).sort(); -} - -let expectedCommonApis = [ - "extension.getURL", - "extension.inIncognitoContext", - "extension.lastError", - "i18n.detectLanguage", - "i18n.getAcceptLanguages", - "i18n.getMessage", - "i18n.getUILanguage", - "runtime.OnInstalledReason", - "runtime.OnRestartRequiredReason", - "runtime.PlatformArch", - "runtime.PlatformOs", - "runtime.RequestUpdateCheckStatus", - "runtime.getManifest", - "runtime.connect", - "runtime.getURL", - "runtime.id", - "runtime.lastError", - "runtime.onConnect", - "runtime.onMessage", - "runtime.sendMessage", - // If you want to add a new powerful test API, please see bug 1287233. - "test.assertEq", - "test.assertFalse", - "test.assertRejects", - "test.assertThrows", - "test.assertTrue", - "test.fail", - "test.log", - "test.notifyFail", - "test.notifyPass", - "test.onMessage", - "test.sendMessage", - "test.succeed", -]; - -let expectedContentApis = [ - ...expectedCommonApis, - ...expectedContentApisTargetSpecific, -]; - -let expectedBackgroundApis = [ - ...expectedCommonApis, - ...expectedBackgroundApisTargetSpecific, - "extension.ViewType", - "extension.getBackgroundPage", - "extension.getViews", - "extension.isAllowedFileSchemeAccess", - "extension.isAllowedIncognitoAccess", - // Note: extensionTypes is not visible in Chrome. - "extensionTypes.ImageFormat", - "extensionTypes.RunAt", - "management.ExtensionDisabledReason", - "management.ExtensionInstallType", - "management.ExtensionType", - "management.getSelf", - "management.uninstallSelf", - "runtime.getBackgroundPage", - "runtime.getBrowserInfo", - "runtime.getPlatformInfo", - "runtime.onConnectExternal", - "runtime.onInstalled", - "runtime.onMessageExternal", - "runtime.onStartup", - "runtime.onUpdateAvailable", - "runtime.openOptionsPage", - "runtime.reload", - "runtime.setUninstallURL", -]; - -function sendAllApis() { - function isEvent(key, val) { - if (!/^on[A-Z]/.test(key)) { - return false; - } - let eventKeys = []; - for (let prop in val) { - eventKeys.push(prop); - } - eventKeys = eventKeys.sort().join(); - return eventKeys === "addListener,hasListener,removeListener"; - } - function mayRecurse(key, val) { - if (Object.keys(val).filter(k => !/^[A-Z\-0-9_]+$/.test(k)).length === 0) { - // Don't recurse on constants and empty objects. - return false; - } - return !isEvent(key, val); - } - - let results = []; - function diveDeeper(path, obj) { - for (let key in obj) { - let val = obj[key]; - if (typeof val == "object" && val !== null && mayRecurse(key, val)) { - diveDeeper(`${path}.${key}`, val); - } else if (val !== undefined) { - results.push(`${path}.${key}`); - } - } - } - diveDeeper("browser", browser); - diveDeeper("chrome", chrome); - browser.test.sendMessage("allApis", results.sort()); -} - -add_task(function* test_enumerate_content_script_apis() { - let extensionData = { - manifest: { - content_scripts: [{ - matches: ["http://mochi.test/*/file_sample.html"], - js: ["contentscript.js"], - run_at: "document_start", - }], - }, - files: { - "contentscript.js": sendAllApis, - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - let actualApis = yield extension.awaitMessage("allApis"); - win.close(); - let expectedApis = generateExpectations(expectedContentApis); - isDeeply(actualApis, expectedApis, "content script APIs"); - - yield extension.unload(); -}); - -add_task(function* test_enumerate_background_script_apis() { - let extensionData = { - background: sendAllApis, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - let actualApis = yield extension.awaitMessage("allApis"); - let expectedApis = generateExpectations(expectedBackgroundApis); - isDeeply(actualApis, expectedApis, "background script APIs"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_background_api_injection.html b/toolkit/components/webextensions/test/mochitest/test_ext_background_api_injection.html deleted file mode 100644 index f43a59f81..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_background_api_injection.html +++ /dev/null @@ -1,46 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for privilege escalation into content pages</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* testBackgroundWindow() { - let extension = ExtensionTestUtils.loadExtension({ - background: function() { - const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; - - browser.test.log("background script executed"); - window.location = `${BASE}/file_privilege_escalation.html`; - }, - }); - - let awaitConsole = new Promise(resolve => { - let chromeScript = SpecialPowers.loadChromeScript( - SimpleTest.getTestFileURL("file_ext_test_api_injection.js")); - - chromeScript.addMessageListener("console-message", resolve); - }); - - yield extension.startup(); - - let message = yield awaitConsole; - - ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"), - "Document does not have `browser` APIs."); - - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_background_canvas.html b/toolkit/components/webextensions/test/mochitest/test_ext_background_canvas.html deleted file mode 100644 index bff7190cb..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_background_canvas.html +++ /dev/null @@ -1,47 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for background page canvas rendering</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_background_canvas() { - function background() { - try { - let canvas = document.createElement("canvas"); - - let context = canvas.getContext("2d"); - - // This ensures that we have a working PresShell, and can successfully - // calculate font metrics. - context.font = "8pt fixed"; - - browser.test.notifyPass("background-canvas"); - } catch (e) { - browser.test.fail(`Error: ${e} :: ${e.stack}`); - browser.test.notifyFail("background-canvas"); - } - } - - let extensionData = { - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - yield extension.awaitFinish("background-canvas"); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_background_generated_url.html b/toolkit/components/webextensions/test/mochitest/test_ext_background_generated_url.html deleted file mode 100644 index f4fcf3d34..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_background_generated_url.html +++ /dev/null @@ -1,47 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test _generated_background_page.html</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -add_task(function* test_url_of_generated_background_page() { - function backgroundScript() { - const EXPECTED_URL = browser.runtime.getURL("/_generated_background_page.html"); - browser.test.assertEq(EXPECTED_URL, location.href); - browser.test.sendMessage("script done", EXPECTED_URL); - } - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - background: { - scripts: ["bg.js"], - }, - web_accessible_resources: ["_generated_background_page.html"], - }, - files: { - "bg.js": backgroundScript, - }, - }); - - yield extension.startup(); - const EXPECTED_URL = yield extension.awaitMessage("script done"); - - let win = window.open(EXPECTED_URL); - ok(win, "Should open new tab at URL: " + EXPECTED_URL); - yield extension.awaitMessage("script done"); - win.close(); - - yield extension.unload(); -}); - -</script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_background_teardown.html b/toolkit/components/webextensions/test/mochitest/test_ext_background_teardown.html deleted file mode 100644 index bb6b2e970..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_background_teardown.html +++ /dev/null @@ -1,76 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for background script teardown</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -add_task(function* test_background_reload_and_unload() { - function background() { - browser.test.onMessage.addListener(msg => { - browser.test.assertEq("reload-background", msg); - location.reload(); - }); - browser.test.sendMessage("background-url", location.href); - } - - let chromeScript = SpecialPowers.loadChromeScript( - SimpleTest.getTestFileURL("file_teardown_test.js")); - yield chromeScript.promiseOneMessage("chromescript-startup"); - - let extensionData = { - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - function* getContextEvents() { - chromeScript.sendAsyncMessage("get-context-events"); - let contextEvents = yield chromeScript.promiseOneMessage("context-events"); - return contextEvents.filter(event => event.extensionId == extension.id); - } - yield extension.startup(); - let backgroundUrl = yield extension.awaitMessage("background-url"); - - let contextEvents = yield* getContextEvents(); - is(contextEvents.length, 1, - "ExtensionContext state change after loading an extension"); - is(contextEvents[0].eventType, "load"); - is(contextEvents[0].url, backgroundUrl, - "The ExtensionContext should be the background page"); - - extension.sendMessage("reload-background"); - yield extension.awaitMessage("background-url"); - - contextEvents = yield* getContextEvents(); - is(contextEvents.length, 2, - "ExtensionContext state changes after reloading the background page"); - is(contextEvents[0].eventType, "unload", - "Unload ExtensionContext of background page"); - is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background"); - is(contextEvents[1].eventType, "load", - "Create new ExtensionContext for background page"); - is(contextEvents[1].url, backgroundUrl, "ExtensionContext URL = background"); - yield extension.unload(); - - contextEvents = yield* getContextEvents(); - is(contextEvents.length, 1, - "ExtensionContext state change after unloading the extension"); - is(contextEvents[0].eventType, "unload", - "Unload ExtensionContext for background page after extension unloads"); - is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background"); - - chromeScript.sendAsyncMessage("cleanup"); - chromeScript.destroy(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_content_security_policy.html b/toolkit/components/webextensions/test/mochitest/test_ext_content_security_policy.html deleted file mode 100644 index a36f29563..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_content_security_policy.html +++ /dev/null @@ -1,162 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension CSP test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/** - * Tests that content security policies for an add-on are actually applied to * - * documents that belong to it. This tests both the base policies and add-on - * specific policies, and ensures that the parsed policies applied to the - * document's principal match what was specified in the policy string. - * - * @param {object} [customCSP] - */ -function* testPolicy(customCSP = null) { - let baseURL; - - let baseCSP = { - "object-src": ["blob:", "filesystem:", "https://*", "moz-extension:", "'self'"], - "script-src": ["'unsafe-eval'", "'unsafe-inline'", "blob:", "filesystem:", "https://*", "moz-extension:", "'self'"], - }; - - let addonCSP = { - "object-src": ["'self'"], - "script-src": ["'self'"], - }; - - let content_security_policy = null; - - if (customCSP) { - for (let key of Object.keys(customCSP)) { - addonCSP[key] = customCSP[key].split(/\s+/); - } - - content_security_policy = Object.keys(customCSP) - .map(key => `${key} ${customCSP[key]}`) - .join("; "); - } - - - function filterSelf(sources) { - return sources.map(src => src == "'self'" ? baseURL : src); - } - - function checkSource(name, policy, expected) { - is(JSON.stringify(policy[name].sort()), - JSON.stringify(filterSelf(expected[name]).sort()), - `Expected value for ${name}`); - } - - function checkCSP(csp, location) { - let policies = csp["csp-policies"]; - - info(`Base policy for ${location}`); - - is(policies[0]["report-only"], false, "Policy is not report-only"); - checkSource("object-src", policies[0], baseCSP); - checkSource("script-src", policies[0], baseCSP); - - info(`Add-on policy for ${location}`); - - is(policies[1]["report-only"], false, "Policy is not report-only"); - checkSource("object-src", policies[1], addonCSP); - checkSource("script-src", policies[1], addonCSP); - } - - - function getCSP(window) { - let {cspJSON} = SpecialPowers.Cu.getObjectPrincipal(window); - return JSON.parse(cspJSON); - } - - function background(getCSPFn) { - browser.test.sendMessage("base-url", browser.extension.getURL("").replace(/\/$/, "")); - - browser.test.sendMessage("background-csp", getCSPFn(window)); - } - - function tabScript(getCSPFn) { - browser.test.sendMessage("tab-csp", getCSPFn(window)); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${background})(${getCSP})`, - - files: { - "tab.html": `<html><head><meta charset="utf-8"> - <script src="tab.js"></${"script"}></head></html>`, - - "tab.js": `(${tabScript})(${getCSP})`, - - "content.html": `<html><head><meta charset="utf-8"></head></html>`, - }, - - manifest: { - content_security_policy, - - web_accessible_resources: ["content.html", "tab.html"], - }, - }); - - - info(`Testing CSP for policy: ${content_security_policy}`); - - yield extension.startup(); - - baseURL = yield extension.awaitMessage("base-url"); - - - let win1 = window.open(`${baseURL}/tab.html`); - - let frame = document.createElement("iframe"); - frame.src = `${baseURL}/content.html`; - document.body.appendChild(frame); - - yield new Promise(resolve => { - frame.onload = resolve; - }); - - - let backgroundCSP = yield extension.awaitMessage("background-csp"); - checkCSP(backgroundCSP, "background page"); - - let tabCSP = yield extension.awaitMessage("tab-csp"); - checkCSP(tabCSP, "tab page"); - - let contentCSP = getCSP(frame.contentWindow); - checkCSP(contentCSP, "content frame"); - - - win1.close(); - frame.remove(); - - yield extension.unload(); -} - -add_task(function* testCSP() { - yield testPolicy(null); - - let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='"; - - yield testPolicy({ - "object-src": "'self' https://*.example.com", - "script-src": `'self' https://*.example.com 'unsafe-eval' ${hash}`, - }); - - yield testPolicy({ - "object-src": "'none'", - "script-src": `'self'`, - }); -}); -</script> -</body> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript.html deleted file mode 100644 index 39f1bfabd..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript.html +++ /dev/null @@ -1,116 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript() { - function background() { - browser.runtime.onMessage.addListener(([msg, expectedStates, readyState], sender) => { - if (msg == "chrome-namespace-ok") { - browser.test.sendMessage(msg); - return; - } - - browser.test.assertEq("script-run", msg, "message type is correct"); - browser.test.assertTrue(expectedStates.includes(readyState), - `readyState "${readyState}" is one of [${expectedStates}]`); - browser.test.sendMessage("script-run-" + expectedStates[0]); - }); - } - - function contentScriptStart() { - browser.runtime.sendMessage(["script-run", ["loading"], document.readyState]); - } - function contentScriptEnd() { - browser.runtime.sendMessage(["script-run", ["interactive", "complete"], document.readyState]); - } - function contentScriptIdle() { - browser.runtime.sendMessage(["script-run", ["complete"], document.readyState]); - } - - function contentScript() { - let manifest = browser.runtime.getManifest(); - void manifest.applications.gecko.id; - chrome.runtime.sendMessage(["chrome-namespace-ok"]); - } - - let extensionData = { - manifest: { - applications: {gecko: {id: "contentscript@tests.mozilla.org"}}, - content_scripts: [ - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script_start.js"], - "run_at": "document_start", - }, - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script_end.js"], - "run_at": "document_end", - }, - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script_idle.js"], - "run_at": "document_idle", - }, - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }, - ], - }, - background, - - files: { - "content_script_start.js": contentScriptStart, - "content_script_end.js": contentScriptEnd, - "content_script_idle.js": contentScriptIdle, - "content_script.js": contentScript, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - let loadingCount = 0; - let interactiveCount = 0; - let completeCount = 0; - extension.onMessage("script-run-loading", () => { loadingCount++; }); - extension.onMessage("script-run-interactive", () => { interactiveCount++; }); - - let completePromise = new Promise(resolve => { - extension.onMessage("script-run-complete", () => { completeCount++; resolve(); }); - }); - - let chromeNamespacePromise = extension.awaitMessage("chrome-namespace-ok"); - - yield extension.startup(); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), completePromise, chromeNamespacePromise]); - info("test page loaded"); - - win.close(); - - is(loadingCount, 1, "document_start script ran exactly once"); - is(interactiveCount, 1, "document_end script ran exactly once"); - is(completeCount, 1, "document_idle script ran exactly once"); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_about_blank.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_about_blank.html deleted file mode 100644 index 3766678e7..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_about_blank.html +++ /dev/null @@ -1,117 +0,0 @@ -<!doctype html> -<html> -<head> - <title>Test content script match_about_blank option</title> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript_about_blank() { - const manifest = { - content_scripts: [ - { - match_about_blank: true, - matches: ["http://mochi.test/*/file_with_about_blank.html", "http://example.com/*"], - all_frames: true, - css: ["all.css"], - js: ["all.js"], - }, { - matches: ["http://mochi.test/*/file_with_about_blank.html"], - css: ["mochi_without.css"], - js: ["mochi_without.js"], - all_frames: true, - }, { - match_about_blank: true, - matches: ["http://mochi.test/*/file_with_about_blank.html"], - css: ["mochi_with.css"], - js: ["mochi_with.js"], - all_frames: true, - }, - ], - }; - - const files = { - "all.js": function() { - browser.runtime.sendMessage("all"); - }, - "all.css": ` - body { color: red; } - `, - "mochi_without.js": function() { - browser.runtime.sendMessage("mochi_without"); - }, - "mochi_without.css": ` - body { background: yellow; } - `, - "mochi_with.js": function() { - browser.runtime.sendMessage("mochi_with"); - }, - "mochi_with.css": ` - body { text-align: right; } - `, - }; - - function background() { - browser.runtime.onMessage.addListener((script, {url}) => { - const kind = url.startsWith("about:") ? url : "top"; - browser.test.sendMessage("script", [script, kind, url]); - browser.test.sendMessage(`${script}:${kind}`); - }); - } - - const PATH = "tests/toolkit/components/extensions/test/mochitest/file_with_about_blank.html"; - const extension = ExtensionTestUtils.loadExtension({manifest, files, background}); - yield extension.startup(); - - let count = 0; - extension.onMessage("script", script => { - info(`script ran: ${script}`); - count++; - }); - - let win = window.open("http://example.com/" + PATH); - yield Promise.all([ - extension.awaitMessage("all:top"), - extension.awaitMessage("all:about:blank"), - extension.awaitMessage("all:about:srcdoc"), - ]); - is(count, 3, "exactly 3 scripts ran"); - win.close(); - - win = window.open("http://mochi.test:8888/" + PATH); - yield Promise.all([ - extension.awaitMessage("all:top"), - extension.awaitMessage("all:about:blank"), - extension.awaitMessage("all:about:srcdoc"), - extension.awaitMessage("mochi_without:top"), - extension.awaitMessage("mochi_with:top"), - extension.awaitMessage("mochi_with:about:blank"), - extension.awaitMessage("mochi_with:about:srcdoc"), - ]); - - let style = win.getComputedStyle(win.document.body); - is(style.color, "rgb(255, 0, 0)", "top window text color is red"); - is(style.backgroundColor, "rgb(255, 255, 0)", "top window background is yellow"); - is(style.textAlign, "right", "top window text is right-aligned"); - - let a_b = win.document.getElementById("a_b"); - style = a_b.contentWindow.getComputedStyle(a_b.contentDocument.body); - is(style.color, "rgb(255, 0, 0)", "about:blank iframe text color is red"); - is(style.backgroundColor, "transparent", "about:blank iframe background is transparent"); - is(style.textAlign, "right", "about:blank text is right-aligned"); - - is(count, 10, "exactly 7 more scripts ran"); - win.close(); - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_api_injection.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_api_injection.html deleted file mode 100644 index abf3d349f..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_api_injection.html +++ /dev/null @@ -1,88 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for privilege escalation into iframe with content script APIs</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it --> -<textarea id="test-asset"> - <!DOCTYPE HTML> - <html> - <head> - <meta charset="utf-8"> - <script type="text/javascript" src="./content_script_iframe.js"> - </script> - </head> - </html> -</textarea> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript_api_injection() { - function contentScript() { - let iframe = document.createElement("iframe"); - iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html")); - document.body.appendChild(iframe); - } - - function contentScriptIframe() { - const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; - window.location = `${BASE}/file_privilege_escalation.html`; - } - - let extensionData = { - manifest: { - content_scripts: [ - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }, - ], - "web_accessible_resources": [ - "content_script_iframe.html", - ], - }, - - files: { - "content_script.js": contentScript, - "content_script_iframe.js": contentScriptIframe, - "content_script_iframe.html": document.querySelector("#test-asset").textContent, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - let awaitConsole = new Promise(resolve => { - let chromeScript = SpecialPowers.loadChromeScript( - SimpleTest.getTestFileURL("file_ext_test_api_injection.js")); - - chromeScript.addMessageListener("console-message", resolve); - }); - - yield extension.startup(); - info("extension loaded"); - - let win = window.open("file_sample.html"); - - let message = yield awaitConsole; - - ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"), - "Document does not have `browser` APIs."); - - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_async_loading.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_async_loading.html deleted file mode 100644 index d78f7ce02..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_async_loading.html +++ /dev/null @@ -1,54 +0,0 @@ -<!doctype html> -<html> -<head> - <title>Test content script async loading</title> - <script src="/tests/SimpleTest/SpawnTask.js"></script> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<script> -"use strict"; - -add_task(function* test_async_loading() { - const adder = `(function add(a = 1) { this.count += a; })();\n`; - const extension = ExtensionTestUtils.loadExtension({ - manifest: { - content_scripts: [{ - matches: ["https://example.org/"], - js: ["first.js", "second.js"], - }], - }, - files: { - "first.js": ` - this.count = 0; - ${adder.repeat(50000)}; // 2Mb - browser.test.assertEq(this.count, 50000, "A 50k line script"); - - this.order = (this.order || 0) + 1; - browser.test.sendMessage("first", this.order); - `, - "second.js": ` - this.order = (this.order || 0) + 1; - browser.test.sendMessage("second", this.order); - `, - }, - }); - - yield extension.startup(); - const win = window.open("https://example.org/"); - - const [first, second] = yield Promise.all([ - extension.awaitMessage("first"), - extension.awaitMessage("second"), - ]); - - is(first, 1, "first.js finished execution first."); - is(second, 2, "second.js finished execution second."); - - yield extension.unload(); - win.close(); -}); - -</script> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_context.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_context.html deleted file mode 100644 index 97b1645dd..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_context.html +++ /dev/null @@ -1,81 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script contexts</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/* eslint-disable mozilla/balanced-listeners */ - -add_task(function* test_contentscript_context() { - function contentScript() { - browser.test.sendMessage("content-script-ready"); - - window.addEventListener("pagehide", () => { - browser.test.sendMessage("content-script-hide"); - }, true); - window.addEventListener("pageshow", () => { - browser.test.sendMessage("content-script-show"); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - content_scripts: [{ - "matches": ["http://example.com/"], - "js": ["content_script.js"], - "run_at": "document_start", - }], - }, - - files: { - "content_script.js": contentScript, - }, - }); - - yield extension.startup(); - - let win = window.open("http://example.com/"); - yield extension.awaitMessage("content-script-ready"); - yield extension.awaitMessage("content-script-show"); - - // Get the content script context and check that it points to the correct window. - - let {DocumentManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {}); - let context = DocumentManager.getContentScriptContext(extension, win); - ok(context != null, "Got content script context"); - - is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct"); - - // Navigate so that the content page is hidden in the bfcache. - - win.location = "http://example.org/"; - yield extension.awaitMessage("content-script-hide"); - - is(context.contentWindow, null, "Context's contentWindow property is null"); - - // Navigate back so the content page is resurrected from the bfcache. - - SpecialPowers.wrap(win).history.back(); - yield extension.awaitMessage("content-script-show"); - - is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct"); - - win.close(); - - yield extension.awaitMessage("content-script-hide"); - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_create_iframe.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_create_iframe.html deleted file mode 100644 index 8aac3e213..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_create_iframe.html +++ /dev/null @@ -1,165 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it --> -<textarea id="test-asset"> - <!DOCTYPE HTML> - <html> - <head> - <meta charset="utf-8"> - <script type="text/javascript" src="content_script_iframe.js"></script> - </head> - </html> -</textarea> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript_create_iframe() { - function background() { - browser.runtime.onMessage.addListener((msg, sender) => { - let {name, availableAPIs, manifest, testGetManifest} = msg; - let hasExtTabsAPI = availableAPIs.indexOf("tabs") > 0; - let hasExtWindowsAPI = availableAPIs.indexOf("windows") > 0; - - browser.test.assertFalse(hasExtTabsAPI, "the created iframe should not be able to use privileged APIs (tabs)"); - browser.test.assertFalse(hasExtWindowsAPI, "the created iframe should not be able to use privileged APIs (windows)"); - - let {applications: {gecko: {id: expectedManifestGeckoId}}} = chrome.runtime.getManifest(); - let {applications: {gecko: {id: actualManifestGeckoId}}} = manifest; - - browser.test.assertEq(actualManifestGeckoId, expectedManifestGeckoId, - "the add-on manifest should be accessible from the created iframe" - ); - - let {applications: {gecko: {id: testGetManifestGeckoId}}} = testGetManifest; - - browser.test.assertEq(testGetManifestGeckoId, expectedManifestGeckoId, - "GET_MANIFEST() returns manifest data before extension unload" - ); - - browser.test.sendMessage(name); - }); - } - - function contentScript() { - let iframe = document.createElement("iframe"); - iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html")); - document.body.appendChild(iframe); - } - - function contentScriptIframe() { - window.GET_MANIFEST = browser.runtime.getManifest.bind(null); - - window.testGetManifestException = () => { - try { - window.GET_MANIFEST(); - } catch (exception) { - return String(exception); - } - }; - - let testGetManifest = window.GET_MANIFEST(); - - let manifest = browser.runtime.getManifest(); - let availableAPIs = Object.keys(browser); - - browser.runtime.sendMessage({ - name: "content-script-iframe-loaded", - availableAPIs, - manifest, - testGetManifest, - }); - } - - const ID = "contentscript@tests.mozilla.org"; - let extensionData = { - manifest: { - applications: {gecko: {id: ID}}, - content_scripts: [ - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }, - ], - web_accessible_resources: [ - "content_script_iframe.html", - ], - }, - - background, - - files: { - "content_script.js": contentScript, - "content_script_iframe.html": document.querySelector("#test-asset").textContent, - "content_script_iframe.js": contentScriptIframe, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - let contentScriptIframeCreatedPromise = new Promise(resolve => { - extension.onMessage("content-script-iframe-loaded", () => { resolve(); }); - }); - - yield extension.startup(); - info("extension loaded"); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), contentScriptIframeCreatedPromise]); - info("content script privileged iframe loaded and executed"); - - info("testing APIs availability once the extension is unloaded..."); - - let iframeWindow = SpecialPowers.wrap(win)[0]; - - ok(iframeWindow, "content script enabled iframe found"); - ok(/content_script_iframe\.html$/.test(iframeWindow.location), "the found iframe has the expected URL"); - - yield extension.unload(); - info("extension unloaded"); - - info("test content script APIs not accessible from the frame once the extension is unloaded"); - - let ww = SpecialPowers.Cu.waiveXrays(iframeWindow); - let isDeadWrapper = SpecialPowers.Cu.isDeadWrapper(ww.browser); - ok(!isDeadWrapper, "the API object should not be a dead object"); - - let manifest; - let manifestException; - - try { - manifest = ww.browser.runtime.getManifest(); - } catch (e) { - manifestException = e; - } - - ok(!manifest, "manifest should be undefined"); - - is(String(manifestException), "TypeError: ww.browser.runtime is undefined", - "expected exception received"); - - let getManifestException = ww.testGetManifestException(); - - is(getManifestException, "TypeError: can't access dead object", - "expected exception received"); - - win.close(); - - info("done"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_css.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_css.html deleted file mode 100644 index 5630a1d68..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_css.html +++ /dev/null @@ -1,48 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_content_script_css() { - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "css": ["content.css"], - }], - }, - - files: { - "content.css": "body { max-width: 42px; }", - }, - }); - - yield extension.startup(); - - let win = window.open("file_sample.html"); - yield waitForLoad(win); - - let style = win.getComputedStyle(win.document.body); - is(style.maxWidth, "42px", "Stylesheet correctly applied"); - - yield extension.unload(); - - style = win.getComputedStyle(win.document.body); - is(style.maxWidth, "none", "Stylesheet correctly removed"); - - win.close(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_devtools_metadata.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_devtools_metadata.html deleted file mode 100644 index 137a3cda4..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_devtools_metadata.html +++ /dev/null @@ -1,81 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for Sandbox metadata on WebExtensions ContentScripts</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript_devtools_sandbox_metadata() { - function contentScript() { - browser.runtime.sendMessage("contentScript.executed"); - } - - function background() { - browser.runtime.onMessage.addListener((msg) => { - if (msg == "contentScript.executed") { - browser.test.notifyPass("contentScript.executed"); - } - }); - } - - let extensionData = { - manifest: { - content_scripts: [ - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }, - ], - }, - - background, - files: { - "content_script.js": contentScript, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - let win = window.open("file_sample.html"); - - let innerWindowID = SpecialPowers.wrap(win) - .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) - .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils) - .currentInnerWindowID; - - yield extension.awaitFinish("contentScript.executed"); - - const {ExtensionContent} = SpecialPowers.Cu.import( - "resource://gre/modules/ExtensionContent.jsm", {} - ); - - let res = ExtensionContent.getContentScriptGlobalsForWindow(win); - is(res.length, 1, "Got the expected array of globals"); - let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {}; - - is(metadata.addonId, extension.id, "Got the expected addonId"); - is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id"); - - yield extension.unload(); - info("extension unloaded"); - - res = ExtensionContent.getContentScriptGlobalsForWindow(win); - is(res.length, 0, "No content scripts globals found once the extension is unloaded"); - - win.close(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_exporthelpers.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_exporthelpers.html deleted file mode 100644 index f3414901d..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_exporthelpers.html +++ /dev/null @@ -1,95 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -add_task(function* test_contentscript_exportHelpers() { - function contentScript() { - browser.test.assertTrue(typeof cloneInto === "function"); - browser.test.assertTrue(typeof createObjectIn === "function"); - browser.test.assertTrue(typeof exportFunction === "function"); - - /* globals exportFunction, precisePi, reportPi */ - let value = 3.14; - exportFunction(() => value, window, {defineAs: "precisePi"}); - - browser.test.assertEq("undefined", typeof precisePi, - "exportFunction should export to the page's scope only"); - - browser.test.assertEq("undefined", typeof window.precisePi, - "exportFunction should export to the page's scope only"); - - let results = []; - exportFunction(pi => results.push(pi), window, {defineAs: "reportPi"}); - - let s = document.createElement("script"); - s.textContent = `(${function() { - let result1 = "unknown 1"; - let result2 = "unknown 2"; - try { - result1 = precisePi(); - } catch (e) { - result1 = "err:" + e; - } - try { - result2 = window.precisePi(); - } catch (e) { - result2 = "err:" + e; - } - reportPi(result1); - reportPi(result2); - }})();`; - - document.documentElement.appendChild(s); - // Inline script ought to run synchronously. - - browser.test.assertEq(3.14, results[0], - "exportFunction on window should define a global function"); - browser.test.assertEq(3.14, results[1], - "exportFunction on window should export a property to window."); - - browser.test.assertEq(2, results.length, - "Expecting the number of results to match the number of method calls"); - - browser.test.notifyPass("export helper test completed"); - } - - let extensionData = { - manifest: { - content_scripts: [{ - js: ["contentscript.js"], - matches: ["http://mochi.test/*/file_sample.html"], - run_at: "document_start", - }], - }, - - files: { - "contentscript.js": contentScript, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - let win = window.open("file_sample.html"); - - yield extension.awaitFinish("export helper test completed"); - win.close(); - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_incognito.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_incognito.html deleted file mode 100644 index a2f38dce6..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_incognito.html +++ /dev/null @@ -1,89 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script private browsing ID</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript_incognito() { - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - content_scripts: [ - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - }, - ], - }, - - background() { - let windowId; - - browser.test.onMessage.addListener(([msg, url]) => { - if (msg === "open-window") { - browser.windows.create({url, incognito: true}).then(window => { - windowId = window.id; - }); - } else if (msg === "close-window") { - browser.windows.remove(windowId).then(() => { - browser.test.sendMessage("done"); - }); - } - }); - }, - - files: { - "content_script.js": async () => { - const COOKIE = "foo=florgheralzps"; - document.cookie = COOKIE; - - let url = new URL("return_headers.sjs", location.href); - - let responses = [ - new Promise(resolve => { - let xhr = new XMLHttpRequest(); - xhr.open("GET", url); - xhr.onload = () => resolve(JSON.parse(xhr.responseText)); - xhr.send(); - }), - - fetch(url, {credentials: "include"}).then(body => body.json()), - ]; - - try { - for (let response of await Promise.all(responses)) { - browser.test.assertEq(COOKIE, response.cookie, "Got expected cookie header"); - } - browser.test.notifyPass("cookies"); - } catch (e) { - browser.test.fail(`Error: ${e}`); - browser.test.notifyFail("cookies"); - } - }, - }, - }); - - yield extension.startup(); - - extension.sendMessage(["open-window", SimpleTest.getTestFileURL("file_sample.html")]); - - yield extension.awaitFinish("cookies"); - - extension.sendMessage(["close-window"]); - yield extension.awaitMessage("done"); - - yield extension.unload(); -}); -</script> - -</body> -</html> - diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_permission.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_permission.html deleted file mode 100644 index eaf815092..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_permission.html +++ /dev/null @@ -1,59 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript() { - function background() { - browser.test.onMessage.addListener(url => { - browser.tabs.create({url}).then(tab => { - return browser.tabs.executeScript(tab.id, {code: "true;"}) - .then(() => { - browser.test.sendMessage("executed", true); - browser.tabs.remove([tab.id]); - }, err => { - browser.test.sendMessage("executed", false); - browser.tabs.remove([tab.id]); - }); - }); - }); - } - - let extensionData = { - manifest: { - permissions: ["<all_urls>"], - }, - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - extension.sendMessage("https://example.com"); - let result = yield extension.awaitMessage("executed"); - is(result, true, "Content script can be run in a page without mozAddonManager"); - - yield SpecialPowers.pushPrefEnv({ - set: [["extensions.webapi.testing", true]], - }); - - extension.sendMessage("https://example.com"); - result = yield extension.awaitMessage("executed"); - is(result, false, "Content script cannot be run in a page with mozAddonManager"); - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_teardown.html b/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_teardown.html deleted file mode 100644 index 33a8c4ccc..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_contentscript_teardown.html +++ /dev/null @@ -1,96 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script teardown</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -add_task(function* test_contentscript_reload_and_unload() { - function contentScript() { - browser.test.sendMessage("contentscript-run"); - } - function background() { - let removedTabs = 0; - browser.tabs.onRemoved.addListener(() => { - browser.test.assertEq(1, ++removedTabs, - "Expected only one tab to be removed during the test"); - browser.test.sendMessage("tab-closed"); - }); - } - - let extensionData = { - background, - manifest: { - content_scripts: [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["contentscript.js"], - }], - }, - - files: { - "contentscript.js": contentScript, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let chromeScript = SpecialPowers.loadChromeScript( - SimpleTest.getTestFileURL("file_teardown_test.js")); - yield chromeScript.promiseOneMessage("chromescript-startup"); - function* getContextEvents() { - chromeScript.sendAsyncMessage("get-context-events"); - let contextEvents = yield chromeScript.promiseOneMessage("context-events"); - return contextEvents.filter(event => event.extensionId == extension.id); - } - - let win = window.open("file_sample.html"); - yield extension.awaitMessage("contentscript-run"); - let tabUrl = win.location.href; - - let contextEvents = yield* getContextEvents(); - is(contextEvents.length, 1, - "ExtensionContext state change after loading a content script"); - is(contextEvents[0].eventType, "load", - "Create ExtensionContext for content script"); - is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); - - let promiseReload = extension.awaitMessage("contentscript-run"); - win.location.reload(); - yield promiseReload; - contextEvents = yield* getContextEvents(); - is(contextEvents.length, 2, - "ExtensionContext state changes after reloading a content script"); - is(contextEvents[0].eventType, "unload", "Unload old ExtensionContext"); - is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); - is(contextEvents[1].eventType, "load", - "Create new ExtensionContext for content script"); - is(contextEvents[1].url, tabUrl, "ExtensionContext URL = page"); - - let tabClosePromise = extension.awaitMessage("tab-closed"); - win.close(); - yield tabClosePromise; - - contextEvents = yield* getContextEvents(); - is(contextEvents.length, 1, - "ExtensionContext state change after unloading a content script"); - is(contextEvents[0].eventType, "unload", - "Unload ExtensionContext after closing the tab with the content script"); - is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page"); - - chromeScript.sendAsyncMessage("cleanup"); - chromeScript.destroy(); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_cookies.html b/toolkit/components/webextensions/test/mochitest/test_ext_cookies.html deleted file mode 100644 index d414a4e46..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_cookies.html +++ /dev/null @@ -1,234 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_cookies() { - async function background() { - function assertExpected(expected, cookie) { - for (let key of Object.keys(cookie)) { - browser.test.assertTrue(key in expected, `found property ${key}`); - browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`); - } - browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found"); - } - - const TEST_URL = "http://example.org/"; - const TEST_SECURE_URL = "https://example.org/"; - const THE_FUTURE = Date.now() + 5 * 60; - const TEST_PATH = "set_path"; - const TEST_URL_WITH_PATH = TEST_URL + TEST_PATH; - const TEST_COOKIE_PATH = `/${TEST_PATH}`; - const STORE_ID = "firefox-default"; - const PRIVATE_STORE_ID = "firefox-private"; - - let expected = { - name: "name1", - value: "value1", - domain: "example.org", - hostOnly: true, - path: "/", - secure: false, - httpOnly: false, - session: false, - expirationDate: THE_FUTURE, - storeId: STORE_ID, - }; - - let cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", expirationDate: THE_FUTURE}); - assertExpected(expected, cookie); - - cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); - assertExpected(expected, cookie); - - let cookies = await browser.cookies.getAll({name: "name1"}); - browser.test.assertEq(cookies.length, 1, "one cookie found for matching name"); - assertExpected(expected, cookies[0]); - - cookies = await browser.cookies.getAll({domain: "example.org"}); - browser.test.assertEq(cookies.length, 1, "one cookie found for matching domain"); - assertExpected(expected, cookies[0]); - - cookies = await browser.cookies.getAll({domain: "example.net"}); - browser.test.assertEq(cookies.length, 0, "no cookies found for non-matching domain"); - - cookies = await browser.cookies.getAll({secure: false}); - browser.test.assertEq(cookies.length, 1, "one non-secure cookie found"); - assertExpected(expected, cookies[0]); - - cookies = await browser.cookies.getAll({secure: true}); - browser.test.assertEq(cookies.length, 0, "no secure cookies found"); - - cookies = await browser.cookies.getAll({storeId: STORE_ID}); - browser.test.assertEq(cookies.length, 1, "one cookie found for valid storeId"); - assertExpected(expected, cookies[0]); - - cookies = await browser.cookies.getAll({storeId: "invalid_id"}); - browser.test.assertEq(cookies.length, 0, "no cookies found for invalid storeId"); - - let details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); - assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details); - - cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); - browser.test.assertEq(null, cookie, "removed cookie not found"); - - let stores = await browser.cookies.getAllCookieStores(); - browser.test.assertEq(1, stores.length, "expected number of stores returned"); - browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned"); - browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store"); - - { - let privateWindow = await browser.windows.create({incognito: true}); - let stores = await browser.cookies.getAllCookieStores(); - - browser.test.assertEq(2, stores.length, "expected number of stores returned"); - browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned"); - browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store"); - browser.test.assertEq(PRIVATE_STORE_ID, stores[1].id, "expected private store id returned"); - browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for private store"); - - await browser.windows.remove(privateWindow.id); - } - - cookie = await browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE}); - browser.test.assertEq(false, cookie.hostOnly, "cookie is not a hostOnly cookie"); - - details = await browser.cookies.remove({url: TEST_URL, name: "name2"}); - assertExpected({url: TEST_URL, name: "name2", storeId: STORE_ID}, details); - - // Create a session cookie. - cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1"}); - browser.test.assertEq(true, cookie.session, "session cookie set"); - - cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); - browser.test.assertEq(true, cookie.session, "got session cookie"); - - cookies = await browser.cookies.getAll({session: true}); - browser.test.assertEq(cookies.length, 1, "one session cookie found"); - browser.test.assertEq(true, cookies[0].session, "found session cookie"); - - cookies = await browser.cookies.getAll({session: false}); - browser.test.assertEq(cookies.length, 0, "no non-session cookies found"); - - details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); - assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details); - - cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); - browser.test.assertEq(null, cookie, "removed cookie not found"); - - cookie = await browser.cookies.set({url: TEST_SECURE_URL, name: "name1", value: "value1", secure: true}); - browser.test.assertEq(true, cookie.secure, "secure cookie set"); - - cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"}); - browser.test.assertEq(true, cookie.session, "got secure cookie"); - - cookies = await browser.cookies.getAll({secure: true}); - browser.test.assertEq(cookies.length, 1, "one secure cookie found"); - browser.test.assertEq(true, cookies[0].secure, "found secure cookie"); - - cookies = await browser.cookies.getAll({secure: false}); - browser.test.assertEq(cookies.length, 0, "no non-secure cookies found"); - - details = await browser.cookies.remove({url: TEST_SECURE_URL, name: "name1"}); - assertExpected({url: TEST_SECURE_URL, name: "name1", storeId: STORE_ID}, details); - - cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"}); - browser.test.assertEq(null, cookie, "removed cookie not found"); - - cookie = await browser.cookies.set({url: TEST_URL_WITH_PATH, path: TEST_COOKIE_PATH, name: "name1", value: "value1", expirationDate: THE_FUTURE}); - browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "created cookie with path"); - - cookie = await browser.cookies.get({url: TEST_URL_WITH_PATH, name: "name1"}); - browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "got cookie with path"); - - cookies = await browser.cookies.getAll({path: TEST_COOKIE_PATH}); - browser.test.assertEq(cookies.length, 1, "one cookie with path found"); - browser.test.assertEq(TEST_COOKIE_PATH, cookies[0].path, "found cookie with path"); - - cookie = await browser.cookies.get({url: TEST_URL + "invalid_path", name: "name1"}); - browser.test.assertEq(null, cookie, "get with invalid path returns null"); - - cookies = await browser.cookies.getAll({path: "/invalid_path"}); - browser.test.assertEq(cookies.length, 0, "getAll with invalid path returns 0 cookies"); - - details = await browser.cookies.remove({url: TEST_URL_WITH_PATH, name: "name1"}); - assertExpected({url: TEST_URL_WITH_PATH, name: "name1", storeId: STORE_ID}, details); - - cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: true}); - browser.test.assertEq(true, cookie.httpOnly, "httpOnly cookie set"); - - cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: false}); - browser.test.assertEq(false, cookie.httpOnly, "non-httpOnly cookie set"); - - details = await browser.cookies.remove({url: TEST_URL, name: "name1"}); - assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details); - - cookie = await browser.cookies.set({url: TEST_URL}); - browser.test.assertEq("", cookie.name, "default name set"); - browser.test.assertEq("", cookie.value, "default value set"); - browser.test.assertEq(true, cookie.session, "no expiry date created session cookie"); - - { - let privateWindow = await browser.windows.create({incognito: true}); - - // Hacky work-around for bugzil.la/1309637 - await new Promise(resolve => setTimeout(resolve, 700)); - - let cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "private", expirationDate: THE_FUTURE, storeId: PRIVATE_STORE_ID}); - browser.test.assertEq("private", cookie.value, "set the private cookie"); - - cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "default", expirationDate: THE_FUTURE, storeId: STORE_ID}); - browser.test.assertEq("default", cookie.value, "set the default cookie"); - - cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); - browser.test.assertEq("private", cookie.value, "get the private cookie"); - browser.test.assertEq(PRIVATE_STORE_ID, cookie.storeId, "get the private cookie storeId"); - - cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID}); - browser.test.assertEq("default", cookie.value, "get the default cookie"); - browser.test.assertEq(STORE_ID, cookie.storeId, "get the default cookie storeId"); - - let details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID}); - assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID}, details); - - cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID}); - browser.test.assertEq(null, cookie, "deleted the default cookie"); - - details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); - assertExpected({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}, details); - - cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}); - browser.test.assertEq(null, cookie, "deleted the private cookie"); - - await browser.windows.remove(privateWindow.id); - } - - browser.test.notifyPass("cookies"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["cookies", "*://example.org/"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("cookies"); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_containers.html b/toolkit/components/webextensions/test/mochitest/test_ext_cookies_containers.html deleted file mode 100644 index bc4994eec..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_containers.html +++ /dev/null @@ -1,93 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* setup() { - // make sure userContext is enabled. - return SpecialPowers.pushPrefEnv({"set": [ - ["privacy.userContext.enabled", true], - ]}); -}); - -add_task(function* test_cookie_containers() { - async function background() { - function assertExpected(expected, cookie) { - for (let key of Object.keys(cookie)) { - browser.test.assertTrue(key in expected, `found property ${key}`); - browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`); - } - browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found"); - } - - const TEST_URL = "http://example.org/"; - const THE_FUTURE = Date.now() + 5 * 60; - - let expected = { - name: "name1", - value: "value1", - domain: "example.org", - hostOnly: true, - path: "/", - secure: false, - httpOnly: false, - session: false, - expirationDate: THE_FUTURE, - storeId: "firefox-container-1", - }; - - let cookie = await browser.cookies.set({ - url: TEST_URL, name: "name1", value: "value1", - expirationDate: THE_FUTURE, storeId: "firefox-container-1", - }); - browser.test.assertEq("firefox-container-1", cookie.storeId, "the cookie has the correct storeId"); - - cookie = await browser.cookies.get({url: TEST_URL, name: "name1"}); - browser.test.assertEq(null, cookie, "get() without storeId returns null"); - - cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); - assertExpected(expected, cookie); - - let cookies = await browser.cookies.getAll({storeId: "firefox-default"}); - browser.test.assertEq(0, cookies.length, "getAll() with default storeId returns an empty array"); - - cookies = await browser.cookies.getAll({storeId: "firefox-container-1"}); - browser.test.assertEq(1, cookies.length, "one cookie found for matching domain"); - assertExpected(expected, cookies[0]); - - let details = await browser.cookies.remove({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); - assertExpected({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}, details); - - cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}); - browser.test.assertEq(null, cookie, "removed cookie not found"); - - browser.test.notifyPass("cookies"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["cookies", "*://example.org/"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("cookies"); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_expiry.html b/toolkit/components/webextensions/test/mochitest/test_ext_cookies_expiry.html deleted file mode 100644 index 3927d9e94..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_expiry.html +++ /dev/null @@ -1,72 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_cookies_expiry() { - function background() { - let expectedEvents = []; - - browser.cookies.onChanged.addListener(event => { - expectedEvents.push(`${event.removed}:${event.cause}`); - if (expectedEvents.length === 1) { - browser.test.assertEq("true:expired", expectedEvents[0], "expired cookie removed"); - browser.test.assertEq("first", event.cookie.name, "expired cookie has the expected name"); - browser.test.assertEq("one", event.cookie.value, "expired cookie has the expected value"); - } else { - browser.test.assertEq("false:explicit", expectedEvents[1], "new cookie added"); - browser.test.assertEq("first", event.cookie.name, "new cookie has the expected name"); - browser.test.assertEq("one-again", event.cookie.value, "new cookie has the expected value"); - browser.test.notifyPass("cookie-expiry"); - } - }); - - setTimeout(() => { - browser.test.sendMessage("change-cookies"); - }, 1000); - } - - let domain = ".example.com"; - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "permissions": ["http://example.com/", "cookies"], - }, - background, - }); - - let cookieSvc = SpecialPowers.Services.cookies; - - let cookie = { - host: domain, - name: "first", - path: "/", - }; - - do { - cookieSvc.add(cookie.host, cookie.path, cookie.name, "one", false, false, false, Date.now() / 1000 + 1); - } while (!cookieSvc.cookieExists(cookie)); - - yield extension.startup(); - yield extension.awaitMessage("change-cookies"); - - cookieSvc.add(cookie.host, cookie.path, cookie.name, "one-again", false, false, false, Date.now() / 1000 + 10); - - yield extension.awaitFinish("cookie-expiry"); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_permissions_bad.html b/toolkit/components/webextensions/test/mochitest/test_ext_cookies_permissions_bad.html deleted file mode 100644 index 15a62855a..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_permissions_bad.html +++ /dev/null @@ -1,112 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <script type="text/javascript" src="head_cookies.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* init() { - // We need to trigger a cookie eviction in order to test our batch delete - // observer. - SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); - SimpleTest.registerCleanupFunction(() => { - SpecialPowers.clearUserPref("network.cookie.maxPerHost"); - }); -}); - -add_task(function* test_bad_cookie_permissions() { - info("Test non-matching, non-secure domain with non-secure cookie"); - yield testCookies({ - permissions: ["http://example.com/", "cookies"], - url: "http://example.net/", - domain: "example.net", - secure: false, - shouldPass: false, - shouldWrite: false, - }); - - info("Test non-matching, secure domain with non-secure cookie"); - yield testCookies({ - permissions: ["https://example.com/", "cookies"], - url: "https://example.net/", - domain: "example.net", - secure: false, - shouldPass: false, - shouldWrite: false, - }); - - info("Test non-matching, secure domain with secure cookie"); - yield testCookies({ - permissions: ["https://example.com/", "cookies"], - url: "https://example.net/", - domain: "example.net", - secure: false, - shouldPass: false, - shouldWrite: false, - }); - - info("Test matching subdomain with superdomain privileges, secure cookie (http)"); - yield testCookies({ - permissions: ["http://foo.bar.example.com/", "cookies"], - url: "http://foo.bar.example.com/", - domain: ".example.com", - secure: true, - shouldPass: false, - shouldWrite: true, - }); - - info("Test matching, non-secure domain with secure cookie"); - yield testCookies({ - permissions: ["http://example.com/", "cookies"], - url: "http://example.com/", - domain: "example.com", - secure: true, - shouldPass: false, - shouldWrite: true, - }); - - info("Test matching, non-secure host, secure URL"); - yield testCookies({ - permissions: ["http://example.com/", "cookies"], - url: "https://example.com/", - domain: "example.com", - secure: true, - shouldPass: false, - shouldWrite: false, - }); - - info("Test non-matching domain"); - yield testCookies({ - permissions: ["http://example.com/", "cookies"], - url: "http://example.com/", - domain: "example.net", - secure: false, - shouldPass: false, - shouldWrite: false, - }); - - info("Test invalid scheme"); - yield testCookies({ - permissions: ["ftp://example.com/", "cookies"], - url: "ftp://example.com/", - domain: "example.com", - secure: false, - shouldPass: false, - shouldWrite: false, - }); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_permissions_good.html b/toolkit/components/webextensions/test/mochitest/test_ext_cookies_permissions_good.html deleted file mode 100644 index 31e83188c..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_cookies_permissions_good.html +++ /dev/null @@ -1,86 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <script type="text/javascript" src="head_cookies.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* init() { - // We need to trigger a cookie eviction in order to test our batch delete - // observer. - SpecialPowers.setIntPref("network.cookie.maxPerHost", 3); - SimpleTest.registerCleanupFunction(() => { - SpecialPowers.clearUserPref("network.cookie.maxPerHost"); - }); -}); - -add_task(function* test_good_cookie_permissions() { - info("Test matching, non-secure domain with non-secure cookie"); - yield testCookies({ - permissions: ["http://example.com/", "cookies"], - url: "http://example.com/", - domain: "example.com", - secure: false, - shouldPass: true, - }); - - info("Test matching, secure domain with non-secure cookie"); - yield testCookies({ - permissions: ["https://example.com/", "cookies"], - url: "https://example.com/", - domain: "example.com", - secure: false, - shouldPass: true, - }); - - info("Test matching, secure domain with secure cookie"); - yield testCookies({ - permissions: ["https://example.com/", "cookies"], - url: "https://example.com/", - domain: "example.com", - secure: true, - shouldPass: true, - }); - - info("Test matching subdomain with superdomain privileges, secure cookie (https)"); - yield testCookies({ - permissions: ["https://foo.bar.example.com/", "cookies"], - url: "https://foo.bar.example.com/", - domain: ".example.com", - secure: true, - shouldPass: true, - }); - - info("Test matching subdomain with superdomain privileges, non-secure cookie (https)"); - yield testCookies({ - permissions: ["https://foo.bar.example.com/", "cookies"], - url: "https://foo.bar.example.com/", - domain: ".example.com", - secure: false, - shouldPass: true, - }); - - info("Test matching subdomain with superdomain privileges, non-secure cookie (http)"); - yield testCookies({ - permissions: ["http://foo.bar.example.com/", "cookies"], - url: "http://foo.bar.example.com/", - domain: ".example.com", - secure: false, - shouldPass: true, - }); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_exclude_include_globs.html b/toolkit/components/webextensions/test/mochitest/test_ext_exclude_include_globs.html deleted file mode 100644 index 640522b40..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_exclude_include_globs.html +++ /dev/null @@ -1,92 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_contentscript() { - function background() { - browser.runtime.onMessage.addListener(([script], sender) => { - browser.test.sendMessage("run", {script}); - browser.test.sendMessage("run-" + script); - }); - browser.test.sendMessage("running"); - } - - function contentScriptAll() { - browser.runtime.sendMessage(["all"]); - } - function contentScriptIncludesTest1() { - browser.runtime.sendMessage(["includes-test1"]); - } - function contentScriptExcludesTest1() { - browser.runtime.sendMessage(["excludes-test1"]); - } - - let extensionData = { - manifest: { - content_scripts: [ - { - "matches": ["http://example.org/", "http://*.example.org/"], - "exclude_globs": [], - "include_globs": ["*"], - "js": ["content_script_all.js"], - }, - { - "matches": ["http://example.org/", "http://*.example.org/"], - "include_globs": ["*test1*"], - "js": ["content_script_includes_test1.js"], - }, - { - "matches": ["http://example.org/", "http://*.example.org/"], - "exclude_globs": ["*test1*"], - "js": ["content_script_excludes_test1.js"], - }, - ], - }, - background, - - files: { - "content_script_all.js": contentScriptAll, - "content_script_includes_test1.js": contentScriptIncludesTest1, - "content_script_excludes_test1.js": contentScriptExcludesTest1, - }, - - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - let ran = 0; - extension.onMessage("run", ({script}) => { - ran++; - }); - - yield Promise.all([extension.startup(), extension.awaitMessage("running")]); - info("extension loaded"); - - let win = window.open("http://example.org/"); - yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-excludes-test1")]); - win.close(); - is(ran, 2); - - win = window.open("http://test1.example.org/"); - yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-includes-test1")]); - win.close(); - is(ran, 4); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_external_messaging.html b/toolkit/components/webextensions/test/mochitest/test_ext_external_messaging.html deleted file mode 100644 index dfc1f9427..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_external_messaging.html +++ /dev/null @@ -1,111 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension external messaging</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function backgroundScript(id, otherId) { - browser.runtime.onMessage.addListener((msg, sender) => { - browser.test.fail(`Got unexpected message: ${uneval(msg)} ${uneval(sender)}`); - }); - - browser.runtime.onConnect.addListener(port => { - browser.test.fail(`Got unexpected connection: ${uneval(port.sender)}`); - }); - - browser.runtime.onMessageExternal.addListener((msg, sender) => { - browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`); - browser.test.assertEq(`helo-${id}`, msg, "Got expected message"); - - browser.test.sendMessage("onMessage-done"); - - return Promise.resolve(`ehlo-${otherId}`); - }); - - browser.runtime.onConnectExternal.addListener(port => { - browser.test.assertEq(otherId, port.sender.id, `${id}: Got expected external connecter ID`); - - port.onMessage.addListener(msg => { - browser.test.assertEq(`helo-${id}`, msg, "Got expected port message"); - - port.postMessage(`ehlo-${otherId}`); - - browser.test.sendMessage("onConnect-done"); - }); - }); - - browser.test.onMessage.addListener(msg => { - if (msg === "go") { - browser.runtime.sendMessage(otherId, `helo-${otherId}`).then(result => { - browser.test.assertEq(`ehlo-${id}`, result, "Got expected reply"); - browser.test.sendMessage("sendMessage-done"); - }); - - let port = browser.runtime.connect(otherId); - port.postMessage(`helo-${otherId}`); - - port.onMessage.addListener(msg => { - port.disconnect(); - - browser.test.assertEq(msg, `ehlo-${id}`, "Got expected port reply"); - browser.test.sendMessage("connect-done"); - }); - } - }); -} - -function makeExtension(id, otherId) { - let args = `${JSON.stringify(id)}, ${JSON.stringify(otherId)}`; - - let extensionData = { - background: `(${backgroundScript})(${args})`, - manifest: { - "applications": {"gecko": {id}}, - }, - }; - - return ExtensionTestUtils.loadExtension(extensionData); -} - -add_task(function* test_contentscript() { - const ID1 = "foo-message@mochitest.mozilla.org"; - const ID2 = "bar-message@mochitest.mozilla.org"; - - let extension1 = makeExtension(ID1, ID2); - let extension2 = makeExtension(ID2, ID1); - - yield Promise.all([extension1.startup(), extension2.startup()]); - - extension1.sendMessage("go"); - extension2.sendMessage("go"); - - yield Promise.all([ - extension1.awaitMessage("sendMessage-done"), - extension2.awaitMessage("sendMessage-done"), - - extension1.awaitMessage("onMessage-done"), - extension2.awaitMessage("onMessage-done"), - - extension1.awaitMessage("connect-done"), - extension2.awaitMessage("connect-done"), - - extension1.awaitMessage("onConnect-done"), - extension2.awaitMessage("onConnect-done"), - ]); - - yield extension1.unload(); - yield extension2.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_generate.html b/toolkit/components/webextensions/test/mochitest/test_ext_generate.html deleted file mode 100644 index cfafcbad9..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_generate.html +++ /dev/null @@ -1,49 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for generating WebExtensions</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function background() { - browser.test.log("running background script"); - - browser.test.onMessage.addListener((x, y) => { - browser.test.assertEq(x, 10, "x is 10"); - browser.test.assertEq(y, 20, "y is 20"); - - browser.test.notifyPass("background test passed"); - }); - - browser.test.sendMessage("running", 1); -} - -let extensionData = { - background, -}; - -add_task(function* test_background() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - info("load complete"); - let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); - is(x, 1, "got correct value from extension"); - info("startup complete"); - extension.sendMessage(10, 20); - yield extension.awaitFinish(); - info("test complete"); - yield extension.unload(); - info("extension unloaded successfully"); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_geturl.html b/toolkit/components/webextensions/test/mochitest/test_ext_geturl.html deleted file mode 100644 index 6e39c2f5d..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_geturl.html +++ /dev/null @@ -1,72 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function background() { - browser.runtime.onMessage.addListener(([url1, url2]) => { - let url3 = browser.runtime.getURL("test_file.html"); - let url4 = browser.extension.getURL("test_file.html"); - - browser.test.assertTrue(url1 !== undefined, "url1 defined"); - - browser.test.assertTrue(url1.startsWith("moz-extension://"), "url1 has correct scheme"); - browser.test.assertTrue(url1.endsWith("test_file.html"), "url1 has correct leaf name"); - - browser.test.assertEq(url1, url2, "url2 matches"); - browser.test.assertEq(url1, url3, "url3 matches"); - browser.test.assertEq(url1, url4, "url4 matches"); - - browser.test.notifyPass("geturl"); - }); -} - -function contentScript() { - let url1 = browser.runtime.getURL("test_file.html"); - let url2 = browser.extension.getURL("test_file.html"); - browser.runtime.sendMessage([url1, url2]); -} - -let extensionData = { - background, - manifest: { - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }], - }, - - files: { - "content_script.js": contentScript, - }, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), extension.awaitFinish("geturl")]); - - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_i18n.html b/toolkit/components/webextensions/test/mochitest/test_ext_i18n.html deleted file mode 100644 index 1f7330bbb..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_i18n.html +++ /dev/null @@ -1,432 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <meta charset="utf-8"> - <title>Test for WebExtension localization APIs</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("intl.accept_languages"); }); -SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("general.useragent.locale"); }); - -add_task(function* test_i18n() { - function runTests(assertEq) { - let _ = browser.i18n.getMessage.bind(browser.i18n); - - let url = browser.runtime.getURL("/"); - assertEq(url, `moz-extension://${_("@@extension_id")}/`, "@@extension_id builtin message"); - - assertEq("Foo.", _("Foo"), "Simple message in selected locale."); - - assertEq("(bar)", _("bar"), "Simple message fallback in default locale."); - - assertEq("", _("some-unknown-locale-string"), "Unknown locale string."); - - assertEq("", _("@@unknown_builtin_string"), "Unknown built-in string."); - assertEq("", _("@@bidi_unknown_builtin_string"), "Unknown built-in bidi string."); - - assertEq("Føo.", _("Föo"), "Multi-byte message in selected locale."); - - let substitutions = []; - substitutions[4] = "5"; - substitutions[13] = "14"; - - assertEq("'$0' '14' '' '5' '$$$$' '$'.", _("basic_substitutions", substitutions), - "Basic numeric substitutions"); - - assertEq("'$0' '' 'just a string' '' '$$$$' '$'.", _("basic_substitutions", "just a string"), - "Basic numeric substitutions, with non-array value"); - - let values = _("named_placeholder_substitutions", ["(subst $1 $2)", "(2 $1 $2)"]).split("\n"); - - assertEq("_foo_ (subst $1 $2) _bar_", values[0], "Named and numeric substitution"); - - assertEq("(2 $1 $2)", values[1], "Numeric substitution amid named placeholders"); - - assertEq("$bad name$", values[2], "Named placeholder with invalid key"); - - assertEq("", values[3], "Named placeholder with an invalid value"); - - assertEq("Accepted, but shouldn't break.", values[4], "Named placeholder with a strange content value"); - - assertEq("$foo", values[5], "Non-placeholder token that should be ignored"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "default_locale": "jp", - - content_scripts: [ - {"matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content.js"]}, - ], - }, - - - files: { - "_locales/en_US/messages.json": { - "foo": { - "message": "Foo.", - "description": "foo", - }, - - "föo": { - "message": "Føo.", - "description": "foo", - }, - - "basic_substitutions": { - "message": "'$0' '$14' '$1' '$5' '$$$$$' '$$'.", - "description": "foo", - }, - - "Named_placeholder_substitutions": { - "message": "$Foo$\n$2\n$bad name$\n$bad_value$\n$bad_content_value$\n$foo", - "description": "foo", - "placeholders": { - "foO": { - "content": "_foo_ $1 _bar_", - "description": "foo", - }, - - "bad name": { - "content": "Nope.", - "description": "bad name", - }, - - "bad_value": "Nope.", - - "bad_content_value": { - "content": ["Accepted, but shouldn't break."], - "description": "bad value", - }, - }, - }, - - "broken_placeholders": { - "message": "$broken$", - "description": "broken placeholders", - "placeholders": "foo.", - }, - }, - - "_locales/jp/messages.json": { - "foo": { - "message": "(foo)", - "description": "foo", - }, - - "bar": { - "message": "(bar)", - "description": "bar", - }, - }, - - "content.js": "new " + function(runTestsFn) { - runTestsFn((...args) => { - browser.runtime.sendMessage(["assertEq", ...args]); - }); - - browser.runtime.sendMessage(["content-script-finished"]); - } + `(${runTests})`, - }, - - background: "new " + function(runTestsFn) { - browser.runtime.onMessage.addListener(([msg, ...args]) => { - if (msg == "assertEq") { - browser.test.assertEq(...args); - } else { - browser.test.sendMessage(msg, ...args); - } - }); - - runTestsFn(browser.test.assertEq.bind(browser.test)); - } + `(${runTests})`, - }); - - yield extension.startup(); - - let win = window.open("file_sample.html"); - yield extension.awaitMessage("content-script-finished"); - win.close(); - - yield extension.unload(); -}); - -add_task(function* test_get_accept_languages() { - function background() { - function checkResults(source, results, expected) { - browser.test.assertEq( - expected.length, - results.length, - `got expected number of languages in ${source}`); - results.forEach((lang, index) => { - browser.test.assertEq( - expected[index], - lang, - `got expected language in ${source}`); - }); - } - - let tabId; - - browser.tabs.query({currentWindow: true, active: true}, tabs => { - tabId = tabs[0].id; - browser.test.sendMessage("ready"); - }); - - browser.test.onMessage.addListener(async ([msg, expected]) => { - let contentResults = await browser.tabs.sendMessage(tabId, "get-results"); - let backgroundResults = await browser.i18n.getAcceptLanguages(); - - checkResults("contentScript", contentResults, expected); - checkResults("background", backgroundResults, expected); - - browser.test.sendMessage("done"); - }); - } - - function content() { - browser.runtime.onMessage.addListener((msg, sender, respond) => { - browser.i18n.getAcceptLanguages(respond); - return true; - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "run_at": "document_start", - "js": ["content_script.js"], - }], - }, - - background, - - files: { - "content_script.js": content, - }, - }); - - let win = window.open("file_sample.html"); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - let expectedLangs = ["en-US", "en"]; - extension.sendMessage(["expect-results", expectedLangs]); - yield extension.awaitMessage("done"); - - expectedLangs = ["en-US", "en", "fr-CA", "fr"]; - SpecialPowers.setCharPref("intl.accept_languages", expectedLangs.toString()); - extension.sendMessage(["expect-results", expectedLangs]); - yield extension.awaitMessage("done"); - SpecialPowers.clearUserPref("intl.accept_languages"); - - win.close(); - - yield extension.unload(); -}); - -add_task(function* test_get_ui_language() { - function getResults() { - return { - getUILanguage: browser.i18n.getUILanguage(), - getMessage: browser.i18n.getMessage("@@ui_locale"), - }; - } - - function background(getResultsFn) { - function checkResults(source, results, expected) { - browser.test.assertEq( - expected, - results.getUILanguage, - `Got expected getUILanguage result in ${source}` - ); - browser.test.assertEq( - expected, - results.getMessage, - `Got expected getMessage result in ${source}` - ); - } - - let tabId; - - browser.test.onMessage.addListener(([msg, expected]) => { - browser.tabs.sendMessage(tabId, "get-results", result => { - checkResults("contentScript", result, expected); - checkResults("background", getResultsFn(), expected); - - browser.test.sendMessage("done"); - }); - }); - - browser.tabs.query({currentWindow: true, active: true}, tabs => { - tabId = tabs[0].id; - browser.test.sendMessage("ready"); - }); - } - - function content(getResultsFn) { - browser.runtime.onMessage.addListener((msg, sender, respond) => { - respond(getResultsFn()); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "run_at": "document_start", - "js": ["content_script.js"], - }], - }, - - background: `(${background})(${getResults})`, - - files: { - "content_script.js": `(${content})(${getResults})`, - }, - }); - - let win = window.open("file_sample.html"); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - extension.sendMessage(["expect-results", "en_US"]); - yield extension.awaitMessage("done"); - - SpecialPowers.setCharPref("general.useragent.locale", "he"); - - extension.sendMessage(["expect-results", "he"]); - yield extension.awaitMessage("done"); - - win.close(); - - yield extension.unload(); -}); - - -add_task(function* test_detect_language() { - const af_string = " aam skukuza die naam beteken hy wat skoonvee of hy wat alles onderstebo keer wysig " + - "bosveldkampe boskampe is kleiner afgeleë ruskampe wat oor min fasiliteite beskik daar is geen restaurante " + - "of winkels nie en slegs oornagbesoekers word toegelaat bateleur"; - // String with intermixed French/English text - const fr_en_string = "France is the largest country in Western Europe and the third-largest in Europe as a whole. " + - "A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " + - "Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " + - "Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." + - "Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumped over the lazy dog"; - - function background() { - function checkResult(source, result, expected) { - browser.test.assertEq(expected.isReliable, result.isReliable, "result.confident is true"); - browser.test.assertEq( - expected.languages.length, - result.languages.length, - `result.languages contains the expected number of languages in ${source}`); - expected.languages.forEach((lang, index) => { - browser.test.assertEq( - lang.percentage, - result.languages[index].percentage, - `element ${index} of result.languages array has the expected percentage in ${source}`); - browser.test.assertEq( - lang.language, - result.languages[index].language, - `element ${index} of result.languages array has the expected language in ${source}`); - }); - } - - let tabId; - - browser.tabs.query({currentWindow: true, active: true}, tabs => { - tabId = tabs[0].id; - browser.test.sendMessage("ready"); - }); - - browser.test.onMessage.addListener(async ([msg, expected]) => { - let backgroundResults = await browser.i18n.detectLanguage(msg); - let contentResults = await browser.tabs.sendMessage(tabId, msg); - - checkResult("background", backgroundResults, expected); - checkResult("contentScript", contentResults, expected); - - browser.test.sendMessage("done"); - }); - } - - function content() { - browser.runtime.onMessage.addListener((msg, sender, respond) => { - browser.i18n.detectLanguage(msg, respond); - return true; - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "run_at": "document_start", - "js": ["content_script.js"], - }], - }, - - background, - - files: { - "content_script.js": content, - }, - }); - - let win = window.open("file_sample.html"); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - let expected = { - isReliable: true, - languages: [ - { - language: "fr", - percentage: 67, - }, - { - language: "en", - percentage: 32, - }, - ], - }; - extension.sendMessage([fr_en_string, expected]); - yield extension.awaitMessage("done"); - - expected = { - isReliable: true, - languages: [ - { - language: "af", - percentage: 99, - }, - ], - }; - extension.sendMessage([af_string, expected]); - yield extension.awaitMessage("done"); - - win.close(); - - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_i18n_css.html b/toolkit/components/webextensions/test/mochitest/test_ext_i18n_css.html deleted file mode 100644 index 7c6a8eeaa..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_i18n_css.html +++ /dev/null @@ -1,116 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_i18n_css() { - let extension = ExtensionTestUtils.loadExtension({ - background: function() { - function backgroundFetch(url) { - return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - xhr.open("GET", url); - xhr.onload = () => { resolve(xhr.responseText); }; - xhr.onerror = reject; - xhr.send(); - }); - } - - Promise.all([backgroundFetch("foo.css"), backgroundFetch("bar.CsS?x#y"), backgroundFetch("foo.txt")]).then(results => { - browser.test.assertEq("body { max-width: 42px; }", results[0], "CSS file localized"); - browser.test.assertEq("body { max-width: 42px; }", results[1], "CSS file localized"); - - browser.test.assertEq("body { __MSG_foo__; }", results[2], "Text file not localized"); - - browser.test.notifyPass("i18n-css"); - }); - - browser.test.sendMessage("ready", browser.runtime.getURL("foo.css")); - }, - - manifest: { - "web_accessible_resources": ["foo.css", "foo.txt", "locale.css"], - - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "css": ["foo.css"], - }], - - "default_locale": "en", - }, - - files: { - "_locales/en/messages.json": JSON.stringify({ - "foo": { - "message": "max-width: 42px", - "description": "foo", - }, - }), - - "foo.css": "body { __MSG_foo__; }", - "bar.CsS": "body { __MSG_foo__; }", - "foo.txt": "body { __MSG_foo__; }", - "locale.css": '* { content: "__MSG_@@ui_locale__ __MSG_@@bidi_dir__ __MSG_@@bidi_reversed_dir__ __MSG_@@bidi_start_edge__ __MSG_@@bidi_end_edge__" }', - }, - }); - - yield extension.startup(); - let cssURL = yield extension.awaitMessage("ready"); - - function fetch(url) { - return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - xhr.open("GET", url); - xhr.onload = () => { resolve(xhr.responseText); }; - xhr.onerror = reject; - xhr.send(); - }); - } - - let css = yield fetch(cssURL); - - is(css, "body { max-width: 42px; }", "CSS file localized in mochitest scope"); - - let win = window.open("file_sample.html"); - yield waitForLoad(win); - - let style = win.getComputedStyle(win.document.body); - is(style.maxWidth, "42px", "stylesheet correctly applied"); - win.close(); - - cssURL = cssURL.replace(/foo.css$/, "locale.css"); - - css = yield fetch(cssURL); - is(css, '* { content: "en_US ltr rtl left right" }', "CSS file localized in mochitest scope"); - - const LOCALE = "general.useragent.locale"; - const DIR = "intl.uidirection.en"; - - // We don't wind up actually switching the chrome registry locale, since we - // don't have a chrome package for Hebrew. So just override it. - SpecialPowers.setCharPref(LOCALE, "he"); - SpecialPowers.setCharPref(DIR, "rtl"); - - css = yield fetch(cssURL); - is(css, '* { content: "he rtl ltr right left" }', "CSS file localized in mochitest scope"); - - SpecialPowers.clearUserPref(LOCALE); - SpecialPowers.clearUserPref(DIR); - - yield extension.awaitFinish("i18n-css"); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_inIncognitoContext_window.html b/toolkit/components/webextensions/test/mochitest/test_ext_inIncognitoContext_window.html deleted file mode 100644 index 675cbb298..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_inIncognitoContext_window.html +++ /dev/null @@ -1,49 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_in_incognito_context_true() { - function background() { - browser.runtime.onMessage.addListener(msg => { - browser.test.assertEq(true, msg, "inIncognitoContext is true"); - browser.test.notifyPass("inIncognitoContext"); - }); - - browser.windows.create({url: browser.runtime.getURL("/tab.html"), incognito: true}); - } - - function tabScript() { - browser.runtime.sendMessage(browser.extension.inIncognitoContext); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - files: { - "tab.js": tabScript, - "tab.html": `<!DOCTYPE html><html><head> - <meta charset="utf-8"> - <script src="tab.js"><\/script> - </head></html>`, - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("inIncognitoContext"); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_jsversion.html b/toolkit/components/webextensions/test/mochitest/test_ext_jsversion.html deleted file mode 100644 index da0c355e0..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_jsversion.html +++ /dev/null @@ -1,86 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for simple WebExtension</title> - <meta charset="utf-8"> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_versioned_js() { - // We need to deal with escaping the close script tags. - // May as well consolidate it into one place. - let script = attrs => `<script ${attrs}><\/script>`; - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "background": {"page": "background.html"}, - }, - - files: { - "background.html": ` - <meta charset="utf-8"> - ${script('src="background.js" type="application/javascript"')} - ${script('src="background-1.js" type="application/javascript;version=1.8"')} - ${script('src="background-2.js" type="application/javascript;version=latest"')} - ${script('src="background-3.js" type="application/javascript"')} - `, - - "background.js": function() { - window.reportResult = msg => { - browser.test.assertEq( - msg, "background-script-3", - "Expected a message only from the unversioned background script."); - - browser.test.sendMessage("finished"); - }; - }, - - "background-1.js": function() { - window.reportResult("background-script-1"); - }, - "background-2.js": function() { - window.reportResult("background-script-2"); - }, - "background-3.js": function() { - window.reportResult("background-script-3"); - }, - }, - }); - - let messages = [/Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/, - /Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/]; - - let waitForConsole = new Promise(resolve => { - SimpleTest.monitorConsole(resolve, messages); - }); - - info("loading extension"); - - yield Promise.all([extension.startup(), - extension.awaitMessage("finished")]); - - info("waiting for console"); - - SimpleTest.endMonitorConsole(); - yield waitForConsole; - - info("unloading extension"); - - yield extension.unload(); - - info("test complete"); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_listener_proxies.html b/toolkit/components/webextensions/test/mochitest/test_ext_listener_proxies.html deleted file mode 100644 index ca8db873e..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_listener_proxies.html +++ /dev/null @@ -1,63 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_listener_proxies() { - let extension = ExtensionTestUtils.loadExtension({ - useAddonManager: "temporary", - - manifest: { - "permissions": ["storage"], - }, - - async background() { - // Test that adding multiple listeners for the same event works as - // expected. - - let awaitChanged = () => new Promise(resolve => { - browser.storage.onChanged.addListener(function listener() { - browser.storage.onChanged.removeListener(listener); - resolve(); - }); - }); - - let promises = [ - awaitChanged(), - awaitChanged(), - ]; - - function removedListener() {} - browser.storage.onChanged.addListener(removedListener); - browser.storage.onChanged.removeListener(removedListener); - - promises.push(awaitChanged(), awaitChanged()); - - browser.storage.local.set({foo: "bar"}); - - await Promise.all(promises); - - browser.test.notifyPass("onchanged-listeners"); - }, - }); - - yield extension.startup(); - - yield extension.awaitFinish("onchanged-listeners"); - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_notifications.html b/toolkit/components/webextensions/test/mochitest/test_ext_notifications.html deleted file mode 100644 index d1b798cf9..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_notifications.html +++ /dev/null @@ -1,224 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for notifications</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -// A 1x1 PNG image. -// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) -let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + - "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); -const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer; - -add_task(function* test_notification() { - async function background() { - let opts = { - type: "basic", - title: "Testing Notification", - message: "Carry on", - }; - - let id = await browser.notifications.create(opts); - - browser.test.sendMessage("running", id); - browser.test.notifyPass("background test passed"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["notifications"], - }, - background, - }); - yield extension.startup(); - let x = yield extension.awaitMessage("running"); - is(x, "0", "got correct id from notifications.create"); - yield extension.awaitFinish(); - yield extension.unload(); -}); - -add_task(function* test_notification_events() { - async function background() { - let opts = { - type: "basic", - title: "Testing Notification", - message: "Carry on", - }; - - // Test an ignored listener. - browser.notifications.onButtonClicked.addListener(function() {}); - - // We cannot test onClicked listener without a mock - // but we can attempt to add a listener. - browser.notifications.onClicked.addListener(function() {}); - - // Test onClosed listener. - browser.notifications.onClosed.addListener(id => { - browser.test.sendMessage("closed", id); - browser.test.notifyPass("background test passed"); - }); - - await browser.notifications.create("5", opts); - let id = await browser.notifications.create("5", opts); - browser.test.sendMessage("running", id); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["notifications"], - }, - background, - }); - yield extension.startup(); - let x = yield extension.awaitMessage("closed"); - is(x, "5", "got correct id from onClosed listener"); - x = yield extension.awaitMessage("running"); - is(x, "5", "got correct id from notifications.create"); - yield extension.awaitFinish(); - yield extension.unload(); -}); - -add_task(function* test_notification_clear() { - async function background() { - let opts = { - type: "basic", - title: "Testing Notification", - message: "Carry on", - }; - - browser.notifications.onClosed.addListener(id => { - browser.test.sendMessage("closed", id); - }); - - let id = await browser.notifications.create("99", opts); - - let wasCleared = await browser.notifications.clear(id); - browser.test.sendMessage("cleared", wasCleared); - - browser.test.notifyPass("background test passed"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["notifications"], - }, - background, - }); - yield extension.startup(); - let x = yield extension.awaitMessage("closed"); - is(x, "99", "got correct id from onClosed listener"); - x = yield extension.awaitMessage("cleared"); - is(x, true, "got correct boolean from notifications.clear"); - yield extension.awaitFinish(); - yield extension.unload(); -}); - -add_task(function* test_notifications_empty_getAll() { - async function background() { - let notifications = await browser.notifications.getAll(); - - browser.test.assertEq("object", typeof notifications, "getAll() returned an object"); - browser.test.assertEq(0, Object.keys(notifications).length, "the object has no properties"); - browser.test.notifyPass("getAll empty"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["notifications"], - }, - background, - }); - yield extension.startup(); - yield extension.awaitFinish("getAll empty"); - yield extension.unload(); -}); - -add_task(function* test_notifications_populated_getAll() { - async function background() { - let opts = { - type: "basic", - iconUrl: "a.png", - title: "Testing Notification", - message: "Carry on", - }; - - await browser.notifications.create("p1", opts); - await browser.notifications.create("p2", opts); - let notifications = await browser.notifications.getAll(); - - browser.test.assertEq("object", typeof notifications, "getAll() returned an object"); - browser.test.assertEq(2, Object.keys(notifications).length, "the object has 2 properties"); - - for (let notificationId of ["p1", "p2"]) { - for (let key of Object.keys(opts)) { - browser.test.assertEq( - opts[key], - notifications[notificationId][key], - `the notification has the expected value for option: ${key}` - ); - } - } - - browser.test.notifyPass("getAll populated"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["notifications"], - }, - background, - files: { - "a.png": IMAGE_ARRAYBUFFER, - }, - }); - yield extension.startup(); - yield extension.awaitFinish("getAll populated"); - yield extension.unload(); -}); - -add_task(function* test_buttons_unsupported() { - function background() { - let opts = { - type: "basic", - title: "Testing Notification", - message: "Carry on", - buttons: [{title: "Button title"}], - }; - - let exception = {}; - try { - browser.notifications.create(opts); - } catch (e) { - exception = e; - } - - browser.test.assertTrue( - String(exception).includes('Property "buttons" is unsupported by Firefox'), - "notifications.create with buttons option threw an expected exception" - ); - browser.test.notifyPass("buttons-unsupported"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["notifications"], - }, - background, - }); - yield extension.startup(); - yield extension.awaitFinish("buttons-unsupported"); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_permission_xhr.html b/toolkit/components/webextensions/test/mochitest/test_ext_permission_xhr.html deleted file mode 100644 index 07967d5d0..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_permission_xhr.html +++ /dev/null @@ -1,119 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension Test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/* eslint-disable mozilla/balanced-listeners */ - -add_task(function* test_simple() { - async function runTests(cx) { - function xhr(XMLHttpRequest) { - return (url) => { - return new Promise((resolve, reject) => { - let req = new XMLHttpRequest(); - req.open("GET", url); - req.addEventListener("load", resolve); - req.addEventListener("error", reject); - req.send(); - }); - }; - } - - function run(shouldFail, fetch) { - function passListener() { - browser.test.succeed(`${cx}.${fetch.name} pass listener`); - } - - function failListener() { - browser.test.fail(`${cx}.${fetch.name} fail listener`); - } - - /* eslint-disable no-else-return */ - if (shouldFail) { - return fetch("http://example.org/example.txt").then(failListener, passListener); - } else { - return fetch("http://example.com/example.txt").then(passListener, failListener); - } - /* eslint-enable no-else-return */ - } - - try { - await run(true, xhr(XMLHttpRequest)); - await run(false, xhr(XMLHttpRequest)); - await run(true, xhr(window.XMLHttpRequest)); - await run(false, xhr(window.XMLHttpRequest)); - await run(true, fetch); - await run(false, fetch); - await run(true, window.fetch); - await run(false, window.fetch); - } catch (err) { - browser.test.fail(`Error: ${err} :: ${err.stack}`); - browser.test.notifyFail("permission_xhr"); - } - } - - async function background(runTestsFn) { - await runTestsFn("bg"); - browser.test.notifyPass("permission_xhr"); - } - - let extensionData = { - background: `(${background})(${runTests})`, - manifest: { - permissions: ["http://example.com/"], - content_scripts: [{ - "matches": ["http://mochi.test/*/file_permission_xhr.html"], - "js": ["content.js"], - }], - }, - files: { - "content.js": `(${async runTestsFn => { - await runTestsFn("content"); - - window.wrappedJSObject.privilegedFetch = fetch; - window.wrappedJSObject.privilegedXHR = XMLHttpRequest; - - window.addEventListener("message", function rcv({data}) { - switch (data.msg) { - case "test": - break; - - case "assertTrue": - browser.test.assertTrue(data.condition, data.description); - break; - - case "finish": - window.removeEventListener("message", rcv, false); - browser.test.sendMessage("content-script-finished"); - break; - } - }, false); - window.postMessage("test", "*"); - }})(${runTests})`, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_permission_xhr.html"); - yield extension.awaitMessage("content-script-finished"); - win.close(); - - yield extension.awaitFinish("permission_xhr"); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect.html b/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect.html deleted file mode 100644 index 60351eaee..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect.html +++ /dev/null @@ -1,83 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function background() { - browser.runtime.onConnect.addListener(port => { - browser.test.assertEq(port.name, "ernie", "port name correct"); - browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "URL correct"); - browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "tab URL correct"); - - let expected = "message 1"; - port.onMessage.addListener(msg => { - browser.test.assertEq(msg, expected, "message is expected"); - if (expected == "message 1") { - port.postMessage("message 2"); - expected = "message 3"; - } else if (expected == "message 3") { - expected = "disconnect"; - browser.test.notifyPass("runtime.connect"); - } - }); - port.onDisconnect.addListener(() => { - browser.test.assertEq(null, port.error, "No error because port is closed by disconnect() at other end"); - browser.test.assertEq(expected, "disconnect", "got disconnection at right time"); - }); - }); -} - -function contentScript() { - let port = browser.runtime.connect({name: "ernie"}); - port.postMessage("message 1"); - port.onMessage.addListener(msg => { - if (msg == "message 2") { - port.postMessage("message 3"); - port.disconnect(); - } - }); -} - -let extensionData = { - background, - manifest: { - "permissions": ["tabs"], - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_start", - }], - }, - - files: { - "content_script.js": contentScript, - }, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]); - - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect2.html b/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect2.html deleted file mode 100644 index dce12b21b..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect2.html +++ /dev/null @@ -1,103 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function backgroundScript(token) { - browser.runtime.onMessage.addListener(msg => { - browser.test.assertEq(msg, "done"); - browser.test.notifyPass("sendmessage_reply"); - }); - - browser.runtime.onConnect.addListener(port => { - browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "sender url correct"); - browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "sender url correct"); - - let tabId = port.sender.tab.id; - browser.tabs.connect(tabId, {name: token}); - - browser.test.assertEq(port.name, token, "token matches"); - port.postMessage(token + "-done"); - }); - - browser.test.sendMessage("background-ready"); -} - -function contentScript(token) { - let gotTabMessage = false; - let badTabMessage = false; - browser.runtime.onConnect.addListener(port => { - if (port.name == token) { - gotTabMessage = true; - } else { - badTabMessage = true; - } - port.disconnect(); - }); - - let port = browser.runtime.connect(null, {name: token}); - port.onMessage.addListener(function(msg) { - if (msg != token + "-done" || !gotTabMessage || badTabMessage) { - return; // test failed - } - - // FIXME: Removing this line causes the test to fail: - // resource://gre/modules/ExtensionUtils.jsm, line 651: NS_ERROR_NOT_INITIALIZED - port.disconnect(); - browser.runtime.sendMessage("done"); - }); -} - -function makeExtension() { - let token = Math.random(); - let extensionData = { - background: `(${backgroundScript})("${token}")`, - manifest: { - "permissions": ["tabs"], - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }], - }, - - files: { - "content_script.js": `(${contentScript})("${token}")`, - }, - }; - return extensionData; -} - -add_task(function* test_contentscript() { - let extension1 = ExtensionTestUtils.loadExtension(makeExtension()); - let extension2 = ExtensionTestUtils.loadExtension(makeExtension()); - yield Promise.all([extension1.startup(), extension2.startup()]); - - yield extension1.awaitMessage("background-ready"); - yield extension2.awaitMessage("background-ready"); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), - extension1.awaitFinish("sendmessage_reply"), - extension2.awaitFinish("sendmessage_reply")]); - - win.close(); - - yield extension1.unload(); - yield extension2.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect_twoway.html b/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect_twoway.html deleted file mode 100644 index e84134eff..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_connect_twoway.html +++ /dev/null @@ -1,127 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -add_task(function* test_connect_bidirectionally_and_postMessage() { - function background() { - let onConnectCount = 0; - browser.runtime.onConnect.addListener(port => { - // 3. onConnect by connect() from CS. - browser.test.assertEq("from-cs", port.name); - browser.test.assertEq(1, ++onConnectCount, - "BG onConnect should be called once"); - - let tabId = port.sender.tab.id; - browser.test.assertTrue(tabId, "content script must have a tab ID"); - - let port2; - let postMessageCount1 = 0; - port.onMessage.addListener(msg => { - // 11. port.onMessage by port.postMessage in CS. - browser.test.assertEq("from CS to port", msg); - browser.test.assertEq(1, ++postMessageCount1, - "BG port.onMessage should be called once"); - - // 12. should trigger port2.onMessage in CS. - port2.postMessage("from BG to port2"); - }); - - // 4. Should trigger onConnect in CS. - port2 = browser.tabs.connect(tabId, {name: "from-bg"}); - let postMessageCount2 = 0; - port2.onMessage.addListener(msg => { - // 7. onMessage by port2.postMessage in CS. - browser.test.assertEq("from CS to port2", msg); - browser.test.assertEq(1, ++postMessageCount2, - "BG port2.onMessage should be called once"); - - // 8. Should trigger port.onMessage in CS. - port.postMessage("from BG to port"); - }); - }); - - // 1. Notify test runner to create a new tab. - browser.test.sendMessage("ready"); - } - - function contentScript() { - let onConnectCount = 0; - let port; - browser.runtime.onConnect.addListener(port2 => { - // 5. onConnect by connect() from BG. - browser.test.assertEq("from-bg", port2.name); - browser.test.assertEq(1, ++onConnectCount, - "CS onConnect should be called once"); - - let postMessageCount2 = 0; - port2.onMessage.addListener(msg => { - // 12. port2.onMessage by port2.postMessage in BG. - browser.test.assertEq("from BG to port2", msg); - browser.test.assertEq(1, ++postMessageCount2, - "CS port2.onMessage should be called once"); - - // TODO(robwu): Do not explicitly disconnect, it should not be a problem - // if we keep the ports open. However, not closing the ports causes the - // test to fail with NS_ERROR_NOT_INITIALIZED in ExtensionUtils.jsm, in - // Port.prototype.disconnect (nsIMessageSender.sendAsyncMessage). - port.disconnect(); - port2.disconnect(); - browser.test.notifyPass("ping pong done"); - }); - // 6. should trigger port2.onMessage in BG. - port2.postMessage("from CS to port2"); - }); - - // 2. should trigger onConnect in BG. - port = browser.runtime.connect({name: "from-cs"}); - let postMessageCount1 = 0; - port.onMessage.addListener(msg => { - // 9. onMessage by port.postMessage in BG. - browser.test.assertEq("from BG to port", msg); - browser.test.assertEq(1, ++postMessageCount1, - "CS port.onMessage should be called once"); - - // 10. should trigger port.onMessage in BG. - port.postMessage("from CS to port"); - }); - } - - let extensionData = { - background, - manifest: { - content_scripts: [{ - js: ["contentscript.js"], - matches: ["http://mochi.test/*/file_sample.html"], - }], - }, - files: { - "contentscript.js": contentScript, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - info("extension loaded"); - - yield extension.awaitMessage("ready"); - - let win = window.open("file_sample.html"); - yield extension.awaitFinish("ping pong done"); - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> -</body> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_disconnect.html b/toolkit/components/webextensions/test/mochitest/test_ext_runtime_disconnect.html deleted file mode 100644 index 5764d0a3c..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_disconnect.html +++ /dev/null @@ -1,78 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function background() { - browser.runtime.onConnect.addListener(port => { - browser.test.assertEq(port.name, "ernie", "port name correct"); - port.onDisconnect.addListener(() => { - browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads"); - // Closing an already-disconnected port is a no-op. - port.disconnect(); - port.disconnect(); - browser.test.sendMessage("disconnected"); - }); - browser.test.sendMessage("connected"); - }); -} - -function contentScript() { - browser.runtime.connect({name: "ernie"}); -} - -let extensionData = { - background, - manifest: { - "permissions": ["tabs"], - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }], - }, - - files: { - "content_script.js": contentScript, - }, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - yield Promise.all([waitForLoad(win), extension.awaitMessage("connected")]); - win.close(); - yield extension.awaitMessage("disconnected"); - - info("win.close() succeeded"); - - win = window.open("file_sample.html"); - yield Promise.all([waitForLoad(win), extension.awaitMessage("connected")]); - - // Add an "unload" listener so that we don't put the window in the - // bfcache. This way it gets destroyed immediately upon navigation. - win.addEventListener("unload", function() {}); // eslint-disable-line mozilla/balanced-listeners - - win.location = "http://example.com"; - yield extension.awaitMessage("disconnected"); - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_id.html b/toolkit/components/webextensions/test/mochitest/test_ext_runtime_id.html deleted file mode 100644 index 4cdefda41..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_runtime_id.html +++ /dev/null @@ -1,61 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <meta charset="utf-8"> - <title>Test for browser.runtime.id</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_runtime_id() { - function background() { - browser.test.sendMessage("background-id", browser.runtime.id); - } - - function content() { - browser.test.sendMessage("content-id", browser.runtime.id); - } - - let uuidGenerator = SpecialPowers.Cc["@mozilla.org/uuid-generator;1"].getService(SpecialPowers.Ci.nsIUUIDGenerator); - let id = uuidGenerator.generateUUID().number; - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - applications: {gecko: {id}}, - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "run_at": "document_start", - "js": ["content_script.js"], - }], - }, - - background, - - files: { - "content_script.js": content, - }, - }); - - yield extension.startup(); - - let backgroundId = yield extension.awaitMessage("background-id"); - is(backgroundId, id, "runtime.id from background script is correct"); - let win = window.open("file_sample.html"); - let contentId = yield extension.awaitMessage("content-id"); - is(contentId, id, "runtime.id from content script is correct"); - - win.close(); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_sandbox_var.html b/toolkit/components/webextensions/test/mochitest/test_ext_sandbox_var.html deleted file mode 100644 index 426a71ac6..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_sandbox_var.html +++ /dev/null @@ -1,60 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function background() { - browser.runtime.onMessage.addListener(result => { - browser.test.assertEq(result, 12, "x is 12"); - browser.test.notifyPass("background test passed"); - }); -} - -function contentScript() { - window.x = 12; - browser.runtime.onMessage.addListener(function() {}); - browser.runtime.sendMessage(window.x); -} - -let extensionData = { - background, - manifest: { - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }], - }, - - files: { - "content_script.js": contentScript, - }, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), extension.awaitFinish()]); - - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_schema.html b/toolkit/components/webextensions/test/mochitest/test_ext_schema.html deleted file mode 100644 index 8a0e11c56..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_schema.html +++ /dev/null @@ -1,73 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for schema API creation</title> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="chrome_head.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* testEmptySchema() { - function background() { - browser.test.assertEq(undefined, browser.manifest, "browser.manifest is not defined"); - browser.test.assertTrue("storage" in browser, "browser.storage should be defined"); - browser.test.assertEq(undefined, browser.contextMenus, "browser.contextMenus should not be defined"); - browser.test.notifyPass("schema"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["storage"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("schema"); - yield extension.unload(); -}); - -add_task(function* testUnknownProperties() { - function background() { - browser.test.notifyPass("loaded"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["unknownPermission"], - - unknown_property: {}, - }, - - background, - }); - - let messages = [ - {message: /processing permissions\.0: Unknown permission "unknownPermission"/}, - {message: /processing unknown_property: An unexpected property was found in the WebExtension manifest/}, - ]; - - let waitForConsole = new Promise(resolve => { - SimpleTest.monitorConsole(resolve, messages); - }); - - yield extension.startup(); - - yield extension.awaitFinish("loaded"); - - yield extension.unload(); - - SimpleTest.endMonitorConsole(); - yield waitForConsole; -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_doublereply.html b/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_doublereply.html deleted file mode 100644 index a3ef37cad..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_doublereply.html +++ /dev/null @@ -1,101 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function background() { - // Add two listeners that both send replies. We're supposed to ignore all but one - // of them. Which one is chosen is non-deterministic. - - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); - - if (msg == "getreply") { - sendReply("reply1"); - } - }); - - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); - - if (msg == "getreply") { - sendReply("reply2"); - } - }); - - function sleep(callback, n = 10) { - if (n == 0) { - callback(); - } else { - setTimeout(function() { sleep(callback, n - 1); }, 0); - } - } - - let done_count = 0; - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); - - if (msg == "done") { - done_count++; - browser.test.assertEq(done_count, 1, "got exactly one reply"); - - // Go through the event loop a few times to make sure we don't get multiple replies. - sleep(function() { - browser.test.notifyPass("sendmessage_doublereply"); - }); - } - }); -} - -function contentScript() { - browser.runtime.sendMessage("getreply", function(resp) { - if (resp != "reply1" && resp != "reply2") { - return; // test failed - } - browser.runtime.sendMessage("done"); - }); -} - -let extensionData = { - background, - manifest: { - "permissions": ["tabs"], - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_start", - }], - }, - - files: { - "content_script.js": contentScript, - }, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]); - - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_no_receiver.html b/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_no_receiver.html deleted file mode 100644 index 96af6558e..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_no_receiver.html +++ /dev/null @@ -1,83 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>WebExtension test</title> - <meta charset="utf-8"> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> -<script> -"use strict"; - -function loadContentScriptExtension(contentScript) { - let extensionData = { - manifest: { - "content_scripts": [{ - "js": ["contentscript.js"], - "matches": ["http://mochi.test/*/file_sample.html"], - }], - }, - files: { - "contentscript.js": contentScript, - }, - }; - return ExtensionTestUtils.loadExtension(extensionData); -} - -add_task(function* test_content_script_sendMessage_without_listener() { - async function contentScript() { - await browser.test.assertRejects( - browser.runtime.sendMessage("msg"), - "Could not establish connection. Receiving end does not exist."); - - browser.test.notifyPass("sendMessage callback was invoked"); - } - - let extension = loadContentScriptExtension(contentScript); - yield extension.startup(); - - let win = window.open("file_sample.html"); - yield extension.awaitFinish("sendMessage callback was invoked"); - win.close(); - - yield extension.unload(); -}); - -add_task(function* test_content_script_chrome_sendMessage_without_listener() { - function contentScript() { - /* globals chrome */ - browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call"); - let retval = chrome.runtime.sendMessage("msg"); - browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call"); - // TODO(robwu): Fix the implementation and uncomment the next expectation. - // When content script APIs are schema-based (bugzil.la/1287007) this bug will be fixed for free. - // browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback"); - browser.test.assertTrue(retval instanceof Promise, "TODO: chrome.runtime.sendMessage should return undefined, not a promise"); - - let isAsyncCall = false; - retval = chrome.runtime.sendMessage("msg", reply => { - browser.test.assertEq(undefined, reply, "no reply"); - browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously"); - browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback"); - browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message); - browser.test.notifyPass("finished chrome.runtime.sendMessage"); - }); - isAsyncCall = true; - } - - let extension = loadContentScriptExtension(contentScript); - yield extension.startup(); - - let win = window.open("file_sample.html"); - yield extension.awaitFinish("finished chrome.runtime.sendMessage"); - win.close(); - - yield extension.unload(); -}); -</script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_reply.html b/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_reply.html deleted file mode 100644 index a4ac708b2..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_reply.html +++ /dev/null @@ -1,79 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function background() { - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct"); - - if (msg == 0) { - sendReply("reply1"); - } else if (msg == 1) { - window.setTimeout(function() { - sendReply("reply2"); - }, 0); - return true; - } else if (msg == 2) { - browser.test.notifyPass("sendmessage_reply"); - } - }); -} - -function contentScript() { - browser.runtime.sendMessage(0, function(resp1) { - if (resp1 != "reply1") { - return; // test failed - } - browser.runtime.sendMessage(1, function(resp2) { - if (resp2 != "reply2") { - return; // test failed - } - browser.runtime.sendMessage(2); - }); - }); -} - -let extensionData = { - background, - manifest: { - "permissions": ["tabs"], - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }], - }, - - files: { - "content_script.js": contentScript, - }, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let win = window.open("file_sample.html"); - - yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]); - - win.close(); - - yield extension.unload(); - info("extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_reply2.html b/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_reply2.html deleted file mode 100644 index 5c350be2f..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_sendmessage_reply2.html +++ /dev/null @@ -1,181 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -function backgroundScript(token, id, otherId) { - browser.tabs.create({url: "tab.html"}); - - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); - - if (msg === `content-${token}`) { - browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), - `${id}: sender url correct`); - - let tabId = sender.tab.id; - browser.tabs.sendMessage(tabId, `${token}-contentMessage`); - - sendReply(`${token}-done`); - } else if (msg === `tab-${token}`) { - browser.runtime.sendMessage(otherId, `${otherId}-tabMessage`); - browser.runtime.sendMessage(`${token}-tabMessage`); - - sendReply(`${token}-done`); - } else { - browser.test.fail(`${id}: Unexpected runtime message received: ${msg} ${uneval(sender)}`); - } - }); - - browser.runtime.onMessageExternal.addListener((msg, sender, sendReply) => { - browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`); - - if (msg === `content-${id}`) { - browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), - `${id}: external sender url correct`); - - sendReply(`${otherId}-done`); - } else if (msg === `tab-${id}`) { - sendReply(`${otherId}-done`); - } else if (msg !== `${id}-tabMessage`) { - browser.test.fail(`${id}: Unexpected runtime external message received: ${msg} ${uneval(sender)}`); - } - }); -} - -function contentScript(token, id, otherId) { - let gotContentMessage = false; - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); - - browser.test.assertEq(`${token}-contentMessage`, msg, - `${id}: Correct content script message`); - if (msg === `${token}-contentMessage`) { - gotContentMessage = true; - } - }); - - Promise.all([ - browser.runtime.sendMessage(otherId, `content-${otherId}`).then(resp => { - browser.test.assertEq(`${id}-done`, resp, `${id}: Correct content script external response token`); - }), - - browser.runtime.sendMessage(`content-${token}`).then(resp => { - browser.test.assertEq(`${token}-done`, resp, `${id}: Correct content script response token`); - }), - ]).then(() => { - browser.test.assertTrue(gotContentMessage, `${id}: Got content script message`); - - browser.test.sendMessage("content-script-done"); - }); -} - -function tabScript(token, id, otherId) { - let gotTabMessage = false; - browser.runtime.onMessage.addListener((msg, sender, sendReply) => { - browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`); - - if (String(msg).startsWith("content-")) { - return; - } - - browser.test.assertEq(`${token}-tabMessage`, msg, - `${id}: Correct tab script message`); - if (msg === `${token}-tabMessage`) { - gotTabMessage = true; - } - }); - - Promise.all([ - browser.runtime.sendMessage(otherId, `tab-${otherId}`).then(resp => { - browser.test.assertEq(`${id}-done`, resp, `${id}: Correct tab script external response token`); - }), - - browser.runtime.sendMessage(`tab-${token}`).then(resp => { - browser.test.assertEq(`${token}-done`, resp, `${id}: Correct tab script response token`); - }), - ]).then(() => { - browser.test.assertTrue(gotTabMessage, `${id}: Got tab script message`); - - window.close(); - - browser.test.sendMessage("tab-script-done"); - }); -} - -function makeExtension(id, otherId) { - let token = Math.random(); - - let args = `${token}, ${JSON.stringify(id)}, ${JSON.stringify(otherId)}`; - - let extensionData = { - background: `(${backgroundScript})(${args})`, - manifest: { - "applications": {"gecko": {id}}, - - "permissions": ["tabs"], - - - "content_scripts": [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_start", - }], - }, - - files: { - "tab.html": `<!DOCTYPE html> - <html> - <head> - <meta charset="utf-8"> - <script src="tab.js"><\/script> - </head> - </html>`, - - "tab.js": `(${tabScript})(${args})`, - - "content_script.js": `(${contentScript})(${args})`, - }, - }; - return extensionData; -} - -add_task(function* test_contentscript() { - const ID1 = "sendmessage1@mochitest.mozilla.org"; - const ID2 = "sendmessage2@mochitest.mozilla.org"; - - let extension1 = ExtensionTestUtils.loadExtension(makeExtension(ID1, ID2)); - let extension2 = ExtensionTestUtils.loadExtension(makeExtension(ID2, ID1)); - - yield Promise.all([extension1.startup(), extension2.startup()]); - - let win = window.open("file_sample.html"); - - yield waitForLoad(win); - - yield Promise.all([ - extension1.awaitMessage("content-script-done"), - extension2.awaitMessage("content-script-done"), - extension1.awaitMessage("tab-script-done"), - extension2.awaitMessage("tab-script-done"), - ]); - - win.close(); - - yield extension1.unload(); - yield extension2.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_storage_content.html b/toolkit/components/webextensions/test/mochitest/test_ext_storage_content.html deleted file mode 100644 index 09a33814a..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_storage_content.html +++ /dev/null @@ -1,330 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="application/javascript"> -"use strict"; - -// Copied from toolkit/components/extensions/test/xpcshell/test_ext_storage.js. -// The storage API in content scripts should behave identical to the storage API -// in background pages. -const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; -/** - * Utility function to ensure that all supported APIs for getting are - * tested. - * - * @param {string} areaName - * either "local" or "sync" according to what we want to test - * @param {string} prop - * "key" to look up using the storage API - * @param {Object} value - * "value" to compare against - */ -async function checkGetImpl(areaName, prop, value) { - let storage = browser.storage[areaName]; - - let data = await storage.get(null); - browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`); - - data = await storage.get(prop); - browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`); - - data = await storage.get([prop]); - browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`); - - data = await storage.get({[prop]: undefined}); - browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`); -} - -async function contentScript(checkGet) { - let globalChanges, gResolve; - function clearGlobalChanges() { - globalChanges = new Promise(resolve => { gResolve = resolve; }); - } - clearGlobalChanges(); - let expectedAreaName; - - browser.storage.onChanged.addListener((changes, areaName) => { - browser.test.assertEq(expectedAreaName, areaName, - "Expected area name received by listener"); - gResolve(changes); - }); - - async function checkChanges(areaName, changes, message) { - function checkSub(obj1, obj2) { - for (let prop in obj1) { - browser.test.assertTrue(obj1[prop] !== undefined, - `checkChanges ${areaName} ${prop} is missing (${message})`); - browser.test.assertTrue(obj2[prop] !== undefined, - `checkChanges ${areaName} ${prop} is missing (${message})`); - browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue, - `checkChanges ${areaName} ${prop} old (${message})`); - browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue, - `checkChanges ${areaName} ${prop} new (${message})`); - } - } - - const recentChanges = await globalChanges; - checkSub(changes, recentChanges); - checkSub(recentChanges, changes); - clearGlobalChanges(); - } - - /* eslint-disable dot-notation */ - async function runTests(areaName) { - expectedAreaName = areaName; - let storage = browser.storage[areaName]; - // Set some data and then test getters. - try { - await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); - await checkChanges(areaName, - {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}}, - "set (a)"); - - await checkGet(areaName, "test-prop1", "value1"); - await checkGet(areaName, "test-prop2", "value2"); - - let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"}); - browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)"); - browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)"); - browser.test.assertEq("default", data["other"], "other correct"); - - data = await storage.get(["test-prop1", "test-prop2", "other"]); - browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)"); - browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)"); - browser.test.assertFalse("other" in data, "other correct"); - - // Remove data in various ways. - await storage.remove("test-prop1"); - await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string"); - - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)"); - browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)"); - - await storage.set({"test-prop1": "value1"}); - await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)"); - - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)"); - browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)"); - - await storage.remove(["test-prop1", "test-prop2"]); - await checkChanges(areaName, - {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, - "remove array"); - - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)"); - browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)"); - - // test storage.clear - await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); - // Make sure that set() handler happened before we clear the - // promise again. - await globalChanges; - - clearGlobalChanges(); - await storage.clear(); - - await checkChanges(areaName, - {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, - "clear"); - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)"); - browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)"); - - // Make sure we can store complex JSON data. - // known previous values - await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); - - // Make sure the set() handler landed. - await globalChanges; - - clearGlobalChanges(); - await storage.set({ - "test-prop1": { - str: "hello", - bool: true, - null: null, - undef: undefined, - obj: {}, - arr: [1, 2], - date: new Date(0), - regexp: /regexp/, - func: function func() {}, - window, - }, - }); - - await storage.set({"test-prop2": function func() {}}); - const recentChanges = await globalChanges; - - browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct"); - browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj"); - clearGlobalChanges(); - - data = await storage.get({"test-prop1": undefined, "test-prop2": undefined}); - let obj = data["test-prop1"]; - - browser.test.assertEq("hello", obj.str, "string part correct"); - browser.test.assertEq(true, obj.bool, "bool part correct"); - browser.test.assertEq(null, obj.null, "null part correct"); - browser.test.assertEq(undefined, obj.undef, "undefined part correct"); - browser.test.assertEq(undefined, obj.func, "function part correct"); - browser.test.assertEq(undefined, obj.window, "window part correct"); - browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct"); - browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct"); - browser.test.assertEq("object", typeof(obj.obj), "object part correct"); - browser.test.assertTrue(Array.isArray(obj.arr), "array part present"); - browser.test.assertEq(1, obj.arr[0], "arr[0] part correct"); - browser.test.assertEq(2, obj.arr[1], "arr[1] part correct"); - browser.test.assertEq(2, obj.arr.length, "arr.length part correct"); - - obj = data["test-prop2"]; - - browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object"); - browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object"); - } catch (e) { - browser.test.fail(`Error: ${e} :: ${e.stack}`); - browser.test.notifyFail("storage"); - } - } - - browser.test.onMessage.addListener(msg => { - let promise; - if (msg === "test-local") { - promise = runTests("local"); - } else if (msg === "test-sync") { - promise = runTests("sync"); - } - promise.then(() => browser.test.sendMessage("test-finished")); - }); - - browser.test.sendMessage("ready"); -} - -let extensionData = { - manifest: { - content_scripts: [{ - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_idle", - }], - - permissions: ["storage"], - }, - - files: { - "content_script.js": `(${contentScript})(${checkGetImpl})`, - }, -}; - -add_task(function* test_contentscript() { - let win = window.open("file_sample.html"); - yield waitForLoad(win); - - yield SpecialPowers.pushPrefEnv({ - set: [[STORAGE_SYNC_PREF, true]], - }); - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); - extension.sendMessage("test-local"); - yield extension.awaitMessage("test-finished"); - - extension.sendMessage("test-sync"); - yield extension.awaitMessage("test-finished"); - - yield SpecialPowers.popPrefEnv(); - yield extension.unload(); - - win.close(); -}); - -add_task(function* test_local_cache_invalidation() { - let win = window.open("file_sample.html"); - - function background(checkGet) { - browser.test.onMessage.addListener(async msg => { - if (msg === "set-initial") { - await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"}); - browser.test.sendMessage("set-initial-done"); - } else if (msg === "check") { - await checkGet("local", "test-prop1", "value1"); - await checkGet("local", "test-prop2", "value2"); - browser.test.sendMessage("check-done"); - } - }); - - browser.test.sendMessage("ready"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["storage"], - }, - background: `(${background})(${checkGetImpl})`, - }); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - extension.sendMessage("set-initial"); - yield extension.awaitMessage("set-initial-done"); - - SpecialPowers.invalidateExtensionStorageCache(); - - extension.sendMessage("check"); - yield extension.awaitMessage("check-done"); - - yield extension.unload(); - win.close(); -}); - -add_task(function* test_config_flag_needed() { - let win = window.open("file_sample.html"); - yield waitForLoad(win); - - function background() { - let promises = []; - let apiTests = [ - {method: "get", args: ["foo"]}, - {method: "set", args: [{foo: "bar"}]}, - {method: "remove", args: ["foo"]}, - {method: "clear", args: []}, - ]; - apiTests.forEach(testDef => { - promises.push(browser.test.assertRejects( - browser.storage.sync[testDef.method](...testDef.args), - "Please set webextensions.storage.sync.enabled to true in about:config", - `storage.sync.${testDef.method} is behind a flag`)); - }); - - Promise.all(promises).then(() => browser.test.notifyPass("flag needed")); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["storage"], - }, - background: `(${background})(${checkGetImpl})`, - }); - - yield extension.startup(); - yield extension.awaitFinish("flag needed"); - yield extension.unload(); - win.close(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_storage_tab.html b/toolkit/components/webextensions/test/mochitest/test_ext_storage_tab.html deleted file mode 100644 index 32d8e6af0..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_storage_tab.html +++ /dev/null @@ -1,118 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_multiple_pages() { - async function background() { - let tabReady = new Promise(resolve => { - browser.runtime.onMessage.addListener(function listener(msg) { - browser.test.log("onMessage " + msg); - if (msg == "tab-ready") { - browser.runtime.onMessage.removeListener(listener); - resolve(); - } - }); - }); - - let tabId; - let tabRemoved = new Promise(resolve => { - browser.tabs.onRemoved.addListener(function listener(removedId) { - if (removedId == tabId) { - browser.tabs.onRemoved.removeListener(listener); - - // Delay long enough to be sure the inner window has been nuked. - setTimeout(resolve, 0); - } - }); - }); - - try { - let storage = browser.storage.local; - - browser.test.log("create"); - let tab = await browser.tabs.create({url: "tab.html"}); - tabId = tab.id; - - await tabReady; - - let result = await storage.get("key"); - browser.test.assertEq(undefined, result.key, "Key should be undefined"); - - await browser.runtime.sendMessage("tab-set-key"); - - result = await storage.get("key"); - browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}), - JSON.stringify(result.key), - "Key should be set to the value from the tab"); - - browser.test.log("Remove tab"); - - await Promise.all([ - browser.tabs.remove(tabId), - tabRemoved, - ]); - - result = await storage.get("key"); - browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}), - JSON.stringify(result.key), - "Key should still be set to the value from the tab"); - - browser.test.notifyPass("storage-multiple"); - } catch (e) { - browser.test.fail(`Error: ${e} :: ${e.stack}`); - browser.test.notifyFail("storage-multiple"); - } - } - - function tab() { - browser.test.log("tab"); - browser.runtime.onMessage.addListener(msg => { - if (msg == "tab-set-key") { - return browser.storage.local.set({key: {foo: {bar: "baz"}}}); - } - }); - - browser.runtime.sendMessage("tab-ready"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - - files: { - "tab.html": `<!DOCTYPE html> - <html> - <head> - <meta charset="utf-8"> - <script src="tab.js"><\/script> - </head> - </html>`, - - "tab.js": tab, - }, - - manifest: { - permissions: ["storage"], - }, - }); - - yield extension.startup(); - - yield extension.awaitFinish("storage-multiple"); - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_subframes_privileges.html b/toolkit/components/webextensions/test/mochitest/test_ext_subframes_privileges.html deleted file mode 100644 index 1f3a9a3c9..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_subframes_privileges.html +++ /dev/null @@ -1,202 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtension test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_webext_tab_subframe_privileges() { - function background() { - browser.runtime.onMessage.addListener(async ({msg, success, tabId, error}) => { - if (msg == "webext-tab-subframe-privileges") { - if (success) { - await browser.tabs.remove(tabId); - - browser.test.notifyPass(msg); - } else { - browser.test.log(`Got an unexpected error: ${error}`); - - let tabs = await browser.tabs.query({active: true}); - await browser.tabs.remove(tabs[0].id); - - browser.test.notifyFail(msg); - } - } - }); - browser.tabs.create({url: browser.runtime.getURL("/tab.html")}); - } - - async function tabSubframeScript() { - browser.test.assertTrue(browser.tabs != undefined, - "Subframe of a privileged page has access to privileged APIs"); - if (browser.tabs) { - try { - let tab = await browser.tabs.getCurrent(); - browser.runtime.sendMessage({ - msg: "webext-tab-subframe-privileges", - success: true, - tabId: tab.id, - }); - } catch (e) { - browser.runtime.sendMessage({msg: "webext-tab-subframe-privileges", success: false, error: `${e}`}); - } - } else { - browser.runtime.sendMessage({ - msg: "webext-tab-subframe-privileges", - success: false, - error: `Privileged APIs missing in WebExtension tab sub-frame`, - }); - } - } - - let extensionData = { - background, - files: { - "tab.html": `<!DOCTYPE> - <head> - <meta charset="utf-8"> - </head> - <body> - <iframe src="tab-subframe.html"></iframe> - </body> - </html>`, - "tab-subframe.html": `<!DOCTYPE> - <head> - <meta charset="utf-8"> - <script src="tab-subframe.js"><\/script> - </head> - </html>`, - "tab-subframe.js": tabSubframeScript, - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - yield extension.awaitFinish("webext-tab-subframe-privileges"); - yield extension.unload(); -}); - -add_task(function* test_webext_background_subframe_privileges() { - function backgroundSubframeScript() { - browser.test.assertTrue(browser.tabs != undefined, - "Subframe of a background page has access to privileged APIs"); - browser.test.notifyPass("webext-background-subframe-privileges"); - } - - let extensionData = { - manifest: { - background: { - page: "background.html", - }, - }, - files: { - "background.html": `<!DOCTYPE> - <head> - <meta charset="utf-8"> - </head> - <body> - <iframe src="background-subframe.html"></iframe> - </body> - </html>`, - "background-subframe.html": `<!DOCTYPE> - <head> - <meta charset="utf-8"> - <script src="background-subframe.js"><\/script> - </head> - </html>`, - "background-subframe.js": backgroundSubframeScript, - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - yield extension.awaitFinish("webext-background-subframe-privileges"); - yield extension.unload(); -}); - -add_task(function* test_webext_contentscript_iframe_subframe_privileges() { - function background() { - browser.runtime.onMessage.addListener(({name, hasTabsAPI, hasStorageAPI}) => { - if (name == "contentscript-iframe-loaded") { - browser.test.assertFalse(hasTabsAPI, - "Subframe of a content script privileged iframes has no access to privileged APIs"); - browser.test.assertTrue(hasStorageAPI, - "Subframe of a content script privileged iframes has access to content script APIs"); - - browser.test.notifyPass("webext-contentscript-subframe-privileges"); - } - }); - } - - function subframeScript() { - browser.runtime.sendMessage({ - name: "contentscript-iframe-loaded", - hasTabsAPI: browser.tabs != undefined, - hasStorageAPI: browser.storage != undefined, - }); - } - - function contentScript() { - let iframe = document.createElement("iframe"); - iframe.setAttribute("src", browser.runtime.getURL("/contentscript-iframe.html")); - document.body.appendChild(iframe); - } - - let extensionData = { - background, - manifest: { - "permissions": ["storage"], - "content_scripts": [{ - "matches": ["http://example.com/*"], - "js": ["contentscript.js"], - }], - web_accessible_resources: [ - "contentscript-iframe.html", - ], - }, - files: { - "contentscript.js": contentScript, - "contentscript-iframe.html": `<!DOCTYPE> - <head> - <meta charset="utf-8"> - </head> - <body> - <iframe src="contentscript-iframe-subframe.html"></iframe> - </body> - </html>`, - "contentscript-iframe-subframe.html": `<!DOCTYPE> - <head> - <meta charset="utf-8"> - <script src="contentscript-iframe-subframe.js"><\/script> - </head> - </html>`, - "contentscript-iframe-subframe.js": subframeScript, - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - let win = window.open("http://example.com"); - - yield extension.awaitFinish("webext-contentscript-subframe-privileges"); - - win.close(); - - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_tab_teardown.html b/toolkit/components/webextensions/test/mochitest/test_ext_tab_teardown.html deleted file mode 100644 index dc351e48a..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_tab_teardown.html +++ /dev/null @@ -1,150 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for extension tab teardown</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -// Test for tabs opened using tabs.create and window.open -function* runTabReloadAndCloseTest(extension) { - let chromeScript = SpecialPowers.loadChromeScript( - SimpleTest.getTestFileURL("file_teardown_test.js")); - yield chromeScript.promiseOneMessage("chromescript-startup"); - function* getContextEvents() { - chromeScript.sendAsyncMessage("get-context-events"); - let contextEvents = yield chromeScript.promiseOneMessage("context-events"); - dump(JSON.stringify(contextEvents)); - return contextEvents.filter(event => event.extensionId == extension.id); - } - - extension.sendMessage("open extension page"); - let extensionPageUrl = yield extension.awaitMessage("extension page loaded"); - - let contextEvents = yield* getContextEvents(); - is(contextEvents.length, 1, "ExtensionContext change for opening a tab"); - is(contextEvents[0].eventType, "load", "create ExtensionContext for tab"); - is(contextEvents[0].url, extensionPageUrl, - "ExtensionContext URL after tab creation should be tab URL"); - - extension.sendMessage("reload extension page"); - let extensionPageUrl2 = yield extension.awaitMessage("extension page loaded"); - - is(extensionPageUrl, extensionPageUrl2, - "The tab's URL is expected to not change after a page reload"); - - contextEvents = yield* getContextEvents(); - is(contextEvents.length, 2, "ExtensionContext change after tab reload"); - is(contextEvents[0].eventType, "unload", "unload old ExtensionContext"); - is(contextEvents[0].url, extensionPageUrl, - "ExtensionContext URL before reload should be tab URL"); - is(contextEvents[1].eventType, "load", "create new ExtensionContext for tab"); - is(contextEvents[1].url, extensionPageUrl2, - "ExtensionContext URL after reload should be tab URL"); - - extension.sendMessage("close extension page"); - yield extension.awaitMessage("closed extension page"); - - contextEvents = yield* getContextEvents(); - is(contextEvents.length, 1, "ExtensionContext after closing tab"); - is(contextEvents[0].eventType, "unload", "unload tab's ExtensionContext"); - is(contextEvents[0].url, extensionPageUrl2, - "ExtensionContext URL at closing tab should be tab URL"); - - chromeScript.sendAsyncMessage("cleanup"); - chromeScript.destroy(); - yield extension.unload(); -} - -add_task(function* test_extension_page_tabs_create_reload_and_close() { - function background() { - let tabId; - browser.test.onMessage.addListener(msg => { - if (msg === "open extension page") { - chrome.tabs.create({url: "page.html"}, tab => { - tabId = tab.id; - }); - } else if (msg === "reload extension page") { - chrome.tabs.reload(tabId); - } else if (msg === "close extension page") { - chrome.tabs.remove(tabId, () => { - browser.test.sendMessage("closed extension page"); - }); - } - }); - } - - function pageScript() { - browser.test.sendMessage("extension page loaded", document.URL); - } - - let extensionData = { - background, - files: { - "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`, - "page.js": pageScript, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield* runTabReloadAndCloseTest(extension); -}); - -add_task(function* test_extension_page_window_open_reload_and_close() { - // This tests whether a context that is opened via window.open is properly - // disposed when the tab closes. - // The background page cannot use window.open (bugzil.la/1282021), so we open - // another extension page that manages the window.open-tab for testing. - function background() { - chrome.tabs.create({url: "window.open.html"}); - } - - function windowOpenScript() { - let win; - browser.test.onMessage.addListener(msg => { - if (msg === "open extension page") { - win = window.open("page.html"); - } else if (msg === "reload extension page") { - win.location.reload(); - } else if (msg === "close extension page") { - browser.tabs.onRemoved.addListener(function listener() { - browser.tabs.onRemoved.removeListener(listener); - browser.test.sendMessage("closed extension page"); - }); - win.close(); - } - }); - browser.test.sendMessage("setup-intermediate-tab"); - } - - function pageScript() { - browser.test.sendMessage("extension page loaded", document.URL); - } - - let extensionData = { - background, - files: { - "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`, - "page.js": pageScript, - "window.open.html": `<!DOCTYPE html><meta charset="utf-8"><script src="window.open.js"><\/script>`, - "window.open.js": windowOpenScript, - }, - }; - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitMessage("setup-intermediate-tab"); - yield* runTabReloadAndCloseTest(extension); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_test.html b/toolkit/components/webextensions/test/mochitest/test_ext_test.html deleted file mode 100644 index fef31e0e2..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_test.html +++ /dev/null @@ -1,191 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Testing test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -function loadExtensionAndInterceptTest(extensionData) { - let results = []; - let testResolve; - let testDone = new Promise(resolve => { testResolve = resolve; }); - let handler = { - testResult(...result) { - result.pop(); - results.push(result); - SimpleTest.info(`Received test result: ${JSON.stringify(result)}`); - }, - - testMessage(msg, ...args) { - results.push(["test-message", msg, ...args]); - SimpleTest.info(`Received message: ${msg} ${JSON.stringify(args)}`); - if (msg === "This is the last browser.test call") { - testResolve(); - } - }, - }; - let extension = SpecialPowers.loadExtension(extensionData, handler); - SimpleTest.registerCleanupFunction(() => { - if (extension.state == "pending" || extension.state == "running") { - SimpleTest.ok(false, "Extension left running at test shutdown"); - return extension.unload(); - } else if (extension.state == "unloading") { - SimpleTest.ok(false, "Extension not fully unloaded at test shutdown"); - } - }); - extension.awaitResults = () => testDone.then(() => results); - return extension; -} - -function testScript() { - // Note: The result of these browser.test calls are intercepted by the test. - // See verifyTestResults for the expectations of each browser.test call. - browser.test.notifyPass("dot notifyPass"); - browser.test.notifyFail("dot notifyFail"); - browser.test.log("dot log"); - browser.test.fail("dot fail"); - browser.test.succeed("dot succeed"); - browser.test.assertTrue(true); - browser.test.assertFalse(false); - browser.test.assertEq("", ""); - - let obj = {}; - let arr = []; - let dom = document.createElement("body"); - browser.test.assertTrue(obj, "Object truthy"); - browser.test.assertTrue(arr, "Array truthy"); - browser.test.assertTrue(dom, "Element truthy"); - browser.test.assertTrue(true, "True truthy"); - browser.test.assertTrue(false, "False truthy"); - browser.test.assertTrue(null, "Null truthy"); - browser.test.assertTrue(undefined, "Void truthy"); - browser.test.assertTrue(false, document.createElement("html")); - - browser.test.assertFalse(obj, "Object falsey"); - browser.test.assertFalse(arr, "Array falsey"); - browser.test.assertFalse(dom, "Element falsey"); - browser.test.assertFalse(true, "True falsey"); - browser.test.assertFalse(false, "False falsey"); - browser.test.assertFalse(null, "Null falsey"); - browser.test.assertFalse(undefined, "Void falsey"); - browser.test.assertFalse(true, document.createElement("head")); - - browser.test.assertEq(obj, obj, "Object equality"); - browser.test.assertEq(arr, arr, "Array equality"); - browser.test.assertEq(dom, dom, "Element equality"); - browser.test.assertEq(null, null, "Null equality"); - browser.test.assertEq(undefined, undefined, "Void equality"); - - browser.test.assertEq({}, {}, "Object reference ineqality"); - browser.test.assertEq([], [], "Array reference ineqality"); - browser.test.assertEq(dom, document.createElement("body"), "Element ineqality"); - browser.test.assertEq(null, undefined, "Null and void ineqality"); - browser.test.assertEq(true, false, document.createElement("div")); - - obj = { - toString() { - return "Dynamic toString forbidden"; - }, - }; - browser.test.assertEq(obj, obj, "obj with dynamic toString()"); - browser.test.sendMessage("Ran test at", location.protocol); - browser.test.sendMessage("This is the last browser.test call"); -} - -function verifyTestResults(results, shortName, expectedProtocol) { - let expectations = [ - ["test-done", true, "dot notifyPass"], - ["test-done", false, "dot notifyFail"], - ["test-log", true, "dot log"], - ["test-result", false, "dot fail"], - ["test-result", true, "dot succeed"], - ["test-result", true, "undefined"], - ["test-result", true, "undefined"], - ["test-eq", true, "undefined", "", ""], - - ["test-result", true, "Object truthy"], - ["test-result", true, "Array truthy"], - ["test-result", true, "Element truthy"], - ["test-result", true, "True truthy"], - ["test-result", false, "False truthy"], - ["test-result", false, "Null truthy"], - ["test-result", false, "Void truthy"], - ["test-result", false, "[object HTMLHtmlElement]"], - - ["test-result", false, "Object falsey"], - ["test-result", false, "Array falsey"], - ["test-result", false, "Element falsey"], - ["test-result", false, "True falsey"], - ["test-result", true, "False falsey"], - ["test-result", true, "Null falsey"], - ["test-result", true, "Void falsey"], - ["test-result", false, "[object HTMLHeadElement]"], - - ["test-eq", true, "Object equality", "[object Object]", "[object Object]"], - ["test-eq", true, "Array equality", "", ""], - ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"], - ["test-eq", true, "Null equality", "null", "null"], - ["test-eq", true, "Void equality", "undefined", "undefined"], - - ["test-eq", false, "Object reference ineqality", "[object Object]", "[object Object] (different)"], - ["test-eq", false, "Array reference ineqality", "", " (different)"], - ["test-eq", false, "Element ineqality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"], - ["test-eq", false, "Null and void ineqality", "null", "undefined"], - ["test-eq", false, "[object HTMLDivElement]", "true", "false"], - - ["test-eq", true, "obj with dynamic toString()", "[object Object]", "[object Object]"], - - ["test-message", "Ran test at", expectedProtocol], - ["test-message", "This is the last browser.test call"], - ]; - - expectations.forEach((expectation, i) => { - let msg = expectation.slice(2).join(" - "); - isDeeply(results[i], expectation, `${shortName} (${msg})`); - }); - is(results[expectations.length], undefined, "No more results"); -} - -add_task(function* test_test_in_background() { - let extensionData = { - background: `(${testScript})()`, - }; - - let extension = loadExtensionAndInterceptTest(extensionData); - yield extension.startup(); - let results = yield extension.awaitResults(); - verifyTestResults(results, "background page", "moz-extension:"); - yield extension.unload(); -}); - -add_task(function* test_test_in_content_script() { - let extensionData = { - manifest: { - content_scripts: [{ - matches: ["http://mochi.test/*/file_sample.html"], - js: ["contentscript.js"], - }], - }, - files: { - "contentscript.js": `(${testScript})()`, - }, - }; - - let extension = loadExtensionAndInterceptTest(extensionData); - yield extension.startup(); - let win = window.open("file_sample.html"); - let results = yield extension.awaitResults(); - win.close(); - verifyTestResults(results, "content script", "http:"); - yield extension.unload(); -}); -</script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_unload_frame.html b/toolkit/components/webextensions/test/mochitest/test_ext_unload_frame.html deleted file mode 100644 index 5572de281..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_unload_frame.html +++ /dev/null @@ -1,170 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>WebExtensions test</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> -</head> -<body> - -<script> -"use strict"; - -/* globals delayedNotifyPass */ // Available in the background page of the test extensions. - -// Background and content script for testSendMessage_* -function sendMessage_background() { - browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { - browser.test.assertEq("from frame", msg, "Expected message from frame"); - sendResponse("msg from back"); // Should not throw or anything like that. - delayedNotifyPass("Received sendMessage from closing frame"); - }); -} -function sendMessage_contentScript(testType) { - browser.runtime.sendMessage("from frame", reply => { - // The frame has been removed, so we should not get this callback! - browser.test.fail(`Unexpected reply: ${reply}`); - }); - if (testType == "frame") { - frameElement.remove(); - } else { - window.close(); - } -} - -// Background and content script for testConnect_* -function connect_background() { - browser.runtime.onConnect.addListener(port => { - browser.test.assertEq("port from frame", port.name); - - let disconnected = false; - let hasMessage = false; - port.onDisconnect.addListener(() => { - browser.test.assertFalse(disconnected, "onDisconnect should fire once"); - disconnected = true; - browser.test.assertTrue(hasMessage, "Expected onMessage before onDisconnect"); - browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads"); - delayedNotifyPass("Received onDisconnect from closing frame"); - }); - port.onMessage.addListener(msg => { - browser.test.assertFalse(hasMessage, "onMessage should fire once"); - hasMessage = true; - browser.test.assertFalse(disconnected, "Should get message before disconnect"); - browser.test.assertEq("from frame", msg, "Expected message from frame"); - }); - - port.postMessage("reply to closing frame"); - }); -} -function connect_contentScript(testType) { - let isUnloading = false; - addEventListener("pagehide", () => { isUnloading = true; }, {once: true}); - - let port = browser.runtime.connect({name: "port from frame"}); - port.onMessage.addListener(msg => { - // The background page sends a reply as soon as we call runtime.connect(). - // It is possible that the reply reaches this frame before the - // window.close() request has been processed. - if (!isUnloading) { - browser.test.log(`Ignorting unexpected reply ("${msg}") because the page is not being unloaded.`); - return; - } - - // The frame has been removed, so we should not get a reply. - browser.test.fail(`Unexpected reply: ${msg}`); - }); - port.postMessage("from frame"); - - // Removing the frame or window should disconnect the port. - if (testType == "frame") { - frameElement.remove(); - } else { - window.close(); - } -} - -// `testType` is "window" or "frame". -function createTestExtension(testType, backgroundScript, contentScript) { - // Make a roundtrip between the background page and the test runner (which is - // in the same process as the content script) to make sure that we record a - // failure in case the content script's sendMessage or onMessage handlers are - // called even after the frame or window was removed. - function delayedNotifyPass(msg) { - browser.test.onMessage.addListener((type, echoMsg) => { - if (type == "pong") { - browser.test.assertEq(msg, echoMsg, "Echoed reply should be the same"); - browser.test.notifyPass(msg); - } - }); - browser.test.log("Starting ping-pong to flush messages..."); - browser.test.sendMessage("ping", msg); - } - let extension = ExtensionTestUtils.loadExtension({ - background: `${delayedNotifyPass};(${backgroundScript})();`, - manifest: { - content_scripts: [{ - js: ["contentscript.js"], - all_frames: testType == "frame", - matches: ["http://mochi.test/*/file_sample.html"], - }], - }, - files: { - "contentscript.js": `(${contentScript})("${testType}");`, - }, - }); - extension.awaitMessage("ping").then(msg => { - extension.sendMessage("pong", msg); - }); - return extension; -} - -add_task(function* testSendMessage_and_remove_frame() { - let extension = createTestExtension("frame", sendMessage_background, sendMessage_contentScript); - yield extension.startup(); - - let frame = document.createElement("iframe"); - frame.src = "file_sample.html"; - document.body.appendChild(frame); - - yield extension.awaitFinish("Received sendMessage from closing frame"); - yield extension.unload(); -}); - -add_task(function* testConnect_and_remove_frame() { - let extension = createTestExtension("frame", connect_background, connect_contentScript); - yield extension.startup(); - - let frame = document.createElement("iframe"); - frame.src = "file_sample.html"; - document.body.appendChild(frame); - - yield extension.awaitFinish("Received onDisconnect from closing frame"); - yield extension.unload(); -}); - -add_task(function* testSendMessage_and_remove_window() { - let extension = createTestExtension("window", sendMessage_background, sendMessage_contentScript); - yield extension.startup(); - - window.open("file_sample.html"); - - yield extension.awaitFinish("Received sendMessage from closing frame"); - yield extension.unload(); -}); - -add_task(function* testConnect_and_remove_window() { - let extension = createTestExtension("window", connect_background, connect_contentScript); - yield extension.startup(); - - window.open("file_sample.html"); - - yield extension.awaitFinish("Received onDisconnect from closing frame"); - yield extension.unload(); -}); - -</script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_web_accessible_resources.html b/toolkit/components/webextensions/test/mochitest/test_ext_web_accessible_resources.html deleted file mode 100644 index fa3228739..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_web_accessible_resources.html +++ /dev/null @@ -1,353 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test the web_accessible_resources manifest directive</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/* eslint-disable mozilla/balanced-listeners */ - -SimpleTest.registerCleanupFunction(() => { - SpecialPowers.clearUserPref("security.mixed_content.block_display_content"); -}); - -let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + - "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); -const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer; - -async function testImageLoading(src, expectedAction) { - let imageLoadingPromise = new Promise((resolve, reject) => { - let cleanupListeners; - let testImage = document.createElement("img"); - testImage.setAttribute("src", src); - - let loadListener = () => { - cleanupListeners(); - resolve(expectedAction === "loaded"); - }; - - let errorListener = () => { - cleanupListeners(); - resolve(expectedAction === "blocked"); - }; - - cleanupListeners = () => { - testImage.removeEventListener("load", loadListener); - testImage.removeEventListener("error", errorListener); - }; - - testImage.addEventListener("load", loadListener); - testImage.addEventListener("error", errorListener); - - document.body.appendChild(testImage); - }); - - let success = await imageLoadingPromise; - browser.runtime.sendMessage({name: "image-loading", expectedAction, success}); -} - -add_task(function* test_web_accessible_resources() { - function background() { - let gotURL; - let tabId; - - function loadFrame(url) { - return new Promise(resolve => { - browser.tabs.sendMessage(tabId, ["load-iframe", url], reply => { - resolve(reply); - }); - }); - } - - let urls = [ - [browser.extension.getURL("accessible.html"), true], - [browser.extension.getURL("accessible.html") + "?foo=bar", true], - [browser.extension.getURL("accessible.html") + "#!foo=bar", true], - [browser.extension.getURL("forbidden.html"), false], - [browser.extension.getURL("wild1.html"), true], - [browser.extension.getURL("wild2.htm"), false], - ]; - - async function runTests() { - for (let [url, shouldLoad] of urls) { - let success = await loadFrame(url); - - browser.test.assertEq(shouldLoad, success, "Load was successful"); - if (shouldLoad) { - browser.test.assertEq(url, gotURL, "Got expected url"); - } else { - browser.test.assertEq(undefined, gotURL, "Got no url"); - } - gotURL = undefined; - } - - browser.test.notifyPass("web-accessible-resources"); - } - - browser.runtime.onMessage.addListener(([msg, url], sender) => { - if (msg == "content-script-ready") { - tabId = sender.tab.id; - runTests(); - } else if (msg == "page-script") { - browser.test.assertEq(undefined, gotURL, "Should have gotten only one message"); - browser.test.assertEq("string", typeof(url), "URL should be a string"); - gotURL = url; - } - }); - - browser.test.sendMessage("ready"); - } - - function contentScript() { - browser.runtime.onMessage.addListener(([msg, url], sender, respond) => { - if (msg == "load-iframe") { - let iframe = document.createElement("iframe"); - iframe.setAttribute("src", url); - iframe.addEventListener("load", () => { respond(true); }); - iframe.addEventListener("error", () => { respond(false); }); - document.body.appendChild(iframe); - return true; - } - }); - browser.runtime.sendMessage(["content-script-ready"]); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - content_scripts: [ - { - "matches": ["http://example.com/"], - "js": ["content_script.js"], - "run_at": "document_idle", - }, - ], - - "web_accessible_resources": [ - "/accessible.html", - "wild*.html", - ], - }, - - background, - - files: { - "content_script.js": contentScript, - - "accessible.html": `<html><head> - <meta charset="utf-8"> - <script src="accessible.js"><\/script> - </head></html>`, - - "accessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);', - - "inaccessible.html": `<html><head> - <meta charset="utf-8"> - <script src="inaccessible.js"><\/script> - </head></html>`, - - "inaccessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);', - - "wild1.html": `<html><head> - <meta charset="utf-8"> - <script src="wild.js"><\/script> - </head></html>`, - - "wild2.htm": `<html><head> - <meta charset="utf-8"> - <script src="wild.js"><\/script> - </head></html>`, - - "wild.js": 'browser.runtime.sendMessage(["page-script", location.href]);', - }, - }); - - yield extension.startup(); - - yield extension.awaitMessage("ready"); - - let win = window.open("http://example.com/"); - - yield extension.awaitFinish("web-accessible-resources"); - - win.close(); - - yield extension.unload(); -}); - -add_task(function* test_web_accessible_resources_csp() { - function background() { - browser.runtime.onMessage.addListener((msg, sender) => { - if (msg.name === "image-loading") { - browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`); - browser.test.sendMessage(`image-${msg.expectedAction}`); - } else { - browser.test.sendMessage(msg); - } - }); - - browser.test.sendMessage("background-ready"); - } - - function content() { - window.addEventListener("message", function rcv(event) { - browser.runtime.sendMessage("script-ran"); - window.removeEventListener("message", rcv, false); - }, false); - - testImageLoading(browser.extension.getURL("image.png"), "loaded"); - - let testScriptElement = document.createElement("script"); - testScriptElement.setAttribute("src", browser.extension.getURL("test_script.js")); - document.head.appendChild(testScriptElement); - browser.runtime.sendMessage("script-loaded"); - } - - function testScript() { - window.postMessage("test-script-loaded", "*"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "content_scripts": [{ - "matches": ["http://example.com/*/file_csp.html"], - "run_at": "document_start", - "js": ["content_script_helper.js", "content_script.js"], - }], - "web_accessible_resources": [ - "image.png", - "test_script.js", - ], - }, - background, - files: { - "content_script_helper.js": `${testImageLoading}`, - "content_script.js": content, - "test_script.js": testScript, - "image.png": IMAGE_ARRAYBUFFER, - }, - }); - - // This is used to watch the blocked data bounce off CSP. - function examiner() { - SpecialPowers.addObserver(this, "csp-on-violate-policy", false); - } - - let cspEventCount = 0; - - examiner.prototype = { - observe: function(subject, topic, data) { - cspEventCount++; - let spec = SpecialPowers.wrap(subject).QueryInterface(SpecialPowers.Ci.nsIURI).spec; - ok(spec.includes("file_image_bad.png") || spec.includes("file_script_bad.js"), - `Expected file: ${spec} rejected by CSP`); - }, - - // We must eventually call this to remove the listener, - // or mochitests might get borked. - remove: function() { - SpecialPowers.removeObserver(this, "csp-on-violate-policy"); - }, - }; - - let observer = new examiner(); - - yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]); - - let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_csp.html"); - - yield Promise.all([ - extension.awaitMessage("image-loaded"), - extension.awaitMessage("script-loaded"), - extension.awaitMessage("script-ran"), - ]); - is(cspEventCount, 2, "Two items were rejected by CSP"); - win.close(); - - observer.remove(); - yield extension.unload(); -}); - -add_task(function* test_web_accessible_resources_mixed_content() { - function background() { - browser.runtime.onMessage.addListener(msg => { - if (msg.name === "image-loading") { - browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`); - browser.test.sendMessage(`image-${msg.expectedAction}`); - } else { - browser.test.sendMessage(msg); - if (msg === "accessible-script-loaded") { - browser.test.notifyPass("mixed-test"); - } - } - }); - - browser.test.sendMessage("background-ready"); - } - - function content() { - testImageLoading("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", "blocked"); - testImageLoading(browser.extension.getURL("image.png"), "loaded"); - - let testScriptElement = document.createElement("script"); - testScriptElement.setAttribute("src", browser.extension.getURL("test_script.js")); - document.head.appendChild(testScriptElement); - - window.addEventListener("message", event => { - browser.runtime.sendMessage(event.data); - }); - } - - function testScript() { - window.postMessage("accessible-script-loaded", "*"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "content_scripts": [{ - "matches": ["https://example.com/*/file_mixed.html"], - "run_at": "document_start", - "js": ["content_script_helper.js", "content_script.js"], - }], - "web_accessible_resources": [ - "image.png", - "test_script.js", - ], - }, - background, - files: { - "content_script_helper.js": `${testImageLoading}`, - "content_script.js": content, - "test_script.js": testScript, - "image.png": IMAGE_ARRAYBUFFER, - }, - }); - - SpecialPowers.setBoolPref("security.mixed_content.block_display_content", true); - - yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]); - - let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html"); - - yield Promise.all([ - extension.awaitMessage("image-blocked"), - extension.awaitMessage("image-loaded"), - extension.awaitMessage("accessible-script-loaded"), - ]); - yield extension.awaitFinish("mixed-test"); - win.close(); - - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_webnavigation.html b/toolkit/components/webextensions/test/mochitest/test_ext_webnavigation.html deleted file mode 100644 index 2287fd9b1..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_webnavigation.html +++ /dev/null @@ -1,559 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for simple WebExtension</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/* globals sendMouseEvent */ - -function backgroundScript() { - const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; - const URL = BASE + "/file_WebNavigation_page1.html"; - - const EVENTS = [ - "onTabReplaced", - "onBeforeNavigate", - "onCommitted", - "onDOMContentLoaded", - "onCompleted", - "onErrorOccurred", - "onReferenceFragmentUpdated", - "onHistoryStateUpdated", - ]; - - let expectedTabId = -1; - - function gotEvent(event, details) { - if (!details.url.startsWith(BASE)) { - return; - } - browser.test.log(`Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`); - - if (expectedTabId == -1) { - browser.test.assertTrue(details.tabId !== undefined, "tab ID defined"); - expectedTabId = details.tabId; - } - - browser.test.assertEq(details.tabId, expectedTabId, "correct tab"); - - browser.test.sendMessage("received", {url: details.url, event}); - - if (details.url == URL) { - browser.test.assertEq(details.frameId, 0, "root frame ID correct"); - browser.test.assertEq(details.parentFrameId, -1, "root parent frame ID correct"); - } else { - browser.test.assertEq(details.parentFrameId, 0, "parent frame ID correct"); - browser.test.assertTrue(details.frameId != 0, "frame ID probably okay"); - } - - browser.test.assertTrue(details.frameId !== undefined); - browser.test.assertTrue(details.parentFrameId !== undefined); - } - - let listeners = {}; - for (let event of EVENTS) { - listeners[event] = gotEvent.bind(null, event); - browser.webNavigation[event].addListener(listeners[event]); - } - - browser.test.sendMessage("ready"); -} - -const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; -const URL = BASE + "/file_WebNavigation_page1.html"; -const FRAME = BASE + "/file_WebNavigation_page2.html"; -const FRAME2 = BASE + "/file_WebNavigation_page3.html"; -const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html"; -const REDIRECT = BASE + "/redirection.sjs"; -const REDIRECTED = BASE + "/dummy_page.html"; -const CLIENT_REDIRECT = BASE + "/file_webNavigation_clientRedirect.html"; -const CLIENT_REDIRECT_HTTPHEADER = BASE + "/file_webNavigation_clientRedirect_httpHeaders.html"; -const FRAME_CLIENT_REDIRECT = BASE + "/file_webNavigation_frameClientRedirect.html"; -const FRAME_REDIRECT = BASE + "/file_webNavigation_frameRedirect.html"; -const FRAME_MANUAL = BASE + "/file_webNavigation_manualSubframe.html"; -const FRAME_MANUAL_PAGE1 = BASE + "/file_webNavigation_manualSubframe_page1.html"; -const FRAME_MANUAL_PAGE2 = BASE + "/file_webNavigation_manualSubframe_page2.html"; -const INVALID_PAGE = "https://invalid.localhost/"; - -const REQUIRED = [ - "onBeforeNavigate", - "onCommitted", - "onDOMContentLoaded", - "onCompleted", -]; - -var received = []; -var completedResolve; -var waitingURL, waitingEvent; - -function loadAndWait(win, event, url, script) { - received = []; - waitingEvent = event; - waitingURL = url; - dump(`RUN ${script}\n`); - script(); - return new Promise(resolve => { completedResolve = resolve; }); -} - -add_task(function* webnav_transitions_props() { - function backgroundScriptTransitions() { - const EVENTS = [ - "onCommitted", - "onCompleted", - ]; - - function gotEvent(event, details) { - browser.test.log(`Got ${event} ${details.url} ${details.transitionType} ${details.transitionQualifiers && JSON.stringify(details.transitionQualifiers)}`); - - browser.test.sendMessage("received", {url: details.url, details, event}); - } - - let listeners = {}; - for (let event of EVENTS) { - listeners[event] = gotEvent.bind(null, event); - browser.webNavigation[event].addListener(listeners[event]); - } - - browser.test.sendMessage("ready"); - } - - let extensionData = { - manifest: { - permissions: [ - "webNavigation", - ], - }, - background: backgroundScriptTransitions, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - extension.onMessage("received", ({url, event, details}) => { - received.push({url, event, details}); - - if (event == waitingEvent && url == waitingURL) { - completedResolve(); - } - }); - - yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); - info("webnavigation extension loaded"); - - let win = window.open(); - - yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; }); - - // transitionType: reload - received = []; - yield loadAndWait(win, "onCompleted", URL, () => { win.location.reload(); }); - - let found = received.find((data) => (data.event == "onCommitted" && data.url == URL)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "reload", - "Got the expected 'reload' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers), - "transitionQualifiers found in the OnCommitted events"); - } - - // transitionType: auto_subframe - found = received.find((data) => (data.event == "onCommitted" && data.url == FRAME)); - - ok(found, "Got the sub-frame onCommitted event"); - - if (found) { - is(found.details.transitionType, "auto_subframe", - "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers), - "transitionQualifiers found in the OnCommitted events"); - } - - // transitionType: form_submit - received = []; - yield loadAndWait(win, "onCompleted", URL, () => { - win.document.querySelector("form").submit(); - }); - - found = received.find((data) => (data.event == "onCommitted" && data.url == URL)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "form_submit", - "Got the expected 'form_submit' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers), - "transitionQualifiers found in the OnCommitted events"); - } - - // transitionQualifier: server_redirect - received = []; - yield loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = REDIRECT; }); - - found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "link", - "Got the expected 'link' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers) && - found.details.transitionQualifiers.find((q) => q == "server_redirect"), - "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events"); - } - - // transitionQualifier: forward_back - received = []; - yield loadAndWait(win, "onCompleted", URL, () => { win.history.back(); }); - - found = received.find((data) => (data.event == "onCommitted" && data.url == URL)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "link", - "Got the expected 'link' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers) && - found.details.transitionQualifiers.find((q) => q == "forward_back"), - "Got the expected 'forward_back' transitionQualifiers in the OnCommitted events"); - } - - // transitionQualifier: client_redirect - // (from meta http-equiv tag) - received = []; - yield loadAndWait(win, "onCompleted", REDIRECTED, () => { - win.location = CLIENT_REDIRECT; - }); - - found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "link", - "Got the expected 'link' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers) && - found.details.transitionQualifiers.find((q) => q == "client_redirect"), - "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); - } - - // transitionQualifier: client_redirect - // (from http headers) - received = []; - yield loadAndWait(win, "onCompleted", REDIRECTED, () => { - win.location = CLIENT_REDIRECT_HTTPHEADER; - }); - - found = received.find((data) => (data.event == "onCommitted" && - data.url == CLIENT_REDIRECT_HTTPHEADER)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "link", - "Got the expected 'link' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers) && - found.details.transitionQualifiers.find((q) => q == "client_redirect"), - "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); - } - - // transitionQualifier: client_redirect (sub-frame) - // (from meta http-equiv tag) - received = []; - yield loadAndWait(win, "onCompleted", REDIRECTED, () => { - win.location = FRAME_CLIENT_REDIRECT; - }); - - found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "auto_subframe", - "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); - ok(Array.isArray(found.details.transitionQualifiers) && - found.details.transitionQualifiers.find((q) => q == "client_redirect"), - "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events"); - } - - // transitionQualifier: server_redirect (sub-frame) - received = []; - yield loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = FRAME_REDIRECT; }); - - found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECT)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "auto_subframe", - "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); - // BUG 1264936: currently the server_redirect is not detected in sub-frames - // once we fix it we can test it here: - // - // ok(Array.isArray(found.details.transitionQualifiers) && - // found.details.transitionQualifiers.find((q) => q == "server_redirect"), - // "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events"); - } - - // transitionType: manual_subframe - received = []; - yield loadAndWait(win, "onCompleted", FRAME_MANUAL, () => { win.location = FRAME_MANUAL; }); - found = received.find((data) => (data.event == "onCommitted" && - data.url == FRAME_MANUAL_PAGE1)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "auto_subframe", - "Got the expected 'auto_subframe' transitionType in the OnCommitted event"); - } - - received = []; - yield loadAndWait(win, "onCompleted", FRAME_MANUAL_PAGE2, () => { - let el = win.document.querySelector("iframe") - .contentDocument.querySelector("a"); - sendMouseEvent({type: "click"}, el, win); - }); - - found = received.find((data) => (data.event == "onCommitted" && - data.url == FRAME_MANUAL_PAGE2)); - - ok(found, "Got the onCommitted event"); - - if (found) { - is(found.details.transitionType, "manual_subframe", - "Got the expected 'manual_subframe' transitionType in the OnCommitted event"); - } - - // cleanup phase - win.close(); - - yield extension.unload(); - info("webnavigation extension unloaded"); -}); - -add_task(function* webnav_ordering() { - let extensionData = { - manifest: { - permissions: [ - "webNavigation", - ], - }, - background: backgroundScript, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - extension.onMessage("received", ({url, event}) => { - received.push({url, event}); - - if (event == waitingEvent && url == waitingURL) { - completedResolve(); - } - }); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - info("webnavigation extension loaded"); - - let win = window.open(); - - yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; }); - - function checkRequired(url) { - for (let event of REQUIRED) { - let found = false; - for (let r of received) { - if (r.url == url && r.event == event) { - found = true; - } - } - ok(found, `Received event ${event} from ${url}`); - } - } - - checkRequired(URL); - checkRequired(FRAME); - - function checkBefore(action1, action2) { - function find(action) { - for (let i = 0; i < received.length; i++) { - if (received[i].url == action.url && received[i].event == action.event) { - return i; - } - } - return -1; - } - - let index1 = find(action1); - let index2 = find(action2); - ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`); - ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`); - ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`); - } - - // As required in the webNavigation API documentation: - // If a navigating frame contains subframes, its onCommitted is fired before any - // of its children's onBeforeNavigate; while onCompleted is fired after - // all of its children's onCompleted. - checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"}); - checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"}); - - // As required in the webNAvigation API documentation, check the event sequence: - // onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted - let expectedEventSequence = [ - "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted", - ]; - - for (let i = 1; i < expectedEventSequence.length; i++) { - let after = expectedEventSequence[i]; - let before = expectedEventSequence[i - 1]; - checkBefore({url: URL, event: before}, {url: URL, event: after}); - checkBefore({url: FRAME, event: before}, {url: FRAME, event: after}); - } - - yield loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; }); - - checkRequired(FRAME2); - - let navigationSequence = [ - { - action: () => { win.frames[0].document.getElementById("elt").click(); }, - waitURL: `${FRAME2}#ref`, - expectedEvent: "onReferenceFragmentUpdated", - description: "clicked an anchor link", - }, - { - action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); }, - waitURL: `${FRAME2}#ref2`, - expectedEvent: "onReferenceFragmentUpdated", - description: "history.pushState, same pathname, different hash", - }, - { - action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); }, - waitURL: `${FRAME2}#ref2`, - expectedEvent: "onHistoryStateUpdated", - description: "history.pushState, same pathname, same hash", - }, - { - action: () => { - win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`); - }, - waitURL: `${FRAME2}?query_param1=value#ref2`, - expectedEvent: "onHistoryStateUpdated", - description: "history.pushState, same pathname, same hash, different query params", - }, - { - action: () => { - win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`); - }, - waitURL: `${FRAME2}?query_param2=value#ref3`, - expectedEvent: "onHistoryStateUpdated", - description: "history.pushState, same pathname, different hash, different query params", - }, - { - action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); }, - waitURL: FRAME_PUSHSTATE, - expectedEvent: "onHistoryStateUpdated", - description: "history.pushState, different pathname", - }, - ]; - - for (let navigation of navigationSequence) { - let {expectedEvent, waitURL, action, description} = navigation; - info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`); - yield loadAndWait(win, expectedEvent, waitURL, action); - info(`Received ${expectedEvent} from ${waitURL} - ${description}`); - } - - for (let i = navigationSequence.length - 1; i > 0; i--) { - let {waitURL: fromURL, expectedEvent} = navigationSequence[i]; - let {waitURL} = navigationSequence[i - 1]; - info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`); - yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); }); - info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`); - } - - for (let i = 0; i < navigationSequence.length - 1; i++) { - let {waitURL: fromURL} = navigationSequence[i]; - let {waitURL, expectedEvent} = navigationSequence[i + 1]; - info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`); - yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); }); - info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`); - } - - win.close(); - - yield extension.unload(); - info("webnavigation extension unloaded"); -}); - -add_task(function* webnav_error_event() { - function backgroundScriptErrorEvent() { - browser.webNavigation.onErrorOccurred.addListener((details) => { - browser.test.log(`Got onErrorOccurred ${details.url} ${details.error}`); - - browser.test.sendMessage("received", {url: details.url, details, event: "onErrorOccurred"}); - }); - - browser.test.sendMessage("ready"); - } - - let extensionData = { - manifest: { - permissions: [ - "webNavigation", - ], - }, - background: backgroundScriptErrorEvent, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - extension.onMessage("received", ({url, event, details}) => { - received.push({url, event, details}); - - if (event == waitingEvent && url == waitingURL) { - completedResolve(); - } - }); - - yield Promise.all([extension.startup(), extension.awaitMessage("ready")]); - info("webnavigation extension loaded"); - - let win = window.open(); - - received = []; - yield loadAndWait(win, "onErrorOccurred", INVALID_PAGE, () => { win.location = INVALID_PAGE; }); - - let found = received.find((data) => (data.event == "onErrorOccurred" && - data.url == INVALID_PAGE)); - - ok(found, "Got the onErrorOccurred event"); - - if (found) { - ok(found.details.error.match(/Error code [0-9]+/), - "Got the expected error string in the onErrorOccurred event"); - } - - // cleanup phase - win.close(); - - yield extension.unload(); - info("webnavigation extension unloaded"); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_webnavigation_filters.html b/toolkit/components/webextensions/test/mochitest/test_ext_webnavigation_filters.html deleted file mode 100644 index a0de5e9e5..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_webnavigation_filters.html +++ /dev/null @@ -1,308 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for simple WebExtension</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_webnav_unresolved_uri_on_expected_URI_scheme() { - function background() { - let lastTest; - - function cleanupTestListeners() { - if (lastTest) { - let {event, okListener, failListener} = lastTest; - lastTest = null; - browser.test.log(`Cleanup previous test event listeners`); - browser.webNavigation[event].removeListener(okListener); - browser.webNavigation[event].removeListener(failListener); - } - } - - function createTestListener(event, fail, urlFilter) { - function listener(details) { - let log = JSON.stringify({url: details.url, urlFilter}); - if (fail) { - browser.test.fail(`Got an unexpected ${event} on the failure listener: ${log}`); - } else { - browser.test.succeed(`Got the expected ${event} on the success listener: ${log}`); - } - - cleanupTestListeners(); - browser.test.sendMessage("test-filter-next"); - } - - browser.webNavigation[event].addListener(listener, urlFilter); - - return listener; - } - - browser.test.onMessage.addListener((msg, event, okFilter, failFilter) => { - if (msg !== "test-filter") { - return; - } - - lastTest = { - event, - // Register the failListener first, which should not be called - // and if it is called the test scenario is marked as a failure. - failListener: createTestListener(event, true, failFilter), - okListener: createTestListener(event, false, okFilter), - }; - - browser.test.sendMessage("test-filter-ready"); - }); - - browser.test.sendMessage("ready"); - } - - let extensionData = { - manifest: { - permissions: [ - "webNavigation", - ], - }, - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - yield extension.awaitMessage("ready"); - - let win = window.open(); - - let testFilterScenarios = [ - { - url: "http://example.net/browser", - filters: [ - // schemes - { - okFilter: [{schemes: ["http"]}], - failFilter: [{schemes: ["https"]}], - }, - // ports - { - okFilter: [{ports: [80, 22, 443]}], - failFilter: [{ports: [81, 82, 83]}], - }, - { - okFilter: [{ports: [22, 443, [10, 80]]}], - failFilter: [{ports: [22, 23, [81, 100]]}], - }, - ], - }, - { - url: "http://example.net/browser?param=1#ref", - filters: [ - // host: Equals, Contains, Prefix, Suffix - { - okFilter: [{hostEquals: "example.net"}], - failFilter: [{hostEquals: "example.com"}], - }, - { - okFilter: [{hostContains: ".example"}], - failFilter: [{hostContains: ".www"}], - }, - { - okFilter: [{hostPrefix: "example"}], - failFilter: [{hostPrefix: "www"}], - }, - { - okFilter: [{hostSuffix: "net"}], - failFilter: [{hostSuffix: "com"}], - }, - // path: Equals, Contains, Prefix, Suffix - { - okFilter: [{pathEquals: "/browser"}], - failFilter: [{pathEquals: "/"}], - }, - { - okFilter: [{pathContains: "brow"}], - failFilter: [{pathContains: "tool"}], - }, - { - okFilter: [{pathPrefix: "/bro"}], - failFilter: [{pathPrefix: "/tool"}], - }, - { - okFilter: [{pathSuffix: "wser"}], - failFilter: [{pathSuffix: "kit"}], - }, - // query: Equals, Contains, Prefix, Suffix - { - okFilter: [{queryEquals: "param=1"}], - failFilter: [{queryEquals: "wrongparam=2"}], - }, - { - okFilter: [{queryContains: "param"}], - failFilter: [{queryContains: "wrongparam"}], - }, - { - okFilter: [{queryPrefix: "param="}], - failFilter: [{queryPrefix: "wrong"}], - }, - { - okFilter: [{querySuffix: "=1"}], - failFilter: [{querySuffix: "=2"}], - }, - // urlMatches, originAndPathMatches - { - okFilter: [{urlMatches: "example.net/.*\?param=1"}], - failFilter: [{urlMatches: "example.net/.*\?wrongparam=2"}], - }, - { - okFilter: [{originAndPathMatches: "example.net\/browser"}], - failFilter: [{originAndPathMatches: "example.net/.*\?param=1"}], - }, - ], - }, - { - url: "http://example.net/browser", - filters: [ - // multiple criteria in a single filter: - // if one of the critera is not verified, the event should not be received. - { - okFilter: [{schemes: ["http"], ports: [80, 22, 443]}], - failFilter: [{schemes: ["http"], ports: [81, 82, 83]}], - }, - // multiple urlFilters on the same listener - // if at least one of the critera is verified, the event should be received. - { - okFilter: [{schemes: ["https"]}, {ports: [80, 22, 443]}], - failFilter: [{schemes: ["https"]}, {ports: [81, 82, 83]}], - }, - ], - }, - ]; - - function* runTestScenario(event, {url, filters}) { - for (let testFilters of filters) { - let {okFilter, failFilter} = testFilters; - - info(`Prepare the new test scenario: ${event} ${url} ${JSON.stringify(testFilters)}`); - win.location = "about:blank"; - - extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); - yield extension.awaitMessage("test-filter-ready"); - - info(`Loading the test url: ${url}`); - win.location = url; - - yield extension.awaitMessage("test-filter-next"); - - info("Test scenario completed. Moving to the next test scenario."); - } - } - - const BASE_WEBNAV_EVENTS = [ - "onBeforeNavigate", - "onCommitted", - "onDOMContentLoaded", - "onCompleted", - ]; - - info("WebNavigation event filters test scenarios starting..."); - - for (let filterScenario of testFilterScenarios) { - for (let event of BASE_WEBNAV_EVENTS) { - yield runTestScenario(event, filterScenario); - } - } - - info("WebNavigation event filters test onReferenceFragmentUpdated scenario starting..."); - - const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest"; - let url = BASE + "/file_WebNavigation_page3.html"; - - let okFilter = [{urlContains: "_page3.html"}]; - let failFilter = [{ports: [444]}]; - let event = "onCompleted"; - - info(`Loading the initial test url: ${url}`); - extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); - - yield extension.awaitMessage("test-filter-ready"); - win.location = url; - yield extension.awaitMessage("test-filter-next"); - - event = "onReferenceFragmentUpdated"; - extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); - - yield extension.awaitMessage("test-filter-ready"); - win.location = url + "#ref1"; - yield extension.awaitMessage("test-filter-next"); - - info("WebNavigation event filters test onHistoryStateUpdated scenario starting..."); - - event = "onHistoryStateUpdated"; - extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter}); - yield extension.awaitMessage("test-filter-ready"); - - win.history.pushState({}, "", BASE + "/pushState_page3.html"); - yield extension.awaitMessage("test-filter-next"); - - // TODO: add additional specific tests for the other webNavigation events: - // onErrorOccurred (and onCreatedNavigationTarget on supported) - - info("WebNavigation event filters test scenarios completed."); - - yield extension.unload(); - - win.close(); -}); - -add_task(function* test_webnav_empty_filter_validation_error() { - function background() { - let catchedException; - - try { - browser.webNavigation.onCompleted.addListener( - // Empty callback (not really used) - () => {}, - // Empty filter (which should raise a validation error exception). - {url: []} - ); - } catch (e) { - catchedException = e; - browser.test.log(`Got an exception`); - } - - if (catchedException && - catchedException.message.includes("Type error for parameter filters") && - catchedException.message.includes("Array requires at least 1 items; you have 0")) { - browser.test.notifyPass("webNav.emptyFilterValidationError"); - } else { - browser.test.notifyFail("webNav.emptyFilterValidationError"); - } - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webNavigation", - ], - }, - background, - }); - - yield extension.startup(); - - yield extension.awaitFinish("webNav.emptyFilterValidationError"); - - yield extension.unload(); -}); - -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_background_events.html b/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_background_events.html deleted file mode 100644 index 78efeab35..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_background_events.html +++ /dev/null @@ -1,116 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for simple WebExtension</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_webRequest_serviceworker_events() { - yield SpecialPowers.pushPrefEnv({ - set: [["dom.serviceWorkers.testing.enabled", true], - ["dom.serviceWorkers.enabled", true], - ["dom.serviceWorkers.openWindow.enabled", true], - ], - }); - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webRequest", - "<all_urls>", - ], - }, - background() { - let eventNames = new Set([ - "onBeforeRequest", - "onBeforeSendHeaders", - "onSendHeaders", - "onHeadersReceived", - "onResponseStarted", - "onCompleted", - ]); - - function listener(name, details) { - browser.test.assertTrue(eventNames.has(name), `recieved ${name}`); - eventNames.delete(name); - if (eventNames.size == 0) { - browser.test.sendMessage("done"); - } - } - - for (let name of eventNames) { - browser.webRequest[name].addListener( - listener.bind(null, name), - {urls: ["https://example.com/*"]} - ); - } - }, - }); - - yield extension.startup(); - let registration = yield navigator.serviceWorker.register("webrequest_worker.js", {scope: "."}); - yield extension.awaitMessage("done"); - yield registration.unregister(); - yield extension.unload(); -}); - -add_task(function* test_webRequest_background_events() { - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webRequest", - "<all_urls>", - ], - }, - background() { - let eventNames = new Set([ - "onBeforeRequest", - "onBeforeSendHeaders", - "onSendHeaders", - "onHeadersReceived", - "onResponseStarted", - "onCompleted", - ]); - - function listener(name, details) { - browser.test.assertTrue(eventNames.has(name), `recieved ${name}`); - eventNames.delete(name); - - if (eventNames.size === 0) { - browser.test.assertEq(0, eventNames.size, "messages recieved"); - browser.test.sendMessage("done"); - } - } - - for (let name of eventNames) { - browser.webRequest[name].addListener( - listener.bind(null, name), - {urls: ["https://example.com/*"]} - ); - } - - fetch("https://example.com/example.txt").then(() => { - browser.test.pass("Fetch succeeded."); - }, () => { - browser.test.fail("fetch recieved"); - browser.test.sendMessage("done"); - }); - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("done"); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_basic.html b/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_basic.html deleted file mode 100644 index ef77fee3b..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_basic.html +++ /dev/null @@ -1,327 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <script type="text/javascript" src="head_webrequest.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -<script> -"use strict"; - -let extension; -add_task(function* setup() { - // SelfSupport has a tendency to fire when running this test alone, without - // a good way to turn it off we just set the url to "" - yield SpecialPowers.pushPrefEnv({ - set: [["browser.selfsupport.url", ""]], - }); - - // Clear the image cache, since it gets in the way otherwise. - let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools); - let cache = imgTools.getImgCacheForDocument(document); - cache.clearCache(false); - - extension = makeExtension(); - yield extension.startup(); -}); - -// expect is a set of test values used by the background script. -// -// type: type of request action -// events: optional, If defined only the events listed are expected for the -// request. If undefined, all events except onErrorOccurred -// and onBeforeRedirect are expected. Must be in order received. -// redirect: url to redirect to during onBeforeSendHeaders -// status: number expected status during onHeadersReceived, 200 default -// cancel: event in which we return cancel=true. cancelled message is sent. -// cached: expected fromCache value, default is false, checked in onCompletion -// headers: request or response headers to modify - -add_task(function* test_webRequest_links() { - let expect = { - "file_style_bad.css": { - type: "stylesheet", - events: ["onBeforeRequest", "onErrorOccurred"], - cancel: "onBeforeRequest", - }, - "file_style_redirect.css": { - type: "stylesheet", - events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], - optional_events: ["onHeadersReceived"], - redirect: "file_style_good.css", - }, - "file_style_good.css": { - type: "stylesheet", - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addStylesheet("file_style_bad.css"); - yield extension.awaitMessage("cancelled"); - // we redirect to style_good which completes the test - addStylesheet("file_style_redirect.css"); - yield extension.awaitMessage("done"); - - let style = window.getComputedStyle(document.getElementById("test"), null); - is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded"); -}); - -add_task(function* test_webRequest_images() { - let expect = { - "file_image_bad.png": { - type: "image", - events: ["onBeforeRequest", "onErrorOccurred"], - cancel: "onBeforeRequest", - }, - "file_image_redirect.png": { - type: "image", - events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], - optional_events: ["onHeadersReceived"], - redirect: "file_image_good.png", - }, - "file_image_good.png": { - type: "image", - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addImage("file_image_bad.png"); - yield extension.awaitMessage("cancelled"); - // we redirect to image_good which completes the test - addImage("file_image_redirect.png"); - yield extension.awaitMessage("done"); -}); - -add_task(function* test_webRequest_scripts() { - let expect = { - "file_script_bad.js": { - type: "script", - events: ["onBeforeRequest", "onErrorOccurred"], - cancel: "onBeforeRequest", - }, - "file_script_redirect.js": { - type: "script", - events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"], - optional_events: ["onHeadersReceived"], - redirect: "file_script_good.js", - }, - "file_script_good.js": { - type: "script", - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addScript("file_script_bad.js"); - yield extension.awaitMessage("cancelled"); - // we redirect to script_good which completes the test - addScript("file_script_redirect.js"); - yield extension.awaitMessage("done"); - - is(window.success, 1, "Good script ran"); - is(window.failure, undefined, "Failure script didn't run"); -}); - -add_task(function* test_webRequest_xhr_get() { - let expect = { - "file_script_xhr.js": { - type: "script", - }, - "xhr_resource": { - status: 404, - type: "xmlhttprequest", - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addScript("file_script_xhr.js"); - yield extension.awaitMessage("done"); -}); - -add_task(function* test_webRequest_nonexistent() { - let expect = { - "nonexistent_script_url.js": { - status: 404, - type: "script", - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addScript("nonexistent_script_url.js"); - yield extension.awaitMessage("done"); -}); - -add_task(function* test_webRequest_checkCached() { - let expect = { - "file_image_good.png": { - type: "image", - cached: true, - }, - "file_script_good.js": { - type: "script", - cached: true, - }, - "file_style_good.css": { - type: "stylesheet", - cached: true, - }, - "nonexistent_script_url.js": { - status: 404, - type: "script", - cached: false, - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addImage("file_image_good.png"); - addScript("file_script_good.js"); - addStylesheet("file_style_good.css"); - addScript("nonexistent_script_url.js"); - yield extension.awaitMessage("done"); - - is(window.success, 2, "Good script ran"); - is(window.failure, undefined, "Failure script didn't run"); -}); - -add_task(function* test_webRequest_headers() { - let expect = { - "file_script_nonexistent.js": { - type: "script", - status: 404, - headers: { - request: { - add: { - "X-WebRequest-request": "text", - "X-WebRequest-request-binary": "binary", - }, - modify: { - "user-agent": "WebRequest", - }, - remove: [ - "referer", - ], - }, - response: { - add: { - "X-WebRequest-response": "text", - "X-WebRequest-response-binary": "binary", - }, - modify: { - "server": "WebRequest", - "content-type": "text/html; charset=utf-8", - }, - remove: [ - "connection", - ], - }, - }, - completion: "onCompleted", - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addScript("file_script_nonexistent.js"); - yield extension.awaitMessage("done"); -}); - -add_task(function* test_webRequest_tabId() { - let expect = { - "file_WebRequest_page3.html": { - type: "main_frame", - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - let a = addLink("file_WebRequest_page3.html?trigger=a"); - a.click(); - yield extension.awaitMessage("done"); -}); - -add_task(function* test_webRequest_tabId_browser() { - async function background(url) { - let tabId; - browser.test.onMessage.addListener(async (msg, expected) => { - await browser.tabs.remove(tabId); - browser.test.sendMessage("done"); - }); - - let tab = await browser.tabs.create({url}); - tabId = tab.id; - } - - let tabExt = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "tabs", - ], - }, - background: `(${background})('${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}')`, - }); - - let expect = { - "file_sample.html": { - type: "main_frame", - }, - }; - // expecting origin == undefined - extension.sendMessage("set-expected", {expect}); - yield extension.awaitMessage("continue"); - - // open a tab from a system principal - yield tabExt.startup(); - - yield extension.awaitMessage("done"); - tabExt.sendMessage("done"); - yield tabExt.awaitMessage("done"); - yield tabExt.unload(); -}); - -add_task(function* test_webRequest_frames() { - let expect = { - "text/plain,webRequestTest": { - type: "sub_frame", - events: ["onBeforeRequest", "onCompleted"], - }, - "text/plain,webRequestTest_bad": { - type: "sub_frame", - events: ["onBeforeRequest", "onCompleted"], - cancel: "onBeforeRequest", - }, - "redirection.sjs": { - status: 302, - type: "sub_frame", - events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"], - }, - "dummy_page.html": { - type: "sub_frame", - status: 404, - }, - "badrobot": { - type: "sub_frame", - status: 404, - events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"], - }, - }; - extension.sendMessage("set-expected", {expect, origin: location.href}); - yield extension.awaitMessage("continue"); - addFrame("data:text/plain,webRequestTest"); - addFrame("data:text/plain,webRequestTest_bad"); - yield extension.awaitMessage("cancelled"); - addFrame("redirection.sjs"); - addFrame("https://invalid.localhost/badrobot"); - yield extension.awaitMessage("done"); -}); - -add_task(function* teardown() { - yield extension.unload(); -}); -</script> -</head> -<body> -<div id="test">Sample text</div> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_suspend.html b/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_suspend.html deleted file mode 100644 index c8423ec7c..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_suspend.html +++ /dev/null @@ -1,216 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for simple WebExtension</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_suspend() { - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webRequest", - "webRequestBlocking", - ], - }, - - background() { - browser.webRequest.onBeforeSendHeaders.addListener( - details => { - // Make sure that returning undefined or a promise that resolves to - // undefined does not break later handlers. - }, - {urls: ["<all_urls>"]}, - ["blocking", "requestHeaders"]); - - browser.webRequest.onBeforeSendHeaders.addListener( - details => { - return Promise.resolve(); - }, - {urls: ["<all_urls>"]}, - ["blocking", "requestHeaders"]); - - browser.webRequest.onBeforeSendHeaders.addListener( - details => { - let requestHeaders = details.requestHeaders.concat({name: "Foo", value: "Bar"}); - - return new Promise(resolve => { - setTimeout(resolve, 500); - }).then(() => { - return {requestHeaders}; - }); - }, - {urls: ["<all_urls>"]}, - ["blocking", "requestHeaders"]); - }, - }); - - yield extension.startup(); - - let result = yield fetch(SimpleTest.getTestFileURL("return_headers.sjs")); - - let headers = JSON.parse(yield result.text()); - - is(headers.foo, "Bar", "Request header was correctly set on suspended request"); - - yield extension.unload(); -}); - - -// Test that requests that were canceled while suspended for a blocking -// listener are correctly resumed. -add_task(function* test_error_resume() { - let chromeScript = SpecialPowers.loadChromeScript(() => { - const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - Cu.import("resource://gre/modules/Services.jsm"); - - let observer = channel => { - if (channel instanceof Ci.nsIHttpChannel && channel.URI.spec === "http://example.com/") { - Services.obs.removeObserver(observer, "http-on-modify-request"); - - // Wait until the next tick to make sure this runs after WebRequest observers. - Promise.resolve().then(() => { - channel.cancel(Cr.NS_BINDING_ABORTED); - }); - } - }; - - Services.obs.addObserver(observer, "http-on-modify-request", false); - }); - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webRequest", - "webRequestBlocking", - ], - }, - - background() { - browser.webRequest.onBeforeSendHeaders.addListener( - details => { - browser.test.log(`onBeforeSendHeaders({url: ${details.url}})`); - - if (details.url === "http://example.com/") { - browser.test.sendMessage("got-before-send-headers"); - } - }, - {urls: ["<all_urls>"]}, - ["blocking"]); - - browser.webRequest.onErrorOccurred.addListener( - details => { - browser.test.log(`onErrorOccurred({url: ${details.url}})`); - - if (details.url === "http://example.com/") { - browser.test.sendMessage("got-error-occurred"); - } - }, - {urls: ["<all_urls>"]}); - }, - }); - - yield extension.startup(); - - try { - yield fetch("http://example.com/"); - ok(false, "Fetch should have failed."); - } catch (e) { - ok(true, "Got expected error."); - } - - yield extension.awaitMessage("got-before-send-headers"); - yield extension.awaitMessage("got-error-occurred"); - - yield extension.unload(); - chromeScript.destroy(); -}); - - -// Test that response header modifications take effect before onStartRequest fires. -add_task(function* test_set_responseHeaders() { - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webRequest", - "webRequestBlocking", - "http://example.com/", - ], - }, - - background() { - browser.webRequest.onHeadersReceived.addListener( - details => { - browser.test.log(`onHeadersReceived({url: ${details.url}})`); - - details.responseHeaders.push({name: "foo", value: "bar"}); - - return {responseHeaders: details.responseHeaders}; - }, - {urls: ["http://example.com/?modify_headers"]}, - ["blocking", "responseHeaders"]); - }, - }); - - yield extension.startup(); - - yield new Promise(resolve => setTimeout(resolve, 0)); - - let chromeScript = SpecialPowers.loadChromeScript(() => { - const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - - Cu.import("resource://gre/modules/NetUtil.jsm"); - Cu.import("resource://gre/modules/Services.jsm"); - Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - - let ssm = Services.scriptSecurityManager; - - let channel = NetUtil.newChannel({ - uri: "http://example.com/?modify_headers", - loadingPrincipal: ssm.createCodebasePrincipalFromOrigin("http://example.com"), - contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST, - securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, - }); - - channel.asyncOpen2({ - QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]), - - onStartRequest(request, context) { - request.QueryInterface(Ci.nsIHttpChannel); - - try { - sendAsyncMessage("response-header-foo", request.getResponseHeader("foo")); - } catch (e) { - sendAsyncMessage("response-header-foo", null); - } - request.cancel(Cr.NS_BINDING_ABORTED); - }, - - onStopRequest() { - }, - - onDataAvailable() { - throw new Components.Exception("", Cr.NS_ERROR_FAILURE); - }, - }); - }); - - let headerValue = yield chromeScript.promiseOneMessage("response-header-foo"); - is(headerValue, "bar", "Expected Foo header value"); - - yield extension.unload(); - chromeScript.destroy(); -}); - - -</script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_upload.html b/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_upload.html deleted file mode 100644 index 998ab9800..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_webrequest_upload.html +++ /dev/null @@ -1,199 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<form method="post" - action="file_WebRequest_page3.html?trigger=form" - target="_blank" - enctype="multipart/form-data" - > -<input type="text" name=""special" chˆrs" value="spÛcial"> -<input type="file" name="testFile"> -<input type="file" name="emptyFile"> -<input type="text" name="textInput1" value="value1"> -</form> - -<form method="post" - action="file_WebRequest_page3.html?trigger=form" - target="_blank" - enctype="multipart/form-data" - > -<input type="text" name="textInput2" value="value2"> -<input type="file" name="testFile"> -<input type="file" name="emptyFile"> -</form> - -</form> -<form method="post" - action="file_WebRequest_page3.html?trigger=form" - target="_blank" - > -<input type="text" name="textInput" value="value1"> -<input type="text" name="textInput" value="value2"> -</form> -<script> -"use strict"; - -let files, testFile, blob, file, uploads; -add_task(function* test_setup() { - files = yield new Promise(resolve => { - SpecialPowers.createFiles([{name: "testFile.pdf", data: "Not really a PDF file :)", "type": "application/x-pdf"}], (result) => { - resolve(result); - }); - }); - testFile = files[0]; - blob = { - name: "blobAsFile", - content: new Blob(["A blob sent as a file"], {type: "text/csv"}), - fileName: "blobAsFile.csv", - }; - file = { - name: "testFile", - fileName: testFile.name, - }; - uploads = { - [blob.name]: blob, - [file.name]: file, - }; -}); - -function background() { - const FILTERS = {urls: ["<all_urls>"]}; - - let requestBodySupported = true; - - function onUpload(details) { - let url = new URL(details.url); - let upload = url.searchParams.get("upload"); - if (!upload || !requestBodySupported) { - return; - } - let requestBody = details.requestBody; - browser.test.log(`onBeforeRequest upload: ${details.url} ${JSON.stringify(details.requestBody)}`); - browser.test.assertTrue(!!requestBody, `Intercepted upload ${details.url} #${details.requestId} ${upload} have a requestBody`); - if (!requestBody) { - return; - } - let byteLength = parseInt(upload, 10); - if (byteLength) { - browser.test.assertTrue(!!requestBody.raw, `Binary upload ${details.url} #${details.requestId} ${upload} have a raw attribute`); - browser.test.assertEq(byteLength, requestBody.raw && requestBody.raw.map(r => r.bytes && r.bytes.byteLength || 0).reduce((a, b) => a + b), `Binary upload size matches`); - return; - } - if ("raw" in requestBody) { - browser.test.assertEq(upload, JSON.stringify(requestBody.raw).replace(/(\bfile: ")[^"]+/, "$1<file>"), `Upload ${details.url} #${details.requestId} matches raw data`); - } else { - browser.test.assertEq(upload, JSON.stringify(requestBody.formData), `Upload ${details.url} #${details.requestId} matches form data.`); - } - } - - browser.webRequest.onCompleted.addListener( - details => { - browser.test.log(`onCompleted ${details.requestId} ${details.url}`); - - browser.test.sendMessage("done"); - }, - FILTERS); - - let onBeforeRequest = details => { - browser.test.log(`${name} ${details.requestId} ${details.url}`); - - onUpload(details); - }; - - try { - browser.webRequest.onBeforeRequest.addListener( - onBeforeRequest, FILTERS, ["requestBody"]); - } catch (e) { - browser.test.assertTrue(/\brequestBody\b/.test(e.message), - "Request body is unsupported"); - - // requestBody is disabled in release builds - if (!/\brequestBody\b/.test(e.message)) { - throw e; - } - - browser.webRequest.onBeforeRequest.addListener( - onBeforeRequest, FILTERS); - } -} - -add_task(function* test_xhr_forms() { - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [ - "webRequest", - "webRequestBlocking", - "<all_urls>", - ], - }, - background, - }); - - yield extension.startup(); - - for (let form of document.forms) { - if (file.name in form.elements) { - SpecialPowers.wrap(form.elements[file.name]).mozSetFileArray(files); - } - let action = new URL(form.action); - let formData = new FormData(form); - let webRequestFD = {}; - - let updateActionURL = () => { - for (let name of formData.keys()) { - webRequestFD[name] = name in uploads ? [uploads[name].fileName] : formData.getAll(name); - } - action.searchParams.set("upload", JSON.stringify(webRequestFD)); - action.searchParams.set("enctype", form.enctype); - }; - - updateActionURL(); - - form.action = action; - form.submit(); - yield extension.awaitMessage("done"); - - if (form.enctype !== "multipart/form-data") { - continue; - } - - let post = (data) => { - let xhr = new XMLHttpRequest(); - action.searchParams.set("xhr", "1"); - xhr.open("POST", action.href); - xhr.send(data); - action.searchParams.delete("xhr"); - return extension.awaitMessage("done"); - }; - - formData.append(blob.name, blob.content, blob.fileName); - formData.append("formDataField", "some value"); - updateActionURL(); - yield post(formData); - - action.searchParams.set("upload", JSON.stringify([{file: "<file>"}])); - yield post(testFile); - - action.searchParams.set("upload", `${blob.content.size} bytes`); - yield post(blob.content); - - let byteLength = 16; - action.searchParams.set("upload", `${byteLength} bytes`); - yield post(new ArrayBuffer(byteLength)); - } - - yield extension.unload(); -}); -</script> -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_window_postMessage.html b/toolkit/components/webextensions/test/mochitest/test_ext_window_postMessage.html deleted file mode 100644 index 7d49d55ba..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_window_postMessage.html +++ /dev/null @@ -1,105 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test for content script</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -/* eslint-disable mozilla/balanced-listeners */ - -add_task(function* test_postMessage() { - let extensionData = { - manifest: { - content_scripts: [ - { - "matches": ["http://mochi.test/*/file_sample.html"], - "js": ["content_script.js"], - "run_at": "document_start", - "all_frames": true, - }, - ], - - web_accessible_resources: ["iframe.html"], - }, - - background() { - browser.test.sendMessage("iframe-url", browser.runtime.getURL("iframe.html")); - }, - - files: { - "content_script.js": function() { - window.addEventListener("message", event => { - if (event.data == "ping") { - event.source.postMessage({pong: location.href}, - event.origin); - } - }); - }, - - "iframe.html": `<!DOCTYPE html> - <html> - <head> - <meta charset="utf-8"> - <script src="content_script.js"><\/script> - </head> - </html>`, - }, - }; - - let createIframe = url => { - let iframe = document.createElement("iframe"); - return new Promise(resolve => { - iframe.src = url; - iframe.onload = resolve; - document.body.appendChild(iframe); - }).then(() => { - return iframe; - }); - }; - - let awaitMessage = () => { - return new Promise(resolve => { - let listener = event => { - if (event.data.pong) { - window.removeEventListener("message", listener); - resolve(event.data); - } - }; - window.addEventListener("message", listener); - }); - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - let iframeURL = yield extension.awaitMessage("iframe-url"); - let testURL = SimpleTest.getTestFileURL("file_sample.html"); - - for (let url of [iframeURL, testURL]) { - info(`Testing URL ${url}`); - - let iframe = yield createIframe(url); - - iframe.contentWindow.postMessage( - "ping", url); - - let pong = yield awaitMessage(); - is(pong.pong, url, "Got expected pong"); - - iframe.remove(); - } - - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/test_ext_xhr_capabilities.html b/toolkit/components/webextensions/test/mochitest/test_ext_xhr_capabilities.html deleted file mode 100644 index 1afdadb9f..000000000 --- a/toolkit/components/webextensions/test/mochitest/test_ext_xhr_capabilities.html +++ /dev/null @@ -1,86 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <title>Test XHR capabilities</title> - <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> - <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> - <script type="text/javascript" src="head.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> -</head> -<body> - -<script type="text/javascript"> -"use strict"; - -add_task(function* test_xhr_capabilities() { - function backgroundScript() { - let xhr = new XMLHttpRequest(); - xhr.open("GET", browser.extension.getURL("bad.xml")); - - browser.test.sendMessage("result", - {name: "Background script XHRs should not be privileged", - result: xhr.channel === undefined}); - - xhr.onload = () => { - browser.test.sendMessage("result", - {name: "Background script XHRs should not yield <parsererrors>", - result: xhr.responseXML === null}); - }; - xhr.send(); - } - - function contentScript() { - let xhr = new XMLHttpRequest(); - xhr.open("GET", browser.extension.getURL("bad.xml")); - - browser.test.sendMessage("result", - {name: "Content script XHRs should not be privileged", - result: xhr.channel === undefined}); - - xhr.onload = () => { - browser.test.sendMessage("result", - {name: "Content script XHRs should not yield <parsererrors>", - result: xhr.responseXML === null}); - }; - xhr.send(); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - background: { - "scripts": ["background.js"], - }, - content_scripts: [{ - "matches": ["http://example.com/"], - "js": ["content_script.js"], - }], - web_accessible_resources: [ - "bad.xml", - ], - }, - - files: { - "bad.xml": "<xml", - "background.js": `(${backgroundScript})()`, - "content_script.js": `(${contentScript})()`, - }, - }); - - yield extension.startup(); - - let win = window.open("http://example.com/"); - - // We expect four test results from the content/background scripts. - for (let i = 0; i < 4; ++i) { - let result = yield extension.awaitMessage("result"); - ok(result.result, result.name); - } - - win.close(); - yield extension.unload(); -}); -</script> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/mochitest/webrequest_chromeworker.js b/toolkit/components/webextensions/test/mochitest/webrequest_chromeworker.js deleted file mode 100644 index ccfb2ac1f..000000000 --- a/toolkit/components/webextensions/test/mochitest/webrequest_chromeworker.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict"; - -onmessage = function(event) { - fetch("https://example.com/example.txt").then(() => { - postMessage("Done!"); - }); -}; - diff --git a/toolkit/components/webextensions/test/mochitest/webrequest_test.jsm b/toolkit/components/webextensions/test/mochitest/webrequest_test.jsm deleted file mode 100644 index bfb148301..000000000 --- a/toolkit/components/webextensions/test/mochitest/webrequest_test.jsm +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; - -this.EXPORTED_SYMBOLS = ["webrequest_test"]; - -Components.utils.importGlobalProperties(["fetch", "XMLHttpRequest"]); - -this.webrequest_test = { - testFetch(url) { - return fetch(url); - }, - - testXHR(url) { - return new Promise(resolve => { - let xhr = new XMLHttpRequest(); - xhr.open("HEAD", url); - xhr.onload = () => { - resolve(); - }; - xhr.send(); - }); - }, -}; diff --git a/toolkit/components/webextensions/test/mochitest/webrequest_worker.js b/toolkit/components/webextensions/test/mochitest/webrequest_worker.js deleted file mode 100644 index dcffd0857..000000000 --- a/toolkit/components/webextensions/test/mochitest/webrequest_worker.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -fetch("https://example.com/example.txt"); diff --git a/toolkit/components/webextensions/test/xpcshell/.eslintrc.js b/toolkit/components/webextensions/test/xpcshell/.eslintrc.js deleted file mode 100644 index 3758537ef..000000000 --- a/toolkit/components/webextensions/test/xpcshell/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -module.exports = { // eslint-disable-line no-undef - "extends": "../../../../../testing/xpcshell/xpcshell.eslintrc.js", - - "globals": { - "browser": false, - }, -}; diff --git a/toolkit/components/webextensions/test/xpcshell/data/file_download.html b/toolkit/components/webextensions/test/xpcshell/data/file_download.html deleted file mode 100644 index d970c6325..000000000 --- a/toolkit/components/webextensions/test/xpcshell/data/file_download.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE HTML> - -<html> -<head> -<meta charset="utf-8"> -</head> -<body> - -<div>Download HTML File</div> - -</body> -</html> diff --git a/toolkit/components/webextensions/test/xpcshell/data/file_download.txt b/toolkit/components/webextensions/test/xpcshell/data/file_download.txt deleted file mode 100644 index 6293c7af7..000000000 --- a/toolkit/components/webextensions/test/xpcshell/data/file_download.txt +++ /dev/null @@ -1 +0,0 @@ -This is a sample file used in download tests. diff --git a/toolkit/components/webextensions/test/xpcshell/head.js b/toolkit/components/webextensions/test/xpcshell/head.js deleted file mode 100644 index 9e22be6da..000000000 --- a/toolkit/components/webextensions/test/xpcshell/head.js +++ /dev/null @@ -1,111 +0,0 @@ -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -/* exported createHttpServer, promiseConsoleOutput, cleanupDir */ - -Components.utils.import("resource://gre/modules/Task.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://gre/modules/Timer.jsm"); -Components.utils.import("resource://testing-common/AddonTestUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Extension", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils", - "resource://testing-common/ExtensionXPCShellUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "HttpServer", - "resource://testing-common/httpd.js"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -ExtensionTestUtils.init(this); - -/** - * Creates a new HttpServer for testing, and begins listening on the - * specified port. Automatically shuts down the server when the test - * unit ends. - * - * @param {integer} [port] - * The port to listen on. If omitted, listen on a random - * port. The latter is the preferred behavior. - * - * @returns {HttpServer} - */ -function createHttpServer(port = -1) { - let server = new HttpServer(); - server.start(port); - - do_register_cleanup(() => { - return new Promise(resolve => { - server.stop(resolve); - }); - }); - - return server; -} - -var promiseConsoleOutput = Task.async(function* (task) { - const DONE = `=== console listener ${Math.random()} done ===`; - - let listener; - let messages = []; - let awaitListener = new Promise(resolve => { - listener = msg => { - if (msg == DONE) { - resolve(); - } else { - void (msg instanceof Ci.nsIConsoleMessage); - messages.push(msg); - } - }; - }); - - Services.console.registerListener(listener); - try { - let result = yield task(); - - Services.console.logStringMessage(DONE); - yield awaitListener; - - return {messages, result}; - } finally { - Services.console.unregisterListener(listener); - } -}); - -// Attempt to remove a directory. If the Windows OS is still using the -// file sometimes remove() will fail. So try repeatedly until we can -// remove it or we give up. -function cleanupDir(dir) { - let count = 0; - return new Promise((resolve, reject) => { - function tryToRemoveDir() { - count += 1; - try { - dir.remove(true); - } catch (e) { - // ignore - } - if (!dir.exists()) { - return resolve(); - } - if (count >= 25) { - return reject(`Failed to cleanup directory: ${dir}`); - } - setTimeout(tryToRemoveDir, 100); - } - tryToRemoveDir(); - }); -} diff --git a/toolkit/components/webextensions/test/xpcshell/head_native_messaging.js b/toolkit/components/webextensions/test/xpcshell/head_native_messaging.js deleted file mode 100644 index f7c619b76..000000000 --- a/toolkit/components/webextensions/test/xpcshell/head_native_messaging.js +++ /dev/null @@ -1,131 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -/* globals AppConstants, FileUtils */ -/* exported getSubprocessCount, setupHosts, waitForSubprocessExit */ - -XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry", - "resource://testing-common/MockRegistry.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); - -let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm"); - - -// It's important that we use a space in this directory name to make sure we -// correctly handle executing batch files with spaces in their path. -let tmpDir = FileUtils.getDir("TmpD", ["Native Messaging"]); -tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - -do_register_cleanup(() => { - tmpDir.remove(true); -}); - -function getPath(filename) { - return OS.Path.join(tmpDir.path, filename); -} - -const ID = "native@tests.mozilla.org"; - - -function* setupHosts(scripts) { - const PERMS = {unixMode: 0o755}; - - const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); - const pythonPath = yield Subprocess.pathSearch(env.get("PYTHON")); - - function* writeManifest(script, scriptPath, path) { - let body = `#!${pythonPath} -u\n${script.script}`; - - yield OS.File.writeAtomic(scriptPath, body); - yield OS.File.setPermissions(scriptPath, PERMS); - - let manifest = { - name: script.name, - description: script.description, - path, - type: "stdio", - allowed_extensions: [ID], - }; - - let manifestPath = getPath(`${script.name}.json`); - yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest)); - - return manifestPath; - } - - switch (AppConstants.platform) { - case "macosx": - case "linux": - let dirProvider = { - getFile(property) { - if (property == "XREUserNativeMessaging") { - return tmpDir.clone(); - } else if (property == "XRESysNativeMessaging") { - return tmpDir.clone(); - } - return null; - }, - }; - - Services.dirsvc.registerProvider(dirProvider); - do_register_cleanup(() => { - Services.dirsvc.unregisterProvider(dirProvider); - }); - - for (let script of scripts) { - let path = getPath(`${script.name}.py`); - - yield writeManifest(script, path, path); - } - break; - - case "win": - const REGKEY = String.raw`Software\Mozilla\NativeMessagingHosts`; - - let registry = new MockRegistry(); - do_register_cleanup(() => { - registry.shutdown(); - }); - - for (let script of scripts) { - // It's important that we use a space in this filename. See directory - // name comment above. - let batPath = getPath(`batch ${script.name}.bat`); - let scriptPath = getPath(`${script.name}.py`); - - let batBody = `@ECHO OFF\n${pythonPath} -u "${scriptPath}" %*\n`; - yield OS.File.writeAtomic(batPath, batBody); - - // Create absolute and relative path versions of the entry. - for (let [name, path] of [[script.name, batPath], - [`relative.${script.name}`, OS.Path.basename(batPath)]]) { - script.name = name; - let manifestPath = yield writeManifest(script, scriptPath, path); - - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - `${REGKEY}\\${script.name}`, "", manifestPath); - } - } - break; - - default: - ok(false, `Native messaging is not supported on ${AppConstants.platform}`); - } -} - - -function getSubprocessCount() { - return SubprocessImpl.Process.getWorker().call("getProcesses", []) - .then(result => result.size); -} -function waitForSubprocessExit() { - return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []).then(() => { - // Return to the main event loop to give IO handlers enough time to consume - // their remaining buffered input. - return new Promise(resolve => setTimeout(resolve, 0)); - }); -} diff --git a/toolkit/components/webextensions/test/xpcshell/head_sync.js b/toolkit/components/webextensions/test/xpcshell/head_sync.js deleted file mode 100644 index 9b66b78e7..000000000 --- a/toolkit/components/webextensions/test/xpcshell/head_sync.js +++ /dev/null @@ -1,67 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -/* exported withSyncContext */ - -Components.utils.import("resource://gre/modules/Services.jsm", this); -Components.utils.import("resource://gre/modules/ExtensionCommon.jsm", this); - -var { - BaseContext, -} = ExtensionCommon; - -class Context extends BaseContext { - constructor(principal) { - super(); - Object.defineProperty(this, "principal", { - value: principal, - configurable: true, - }); - this.sandbox = Components.utils.Sandbox(principal, {wantXrays: false}); - this.extension = {id: "test@web.extension"}; - } - - get cloneScope() { - return this.sandbox; - } -} - -/** - * Call the given function with a newly-constructed context. - * Unload the context on the way out. - * - * @param {function} f the function to call - */ -function* withContext(f) { - const ssm = Services.scriptSecurityManager; - const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org"); - const context = new Context(PRINCIPAL1); - try { - yield* f(context); - } finally { - yield context.unload(); - } -} - -/** - * Like withContext(), but also turn on the "storage.sync" pref for - * the duration of the function. - * Calls to this function can be replaced with calls to withContext - * once the pref becomes on by default. - * - * @param {function} f the function to call - */ -function* withSyncContext(f) { - const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; - let prefs = Services.prefs; - - try { - prefs.setBoolPref(STORAGE_SYNC_PREF, true); - yield* withContext(f); - } finally { - prefs.clearUserPref(STORAGE_SYNC_PREF); - } -} diff --git a/toolkit/components/webextensions/test/xpcshell/native_messaging.ini b/toolkit/components/webextensions/test/xpcshell/native_messaging.ini deleted file mode 100644 index d0e1da163..000000000 --- a/toolkit/components/webextensions/test/xpcshell/native_messaging.ini +++ /dev/null @@ -1,13 +0,0 @@ -[DEFAULT] -head = head.js head_native_messaging.js -tail = -firefox-appdir = browser -skip-if = appname == "thunderbird" || os == "android" -subprocess = true -support-files = - data/** -tags = webextensions - -[test_ext_native_messaging.js] -[test_ext_native_messaging_perf.js] -[test_ext_native_messaging_unresponsive.js] diff --git a/toolkit/components/webextensions/test/xpcshell/test_csp_custom_policies.js b/toolkit/components/webextensions/test/xpcshell/test_csp_custom_policies.js deleted file mode 100644 index b6213baac..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_csp_custom_policies.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -Cu.import("resource://gre/modules/Preferences.jsm"); - -const ADDON_ID = "test@web.extension"; - -const aps = Cc["@mozilla.org/addons/policy-service;1"] - .getService(Ci.nsIAddonPolicyService).wrappedJSObject; - -do_register_cleanup(() => { - aps.setAddonCSP(ADDON_ID, null); -}); - -add_task(function* test_addon_csp() { - equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"), - "Expected base CSP value"); - - equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"), - "Expected default CSP value"); - - equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP, - "CSP for unknown add-on ID should be the default CSP"); - - - const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'"; - - aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY); - - equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy"); - - - aps.setAddonCSP(ADDON_ID, null); - - equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP, - "CSP should revert to default when set to null"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_csp_validator.js b/toolkit/components/webextensions/test/xpcshell/test_csp_validator.js deleted file mode 100644 index 59a7322bc..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_csp_validator.js +++ /dev/null @@ -1,85 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -const cps = Cc["@mozilla.org/addons/content-policy;1"].getService(Ci.nsIAddonContentPolicy); - -add_task(function* test_csp_validator() { - let checkPolicy = (policy, expectedResult, message = null) => { - do_print(`Checking policy: ${policy}`); - - let result = cps.validateAddonCSP(policy); - equal(result, expectedResult); - }; - - checkPolicy("script-src 'self'; object-src 'self';", - null); - - let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='"; - - checkPolicy(`script-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash} 'unsafe-eval'; ` + - `object-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash}`, - null); - - checkPolicy("", - "Policy is missing a required \u2018script-src\u2019 directive"); - - checkPolicy("object-src 'none';", - "Policy is missing a required \u2018script-src\u2019 directive"); - - - checkPolicy("default-src 'self'", null, - "A valid default-src should count as a valid script-src or object-src"); - - checkPolicy("default-src 'self'; script-src 'self'", null, - "A valid default-src should count as a valid script-src or object-src"); - - checkPolicy("default-src 'self'; object-src 'self'", null, - "A valid default-src should count as a valid script-src or object-src"); - - - checkPolicy("default-src 'self'; script-src http://example.com", - "\u2018script-src\u2019 directive contains a forbidden http: protocol source", - "A valid default-src should not allow an invalid script-src directive"); - - checkPolicy("default-src 'self'; object-src http://example.com", - "\u2018object-src\u2019 directive contains a forbidden http: protocol source", - "A valid default-src should not allow an invalid object-src directive"); - - - checkPolicy("script-src 'self';", - "Policy is missing a required \u2018object-src\u2019 directive"); - - checkPolicy("script-src 'none'; object-src 'none'", - "\u2018script-src\u2019 must include the source 'self'"); - - checkPolicy("script-src 'self'; object-src 'none';", - null); - - checkPolicy("script-src 'self' 'unsafe-inline'; object-src 'self';", - "\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword"); - - - let directives = ["script-src", "object-src"]; - - for (let [directive, other] of [directives, directives.slice().reverse()]) { - for (let src of ["https://*", "https://*.blogspot.com", "https://*"]) { - checkPolicy(`${directive} 'self' ${src}; ${other} 'self';`, - `https: wildcard sources in \u2018${directive}\u2019 directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)`); - } - - checkPolicy(`${directive} 'self' https:; ${other} 'self';`, - `https: protocol requires a host in \u2018${directive}\u2019 directives`); - - checkPolicy(`${directive} 'self' http://example.com; ${other} 'self';`, - `\u2018${directive}\u2019 directive contains a forbidden http: protocol source`); - - for (let protocol of ["http", "ftp", "meh"]) { - checkPolicy(`${directive} 'self' ${protocol}:; ${other} 'self';`, - `\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source`); - } - - checkPolicy(`${directive} 'self' 'nonce-01234'; ${other} 'self';`, - `\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword`); - } -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms.js b/toolkit/components/webextensions/test/xpcshell/test_ext_alarms.js deleted file mode 100644 index 936c984c6..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms.js +++ /dev/null @@ -1,210 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_alarm_without_permissions() { - function backgroundScript() { - browser.test.assertTrue(!browser.alarms, - "alarm API is not available when the alarm permission is not required"); - browser.test.notifyPass("alarms_permission"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: [], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarms_permission"); - yield extension.unload(); -}); - - -add_task(function* test_alarm_fires() { - function backgroundScript() { - let ALARM_NAME = "test_ext_alarms"; - let timer; - - browser.alarms.onAlarm.addListener(alarm => { - browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the correct name"); - clearTimeout(timer); - browser.test.notifyPass("alarm-fires"); - }); - - browser.alarms.create(ALARM_NAME, {delayInMinutes: 0.02}); - - timer = setTimeout(async () => { - browser.test.fail("alarm fired within expected time"); - let wasCleared = await browser.alarms.clear(ALARM_NAME); - browser.test.assertTrue(wasCleared, "alarm was cleared"); - browser.test.notifyFail("alarm-fires"); - }, 10000); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarm-fires"); - yield extension.unload(); -}); - - -add_task(function* test_alarm_fires_with_when() { - function backgroundScript() { - let ALARM_NAME = "test_ext_alarms"; - let timer; - - browser.alarms.onAlarm.addListener(alarm => { - browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the expected name"); - clearTimeout(timer); - browser.test.notifyPass("alarm-when"); - }); - - browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000}); - - timer = setTimeout(async () => { - browser.test.fail("alarm fired within expected time"); - let wasCleared = await browser.alarms.clear(ALARM_NAME); - browser.test.assertTrue(wasCleared, "alarm was cleared"); - browser.test.notifyFail("alarm-when"); - }, 10000); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarm-when"); - yield extension.unload(); -}); - - -add_task(function* test_alarm_clear_non_matching_name() { - async function backgroundScript() { - let ALARM_NAME = "test_ext_alarms"; - - browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000}); - - let wasCleared = await browser.alarms.clear(ALARM_NAME + "1"); - browser.test.assertFalse(wasCleared, "alarm was not cleared"); - - let alarms = await browser.alarms.getAll(); - browser.test.assertEq(1, alarms.length, "alarm was not removed"); - browser.test.notifyPass("alarm-clear"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarm-clear"); - yield extension.unload(); -}); - - -add_task(function* test_alarm_get_and_clear_single_argument() { - async function backgroundScript() { - browser.alarms.create({when: Date.now() + 2000}); - - let alarm = await browser.alarms.get(); - browser.test.assertEq("", alarm.name, "expected alarm returned"); - - let wasCleared = await browser.alarms.clear(); - browser.test.assertTrue(wasCleared, "alarm was cleared"); - - let alarms = await browser.alarms.getAll(); - browser.test.assertEq(0, alarms.length, "alarm was removed"); - - browser.test.notifyPass("alarm-single-arg"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarm-single-arg"); - yield extension.unload(); -}); - - -add_task(function* test_get_get_all_clear_all_alarms() { - async function backgroundScript() { - const ALARM_NAME = "test_alarm"; - - let suffixes = [0, 1, 2]; - - for (let suffix of suffixes) { - browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000}); - } - - let alarms = await browser.alarms.getAll(); - browser.test.assertEq(suffixes.length, alarms.length, "expected number of alarms were found"); - alarms.forEach((alarm, index) => { - browser.test.assertEq(ALARM_NAME + index, alarm.name, "alarm has the expected name"); - }); - - - for (let suffix of suffixes) { - let alarm = await browser.alarms.get(ALARM_NAME + suffix); - browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "alarm has the expected name"); - browser.test.sendMessage(`get-${suffix}`); - } - - let wasCleared = await browser.alarms.clear(ALARM_NAME + suffixes[0]); - browser.test.assertTrue(wasCleared, "alarm was cleared"); - - alarms = await browser.alarms.getAll(); - browser.test.assertEq(2, alarms.length, "alarm was removed"); - - let alarm = await browser.alarms.get(ALARM_NAME + suffixes[0]); - browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined"); - browser.test.sendMessage(`get-invalid`); - - wasCleared = await browser.alarms.clearAll(); - browser.test.assertTrue(wasCleared, "alarms were cleared"); - - alarms = await browser.alarms.getAll(); - browser.test.assertEq(0, alarms.length, "no alarms exist"); - browser.test.sendMessage("clearAll"); - browser.test.sendMessage("clear"); - browser.test.sendMessage("getAll"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield Promise.all([ - extension.startup(), - extension.awaitMessage("getAll"), - extension.awaitMessage("get-0"), - extension.awaitMessage("get-1"), - extension.awaitMessage("get-2"), - extension.awaitMessage("clear"), - extension.awaitMessage("get-invalid"), - extension.awaitMessage("clearAll"), - ]); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_does_not_fire.js b/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_does_not_fire.js deleted file mode 100644 index 11407b108..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_does_not_fire.js +++ /dev/null @@ -1,33 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_cleared_alarm_does_not_fire() { - async function backgroundScript() { - let ALARM_NAME = "test_ext_alarms"; - - browser.alarms.onAlarm.addListener(alarm => { - browser.test.fail("cleared alarm does not fire"); - browser.test.notifyFail("alarm-cleared"); - }); - browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000}); - - let wasCleared = await browser.alarms.clear(ALARM_NAME); - browser.test.assertTrue(wasCleared, "alarm was cleared"); - - await new Promise(resolve => setTimeout(resolve, 2000)); - - browser.test.notifyPass("alarm-cleared"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarm-cleared"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_periodic.js b/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_periodic.js deleted file mode 100644 index 6bcdf4e33..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_periodic.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_periodic_alarm_fires() { - function backgroundScript() { - const ALARM_NAME = "test_ext_alarms"; - let count = 0; - let timer; - - browser.alarms.onAlarm.addListener(async alarm => { - browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name"); - if (count++ === 3) { - clearTimeout(timer); - let wasCleared = await browser.alarms.clear(ALARM_NAME); - browser.test.assertTrue(wasCleared, "alarm was cleared"); - - browser.test.notifyPass("alarm-periodic"); - } - }); - - browser.alarms.create(ALARM_NAME, {periodInMinutes: 0.02}); - - timer = setTimeout(async () => { - browser.test.fail("alarm fired expected number of times"); - - let wasCleared = await browser.alarms.clear(ALARM_NAME); - browser.test.assertTrue(wasCleared, "alarm was cleared"); - - browser.test.notifyFail("alarm-periodic"); - }, 30000); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarm-periodic"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_replaces.js b/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_replaces.js deleted file mode 100644 index 96f61acb5..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_alarms_replaces.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - - -add_task(function* test_duplicate_alarm_name_replaces_alarm() { - function backgroundScript() { - let count = 0; - - browser.alarms.onAlarm.addListener(async alarm => { - if (alarm.name === "master alarm") { - browser.alarms.create("child alarm", {delayInMinutes: 0.05}); - let results = await browser.alarms.getAll(); - - browser.test.assertEq(2, results.length, "exactly two alarms exist"); - browser.test.assertEq("master alarm", results[0].name, "first alarm has the expected name"); - browser.test.assertEq("child alarm", results[1].name, "second alarm has the expected name"); - - if (count++ === 3) { - await browser.alarms.clear("master alarm"); - await browser.alarms.clear("child alarm"); - - browser.test.notifyPass("alarm-duplicate"); - } - } else { - browser.test.fail("duplicate named alarm replaced existing alarm"); - browser.test.notifyFail("alarm-duplicate"); - } - }); - - browser.alarms.create("master alarm", {delayInMinutes: 0.025, periodInMinutes: 0.025}); - } - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["alarms"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("alarm-duplicate"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_api_permissions.js b/toolkit/components/webextensions/test/xpcshell/test_ext_api_permissions.js deleted file mode 100644 index d653d0e7a..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_api_permissions.js +++ /dev/null @@ -1,64 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -let {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); -function getNextContext() { - return new Promise(resolve => { - Management.on("proxy-context-load", function listener(type, context) { - Management.off("proxy-context-load", listener); - resolve(context); - }); - }); -} - -add_task(function* test_storage_api_without_permissions() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - // Force API initialization. - void browser.storage; - }, - - manifest: { - permissions: [], - }, - }); - - let contextPromise = getNextContext(); - yield extension.startup(); - - let context = yield contextPromise; - - // Force API initialization. - void context.apiObj; - - ok(!("storage" in context.apiObj), - "The storage API should not be initialized"); - - yield extension.unload(); -}); - -add_task(function* test_storage_api_with_permissions() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - void browser.storage; - }, - - manifest: { - permissions: ["storage"], - }, - }); - - let contextPromise = getNextContext(); - yield extension.startup(); - - let context = yield contextPromise; - - // Force API initialization. - void context.apiObj; - - equal(typeof context.apiObj.storage, "object", - "The storage API should be initialized"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_apimanager.js b/toolkit/components/webextensions/test/xpcshell/test_ext_apimanager.js deleted file mode 100644 index 3f6672a11..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_apimanager.js +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); - -const { - SchemaAPIManager, -} = ExtensionCommon; - -this.unknownvar = "Some module-global var"; - -var gUniqueId = 0; - -// SchemaAPIManager's loadScript uses loadSubScript to load a script. This -// requires a local (resource://) URL. So create such a temporary URL for -// testing. -function toLocalURI(code) { - let dataUrl = `data:charset=utf-8,${encodeURIComponent(code)}`; - let uniqueResPart = `need-a-local-uri-for-subscript-loading-${++gUniqueId}`; - Services.io.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler) - .setSubstitution(uniqueResPart, Services.io.newURI(dataUrl, null, null)); - return `resource://${uniqueResPart}`; -} - -add_task(function* test_global_isolation() { - let manA = new SchemaAPIManager("procA"); - let manB = new SchemaAPIManager("procB"); - - // The "global" variable should be persistent and shared. - manA.loadScript(toLocalURI`global.globalVar = 1;`); - do_check_eq(manA.global.globalVar, 1); - do_check_eq(manA.global.unknownvar, undefined); - manA.loadScript(toLocalURI`global.canSeeGlobal = global.globalVar;`); - do_check_eq(manA.global.canSeeGlobal, 1); - - // Each loadScript call should have their own scope, and global is shared. - manA.loadScript(toLocalURI`this.aVar = 1; global.thisScopeVar = aVar`); - do_check_eq(manA.global.aVar, undefined); - do_check_eq(manA.global.thisScopeVar, 1); - manA.loadScript(toLocalURI`global.differentScopeVar = this.aVar;`); - do_check_eq(manA.global.differentScopeVar, undefined); - manA.loadScript(toLocalURI`global.cantSeeOtherScope = typeof aVar;`); - do_check_eq(manA.global.cantSeeOtherScope, "undefined"); - - manB.loadScript(toLocalURI`global.tryReadOtherGlobal = global.tryagain;`); - do_check_eq(manA.global.tryReadOtherGlobal, undefined); - - // Cu.import without second argument exports to the caller's global. Let's - // verify that it does not leak to the SchemaAPIManager's global. - do_check_eq(typeof ExtensionUtils, "undefined"); // Sanity check #1. - manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); - do_check_eq(manA.global.hasExtUtils, "undefined"); // Sanity check #2 - - Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - do_check_eq(typeof ExtensionUtils, "object"); // Sanity check #3. - - manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); - do_check_eq(manA.global.hasExtUtils, "undefined"); - manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); - do_check_eq(manB.global.hasExtUtils, "undefined"); - - // Confirm that Cu.import does not leak between SchemaAPIManager globals. - manA.loadScript(toLocalURI` - Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - global.hasExtUtils = typeof ExtensionUtils; - `); - do_check_eq(manA.global.hasExtUtils, "object"); // Sanity check. - manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`); - do_check_eq(manB.global.hasExtUtils, "undefined"); - - // Prototype modifications should be isolated. - manA.loadScript(toLocalURI` - Object.prototype.modifiedByA = "Prrft"; - global.fromA = {}; - `); - manA.loadScript(toLocalURI` - global.fromAagain = {}; - `); - manB.loadScript(toLocalURI` - global.fromB = {}; - `); - do_check_eq(manA.global.modifiedByA, "Prrft"); - do_check_eq(manA.global.fromA.modifiedByA, "Prrft"); - do_check_eq(manA.global.fromAagain.modifiedByA, "Prrft"); - do_check_eq(manB.global.modifiedByA, undefined); - do_check_eq(manB.global.fromB.modifiedByA, undefined); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_background_generated_load_events.js b/toolkit/components/webextensions/test/xpcshell/test_ext_background_generated_load_events.js deleted file mode 100644 index 26282fcb9..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_background_generated_load_events.js +++ /dev/null @@ -1,23 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -/* eslint-disable mozilla/balanced-listeners */ - -add_task(function* test_DOMContentLoaded_in_generated_background_page() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - function reportListener(event) { - browser.test.sendMessage("eventname", event.type); - } - document.addEventListener("DOMContentLoaded", reportListener); - window.addEventListener("load", reportListener); - }, - }); - - yield extension.startup(); - equal("DOMContentLoaded", yield extension.awaitMessage("eventname")); - equal("load", yield extension.awaitMessage("eventname")); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_background_generated_reload.js b/toolkit/components/webextensions/test/xpcshell/test_ext_background_generated_reload.js deleted file mode 100644 index 4bf59b798..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_background_generated_reload.js +++ /dev/null @@ -1,24 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_reload_generated_background_page() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - if (location.hash !== "#firstrun") { - browser.test.sendMessage("first run"); - location.hash = "#firstrun"; - browser.test.assertEq("#firstrun", location.hash); - location.reload(); - } else { - browser.test.notifyPass("second run"); - } - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("first run"); - yield extension.awaitFinish("second run"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_background_global_history.js b/toolkit/components/webextensions/test/xpcshell/test_ext_background_global_history.js deleted file mode 100644 index 092a9f5b3..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_background_global_history.js +++ /dev/null @@ -1,22 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -Cu.import("resource://testing-common/PlacesTestUtils.jsm"); - -add_task(function* test_global_history() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - browser.test.sendMessage("background-loaded", location.href); - }, - }); - - yield extension.startup(); - - let backgroundURL = yield extension.awaitMessage("background-loaded"); - - yield extension.unload(); - - let exists = yield PlacesTestUtils.isPageInDB(backgroundURL); - ok(!exists, "Background URL should not be in history database"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_background_private_browsing.js b/toolkit/components/webextensions/test/xpcshell/test_ext_background_private_browsing.js deleted file mode 100644 index 8e8b5e0b0..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_background_private_browsing.js +++ /dev/null @@ -1,40 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -Cu.import("resource://gre/modules/Preferences.jsm"); - -function* testBackgroundPage(expected) { - let extension = ExtensionTestUtils.loadExtension({ - async background() { - browser.test.assertEq(window, browser.extension.getBackgroundPage(), - "Caller should be able to access itself as a background page"); - browser.test.assertEq(window, await browser.runtime.getBackgroundPage(), - "Caller should be able to access itself as a background page"); - - browser.test.sendMessage("incognito", browser.extension.inIncognitoContext); - }, - }); - - yield extension.startup(); - - let incognito = yield extension.awaitMessage("incognito"); - equal(incognito, expected.incognito, "Expected incognito value"); - - yield extension.unload(); -} - -add_task(function* test_background_incognito() { - do_print("Test background page incognito value with permanent private browsing disabled"); - - yield testBackgroundPage({incognito: false}); - - do_print("Test background page incognito value with permanent private browsing enabled"); - - Preferences.set("browser.privatebrowsing.autostart", true); - do_register_cleanup(() => { - Preferences.reset("browser.privatebrowsing.autostart"); - }); - - yield testBackgroundPage({incognito: true}); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_background_runtime_connect_params.js b/toolkit/components/webextensions/test/xpcshell/test_ext_background_runtime_connect_params.js deleted file mode 100644 index 426833edd..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_background_runtime_connect_params.js +++ /dev/null @@ -1,72 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -function backgroundScript() { - let received_ports_number = 0; - - const expected_received_ports_number = 1; - - function countReceivedPorts(port) { - received_ports_number++; - - if (port.name == "check-results") { - browser.runtime.onConnect.removeListener(countReceivedPorts); - - browser.test.assertEq(expected_received_ports_number, received_ports_number, "invalid connect should not create a port"); - - browser.test.notifyPass("runtime.connect invalid params"); - } - } - - browser.runtime.onConnect.addListener(countReceivedPorts); - - let childFrame = document.createElement("iframe"); - childFrame.src = "extensionpage.html"; - document.body.appendChild(childFrame); -} - -function senderScript() { - let detected_invalid_connect_params = 0; - - const invalid_connect_params = [ - // too many params - ["fake-extensions-id", {name: "fake-conn-name"}, "unexpected third params"], - // invalid params format - [{}, {}], - ["fake-extensions-id", "invalid-connect-info-format"], - ]; - const expected_detected_invalid_connect_params = invalid_connect_params.length; - - function assertInvalidConnectParamsException(params) { - try { - browser.runtime.connect(...params); - } catch (e) { - detected_invalid_connect_params++; - browser.test.assertTrue(e.toString().indexOf("Incorrect argument types for runtime.connect.") >= 0, "exception message is correct"); - } - } - for (let params of invalid_connect_params) { - assertInvalidConnectParamsException(params); - } - browser.test.assertEq(expected_detected_invalid_connect_params, detected_invalid_connect_params, "all invalid runtime.connect params detected"); - - browser.runtime.connect(browser.runtime.id, {name: "check-results"}); -} - -let extensionData = { - background: backgroundScript, - files: { - "senderScript.js": senderScript, - "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`, - }, -}; - -add_task(function* test_backgroundRuntimeConnectParams() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitFinish("runtime.connect invalid params"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_background_sub_windows.js b/toolkit/components/webextensions/test/xpcshell/test_ext_background_sub_windows.js deleted file mode 100644 index c5f2f1332..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_background_sub_windows.js +++ /dev/null @@ -1,45 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* testBackgroundWindow() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - browser.test.log("background script executed"); - - browser.test.sendMessage("background-script-load"); - - let img = document.createElement("img"); - img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; - document.body.appendChild(img); - - img.onload = () => { - browser.test.log("image loaded"); - - let iframe = document.createElement("iframe"); - iframe.src = "about:blank?1"; - - iframe.onload = () => { - browser.test.log("iframe loaded"); - setTimeout(() => { - browser.test.notifyPass("background sub-window test done"); - }, 0); - }; - document.body.appendChild(iframe); - }; - }, - }); - - let loadCount = 0; - extension.onMessage("background-script-load", () => { - loadCount++; - }); - - yield extension.startup(); - - yield extension.awaitFinish("background sub-window test done"); - - equal(loadCount, 1, "background script loaded only once"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_background_window_properties.js b/toolkit/components/webextensions/test/xpcshell/test_ext_background_window_properties.js deleted file mode 100644 index 948e2913e..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_background_window_properties.js +++ /dev/null @@ -1,34 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* testBackgroundWindowProperties() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - let expectedValues = { - screenX: 0, - screenY: 0, - outerWidth: 0, - outerHeight: 0, - }; - - for (let k in window) { - try { - if (k in expectedValues) { - browser.test.assertEq(expectedValues[k], window[k], - `should return the expected value for window property: ${k}`); - } else { - void window[k]; - } - } catch (e) { - browser.test.assertEq(null, e, `unexpected exception accessing window property: ${k}`); - } - } - - browser.test.notifyPass("background.testWindowProperties.done"); - }, - }); - yield extension.startup(); - yield extension.awaitFinish("background.testWindowProperties.done"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_contexts.js b/toolkit/components/webextensions/test/xpcshell/test_ext_contexts.js deleted file mode 100644 index 56a14e189..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_contexts.js +++ /dev/null @@ -1,190 +0,0 @@ -"use strict"; - -const global = this; - -Cu.import("resource://gre/modules/Timer.jsm"); - -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -var { - BaseContext, -} = ExtensionCommon; - -var { - EventManager, - SingletonEventManager, -} = ExtensionUtils; - -class StubContext extends BaseContext { - constructor() { - let fakeExtension = {id: "test@web.extension"}; - super("testEnv", fakeExtension); - this.sandbox = Cu.Sandbox(global); - } - - get cloneScope() { - return this.sandbox; - } -} - - -add_task(function* test_post_unload_promises() { - let context = new StubContext(); - - let fail = result => { - ok(false, `Unexpected callback: ${result}`); - }; - - // Make sure promises resolve normally prior to unload. - let promises = [ - context.wrapPromise(Promise.resolve()), - context.wrapPromise(Promise.reject({message: ""})).catch(() => {}), - ]; - - yield Promise.all(promises); - - // Make sure promises that resolve after unload do not trigger - // resolution handlers. - - context.wrapPromise(Promise.resolve("resolved")) - .then(fail); - - context.wrapPromise(Promise.reject({message: "rejected"})) - .then(fail, fail); - - context.unload(); - - // The `setTimeout` ensures that we return to the event loop after - // promise resolution, which means we're guaranteed to return after - // any micro-tasks that get enqueued by the resolution handlers above. - yield new Promise(resolve => setTimeout(resolve, 0)); -}); - - -add_task(function* test_post_unload_listeners() { - let context = new StubContext(); - - let fireEvent; - let onEvent = new EventManager(context, "onEvent", fire => { - fireEvent = fire; - return () => {}; - }); - - let fireSingleton; - let onSingleton = new SingletonEventManager(context, "onSingleton", callback => { - fireSingleton = () => { - Promise.resolve().then(callback); - }; - return () => {}; - }); - - let fail = event => { - ok(false, `Unexpected event: ${event}`); - }; - - // Check that event listeners aren't called after they've been removed. - onEvent.addListener(fail); - onSingleton.addListener(fail); - - let promises = [ - new Promise(resolve => onEvent.addListener(resolve)), - new Promise(resolve => onSingleton.addListener(resolve)), - ]; - - fireEvent("onEvent"); - fireSingleton("onSingleton"); - - // Both `fireEvent` calls are dispatched asynchronously, so they won't - // have fired by this point. The `fail` listeners that we remove now - // should not be called, even though the events have already been - // enqueued. - onEvent.removeListener(fail); - onSingleton.removeListener(fail); - - // Wait for the remaining listeners to be called, which should always - // happen after the `fail` listeners would normally be called. - yield Promise.all(promises); - - // Check that event listeners aren't called after the context has - // unloaded. - onEvent.addListener(fail); - onSingleton.addListener(fail); - - // The EventManager `fire` callback always dispatches events - // asynchronously, so we need to test that any pending event callbacks - // aren't fired after the context unloads. We also need to test that - // any `fire` calls that happen *after* the context is unloaded also - // do not trigger callbacks. - fireEvent("onEvent"); - Promise.resolve("onEvent").then(fireEvent); - - fireSingleton("onSingleton"); - Promise.resolve("onSingleton").then(fireSingleton); - - context.unload(); - - // The `setTimeout` ensures that we return to the event loop after - // promise resolution, which means we're guaranteed to return after - // any micro-tasks that get enqueued by the resolution handlers above. - yield new Promise(resolve => setTimeout(resolve, 0)); -}); - -class Context extends BaseContext { - constructor(principal) { - let fakeExtension = {id: "test@web.extension"}; - super("testEnv", fakeExtension); - Object.defineProperty(this, "principal", { - value: principal, - configurable: true, - }); - this.sandbox = Cu.Sandbox(principal, {wantXrays: false}); - } - - get cloneScope() { - return this.sandbox; - } -} - -let ssm = Services.scriptSecurityManager; -const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org"); -const PRINCIPAL2 = ssm.createCodebasePrincipalFromOrigin("http://www.somethingelse.org"); - -// Test that toJSON() works in the json sandbox -add_task(function* test_stringify_toJSON() { - let context = new Context(PRINCIPAL1); - let obj = Cu.evalInSandbox("({hidden: true, toJSON() { return {visible: true}; } })", context.sandbox); - - let stringified = context.jsonStringify(obj); - let expected = JSON.stringify({visible: true}); - equal(stringified, expected, "Stringified object with toJSON() method is as expected"); -}); - -// Test that stringifying in inaccessible property throws -add_task(function* test_stringify_inaccessible() { - let context = new Context(PRINCIPAL1); - let sandbox = context.sandbox; - let sandbox2 = Cu.Sandbox(PRINCIPAL2); - - Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2); - let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox); - Assert.throws(() => { - context.jsonStringify(obj); - }); -}); - -add_task(function* test_stringify_accessible() { - // Test that an accessible property from another global is included - let principal = Cu.getObjectPrincipal(Cu.Sandbox([PRINCIPAL1, PRINCIPAL2])); - let context = new Context(principal); - let sandbox = context.sandbox; - let sandbox2 = Cu.Sandbox(PRINCIPAL2); - - Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2); - let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox); - let stringified = context.jsonStringify(obj); - - let expected = JSON.stringify({local: true, nested: {subobject: true}}); - equal(stringified, expected, "Stringified object with accessible property is as expected"); -}); - diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads.js b/toolkit/components/webextensions/test/xpcshell/test_ext_downloads.js deleted file mode 100644 index 058b9b18c..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads.js +++ /dev/null @@ -1,76 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_downloads_api_namespace_and_permissions() { - function backgroundScript() { - browser.test.assertTrue(!!browser.downloads, "`downloads` API is present."); - browser.test.assertTrue(!!browser.downloads.FilenameConflictAction, - "`downloads.FilenameConflictAction` enum is present."); - browser.test.assertTrue(!!browser.downloads.InterruptReason, - "`downloads.InterruptReason` enum is present."); - browser.test.assertTrue(!!browser.downloads.DangerType, - "`downloads.DangerType` enum is present."); - browser.test.assertTrue(!!browser.downloads.State, - "`downloads.State` enum is present."); - browser.test.notifyPass("downloads tests"); - } - - let extensionData = { - background: backgroundScript, - manifest: { - permissions: ["downloads", "downloads.open", "downloads.shelf"], - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitFinish("downloads tests"); - yield extension.unload(); -}); - -add_task(function* test_downloads_open_permission() { - function backgroundScript() { - browser.test.assertFalse("open" in browser.downloads, - "`downloads.open` permission is required."); - browser.test.notifyPass("downloads tests"); - } - - let extensionData = { - background: backgroundScript, - manifest: { - permissions: ["downloads"], - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitFinish("downloads tests"); - yield extension.unload(); -}); - -add_task(function* test_downloads_open() { - async function backgroundScript() { - await browser.test.assertRejects( - browser.downloads.open(10), - "Invalid download id 10", - "The error is informative."); - - browser.test.notifyPass("downloads tests"); - - // TODO: Once downloads.{pause,cancel,resume} lands (bug 1245602) test that this gives a good - // error when called with an incompleted download. - } - - let extensionData = { - background: backgroundScript, - manifest: { - permissions: ["downloads", "downloads.open"], - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitFinish("downloads tests"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_download.js b/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_download.js deleted file mode 100644 index 37ddd4d7c..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_download.js +++ /dev/null @@ -1,354 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -/* global OS */ - -Cu.import("resource://gre/modules/osfile.jsm"); -Cu.import("resource://gre/modules/Downloads.jsm"); - -const gServer = createHttpServer(); -gServer.registerDirectory("/data/", do_get_file("data")); - -const WINDOWS = AppConstants.platform == "win"; - -const BASE = `http://localhost:${gServer.identity.primaryPort}/data`; -const FILE_NAME = "file_download.txt"; -const FILE_URL = BASE + "/" + FILE_NAME; -const FILE_NAME_UNIQUE = "file_download(1).txt"; -const FILE_LEN = 46; - -let downloadDir; - -function setup() { - downloadDir = FileUtils.getDir("TmpD", ["downloads"]); - downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - do_print(`Using download directory ${downloadDir.path}`); - - Services.prefs.setIntPref("browser.download.folderList", 2); - Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir); - - do_register_cleanup(() => { - Services.prefs.clearUserPref("browser.download.folderList"); - Services.prefs.clearUserPref("browser.download.dir"); - - let entries = downloadDir.directoryEntries; - while (entries.hasMoreElements()) { - let entry = entries.getNext().QueryInterface(Ci.nsIFile); - ok(false, `Leftover file ${entry.path} in download directory`); - entry.remove(false); - } - - downloadDir.remove(false); - }); -} - -function backgroundScript() { - let blobUrl; - browser.test.onMessage.addListener(async (msg, ...args) => { - if (msg == "download.request") { - let options = args[0]; - - if (options.blobme) { - let blob = new Blob(options.blobme); - delete options.blobme; - blobUrl = options.url = window.URL.createObjectURL(blob); - } - - try { - let id = await browser.downloads.download(options); - browser.test.sendMessage("download.done", {status: "success", id}); - } catch (error) { - browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}); - } - } else if (msg == "killTheBlob") { - window.URL.revokeObjectURL(blobUrl); - blobUrl = null; - } - }); - - browser.test.sendMessage("ready"); -} - -// This function is a bit of a sledgehammer, it looks at every download -// the browser knows about and waits for all active downloads to complete. -// But we only start one at a time and only do a handful in total, so -// this lets us test download() without depending on anything else. -async function waitForDownloads() { - let list = await Downloads.getList(Downloads.ALL); - let downloads = await list.getAll(); - - let inprogress = downloads.filter(dl => !dl.stopped); - return Promise.all(inprogress.map(dl => dl.whenSucceeded())); -} - -// Create a file in the downloads directory. -function touch(filename) { - let file = downloadDir.clone(); - file.append(filename); - file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); -} - -// Remove a file in the downloads directory. -function remove(filename, recursive = false) { - let file = downloadDir.clone(); - file.append(filename); - file.remove(recursive); -} - -add_task(function* test_downloads() { - setup(); - - let extension = ExtensionTestUtils.loadExtension({ - background: `(${backgroundScript})()`, - manifest: { - permissions: ["downloads"], - }, - }); - - function download(options) { - extension.sendMessage("download.request", options); - return extension.awaitMessage("download.done"); - } - - async function testDownload(options, localFile, expectedSize, description) { - let msg = await download(options); - equal(msg.status, "success", `downloads.download() works with ${description}`); - - await waitForDownloads(); - - let localPath = downloadDir.clone(); - let parts = Array.isArray(localFile) ? localFile : [localFile]; - - parts.map(p => localPath.append(p)); - equal(localPath.fileSize, expectedSize, "Downloaded file has expected size"); - localPath.remove(false); - } - - yield extension.startup(); - yield extension.awaitMessage("ready"); - do_print("extension started"); - - // Call download() with just the url property. - yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source"); - - // Call download() with a filename property. - yield testDownload({ - url: FILE_URL, - filename: "newpath.txt", - }, "newpath.txt", FILE_LEN, "source and filename"); - - // Call download() with a filename with subdirs. - yield testDownload({ - url: FILE_URL, - filename: "sub/dir/file", - }, ["sub", "dir", "file"], FILE_LEN, "source and filename with subdirs"); - - // Call download() with a filename with existing subdirs. - yield testDownload({ - url: FILE_URL, - filename: "sub/dir/file2", - }, ["sub", "dir", "file2"], FILE_LEN, "source and filename with existing subdirs"); - - // Only run Windows path separator test on Windows. - if (WINDOWS) { - // Call download() with a filename with Windows path separator. - yield testDownload({ - url: FILE_URL, - filename: "sub\\dir\\file3", - }, ["sub", "dir", "file3"], FILE_LEN, "filename with Windows path separator"); - } - remove("sub", true); - - // Call download(), filename with subdir, skipping parts. - yield testDownload({ - url: FILE_URL, - filename: "skip//part", - }, ["skip", "part"], FILE_LEN, "source, filename, with subdir, skipping parts"); - remove("skip", true); - - // Check conflictAction of "uniquify". - touch(FILE_NAME); - yield testDownload({ - url: FILE_URL, - conflictAction: "uniquify", - }, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify"); - // todo check that preexisting file was not modified? - remove(FILE_NAME); - - // Check conflictAction of "overwrite". - touch(FILE_NAME); - yield testDownload({ - url: FILE_URL, - conflictAction: "overwrite", - }, FILE_NAME, FILE_LEN, "conflictAction=overwrite"); - - // Try to download in invalid url - yield download({url: "this is not a valid URL"}).then(msg => { - equal(msg.status, "error", "downloads.download() fails with invalid url"); - ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct"); - }); - - // Try to download to an empty path. - yield download({ - url: FILE_URL, - filename: "", - }).then(msg => { - equal(msg.status, "error", "downloads.download() fails with empty filename"); - equal(msg.errmsg, "filename must not be empty", "error message for empty filename is correct"); - }); - - // Try to download to an absolute path. - const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt"); - yield download({ - url: FILE_URL, - filename: absolutePath, - }).then(msg => { - equal(msg.status, "error", "downloads.download() fails with absolute filename"); - equal(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`); - }); - - if (WINDOWS) { - yield download({ - url: FILE_URL, - filename: "C:\\file_download.txt", - }).then(msg => { - equal(msg.status, "error", "downloads.download() fails with absolute filename"); - equal(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct"); - }); - } - - // Try to download to a relative path containing .. - yield download({ - url: FILE_URL, - filename: OS.Path.join("..", "file_download.txt"), - }).then(msg => { - equal(msg.status, "error", "downloads.download() fails with back-references"); - equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct"); - }); - - // Try to download to a long relative path containing .. - yield download({ - url: FILE_URL, - filename: OS.Path.join("foo", "..", "..", "file_download.txt"), - }).then(msg => { - equal(msg.status, "error", "downloads.download() fails with back-references"); - equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct"); - }); - - // Try to download a blob url - const BLOB_STRING = "Hello, world"; - yield testDownload({ - blobme: [BLOB_STRING], - filename: FILE_NAME, - }, FILE_NAME, BLOB_STRING.length, "blob url"); - extension.sendMessage("killTheBlob"); - - // Try to download a blob url without a given filename - yield testDownload({ - blobme: [BLOB_STRING], - }, "download", BLOB_STRING.length, "blob url with no filename"); - extension.sendMessage("killTheBlob"); - - yield extension.unload(); -}); - -add_task(function* test_download_post() { - const server = createHttpServer(); - const url = `http://localhost:${server.identity.primaryPort}/post-log`; - - let received; - server.registerPathHandler("/post-log", request => { - received = request; - }); - - // Confirm received vs. expected values. - function confirm(method, headers = {}, body) { - equal(received.method, method, "method is correct"); - - for (let name in headers) { - ok(received.hasHeader(name), `header ${name} received`); - equal(received.getHeader(name), headers[name], `header ${name} is correct`); - } - - if (body) { - const str = NetUtil.readInputStreamToString(received.bodyInputStream, - received.bodyInputStream.available()); - equal(str, body, "body is correct"); - } - } - - function background() { - browser.test.onMessage.addListener(async options => { - try { - await browser.downloads.download(options); - } catch (err) { - browser.test.sendMessage("done", {err: err.message}); - } - }); - browser.downloads.onChanged.addListener(({state}) => { - if (state && state.current === "complete") { - browser.test.sendMessage("done", {ok: true}); - } - }); - } - - const manifest = {permissions: ["downloads"]}; - const extension = ExtensionTestUtils.loadExtension({background, manifest}); - yield extension.startup(); - - function download(options) { - options.url = url; - options.conflictAction = "overwrite"; - - extension.sendMessage(options); - return extension.awaitMessage("done"); - } - - // Test method option. - let result = yield download({}); - ok(result.ok, "download works without the method option, defaults to GET"); - confirm("GET"); - - result = yield download({method: "PUT"}); - ok(!result.ok, "download rejected with PUT method"); - ok(/method: Invalid enumeration/.test(result.err), "descriptive error message"); - - result = yield download({method: "POST"}); - ok(result.ok, "download works with POST method"); - confirm("POST"); - - // Test body option values. - result = yield download({body: []}); - ok(!result.ok, "download rejected because of non-string body"); - ok(/body: Expected string/.test(result.err), "descriptive error message"); - - result = yield download({method: "POST", body: "of work"}); - ok(result.ok, "download works with POST method and body"); - confirm("POST", {"Content-Length": 7}, "of work"); - - // Test custom headers. - result = yield download({headers: [{name: "X-Custom"}]}); - ok(!result.ok, "download rejected because of missing header value"); - ok(/"value" is required/.test(result.err), "descriptive error message"); - - result = yield download({headers: [{name: "X-Custom", value: "13"}]}); - ok(result.ok, "download works with a custom header"); - confirm("GET", {"X-Custom": "13"}); - - // Test forbidden headers. - result = yield download({headers: [{name: "DNT", value: "1"}]}); - ok(!result.ok, "download rejected because of forbidden header name DNT"); - ok(/Forbidden request header/.test(result.err), "descriptive error message"); - - result = yield download({headers: [{name: "Proxy-Connection", value: "keep"}]}); - ok(!result.ok, "download rejected because of forbidden header name prefix Proxy-"); - ok(/Forbidden request header/.test(result.err), "descriptive error message"); - - result = yield download({headers: [{name: "Sec-ret", value: "13"}]}); - ok(!result.ok, "download rejected because of forbidden header name prefix Sec-"); - ok(/Forbidden request header/.test(result.err), "descriptive error message"); - - remove("post-log"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_misc.js b/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_misc.js deleted file mode 100644 index d08aab666..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_misc.js +++ /dev/null @@ -1,862 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -Cu.import("resource://gre/modules/Downloads.jsm"); - -const server = createHttpServer(); -server.registerDirectory("/data/", do_get_file("data")); - -const ROOT = `http://localhost:${server.identity.primaryPort}`; -const BASE = `${ROOT}/data`; -const TXT_FILE = "file_download.txt"; -const TXT_URL = BASE + "/" + TXT_FILE; - -// Keep these in sync with code in interruptible.sjs -const INT_PARTIAL_LEN = 15; -const INT_TOTAL_LEN = 31; - -const TEST_DATA = "This is 31 bytes of sample data"; -const TOTAL_LEN = TEST_DATA.length; -const PARTIAL_LEN = 15; - -// A handler to let us systematically test pausing/resuming/canceling -// of downloads. This target represents a small text file but a simple -// GET will stall after sending part of the data, to give the test code -// a chance to pause or do other operations on an in-progress download. -// A resumed download (ie, a GET with a Range: header) will allow the -// download to complete. -function handleRequest(request, response) { - response.setHeader("Content-Type", "text/plain", false); - - if (request.hasHeader("Range")) { - let start, end; - let matches = request.getHeader("Range") - .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); - if (matches != null) { - start = matches[1] ? parseInt(matches[1], 10) : 0; - end = matches[2] ? parseInt(matches[2], 10) : (TOTAL_LEN - 1); - } - - if (end == undefined || end >= TOTAL_LEN) { - response.setStatusLine(request.httpVersion, 416, "Requested Range Not Satisfiable"); - response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false); - response.finish(); - return; - } - - response.setStatusLine(request.httpVersion, 206, "Partial Content"); - response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false); - response.write(TEST_DATA.slice(start, end + 1)); - } else { - response.processAsync(); - response.setHeader("Content-Length", `${TOTAL_LEN}`, false); - response.write(TEST_DATA.slice(0, PARTIAL_LEN)); - } - - do_register_cleanup(() => { - try { - response.finish(); - } catch (e) { - // This will throw, but we don't care at this point. - } - }); -} - -server.registerPathHandler("/interruptible.html", handleRequest); - -let interruptibleCount = 0; -function getInterruptibleUrl() { - let n = interruptibleCount++; - return `${ROOT}/interruptible.html?count=${n}`; -} - -function backgroundScript() { - let events = new Set(); - let eventWaiter = null; - - browser.downloads.onCreated.addListener(data => { - events.add({type: "onCreated", data}); - if (eventWaiter) { - eventWaiter(); - } - }); - - browser.downloads.onChanged.addListener(data => { - events.add({type: "onChanged", data}); - if (eventWaiter) { - eventWaiter(); - } - }); - - browser.downloads.onErased.addListener(data => { - events.add({type: "onErased", data}); - if (eventWaiter) { - eventWaiter(); - } - }); - - // Returns a promise that will resolve when the given list of expected - // events have all been seen. By default, succeeds only if the exact list - // of expected events is seen in the given order. options.exact can be - // set to false to allow other events and options.inorder can be set to - // false to allow the events to arrive in any order. - function waitForEvents(expected, options = {}) { - function compare(a, b) { - if (typeof b == "object" && b != null) { - if (typeof a != "object") { - return false; - } - return Object.keys(b).every(fld => compare(a[fld], b[fld])); - } - return (a == b); - } - - const exact = ("exact" in options) ? options.exact : true; - const inorder = ("inorder" in options) ? options.inorder : true; - return new Promise((resolve, reject) => { - function check() { - function fail(msg) { - browser.test.fail(msg); - reject(new Error(msg)); - } - if (events.size < expected.length) { - return; - } - if (exact && expected.length < events.size) { - fail(`Got ${events.size} events but only expected ${expected.length}`); - return; - } - - let remaining = new Set(events); - if (inorder) { - for (let event of events) { - if (compare(event, expected[0])) { - expected.shift(); - remaining.delete(event); - } - } - } else { - expected = expected.filter(val => { - for (let remainingEvent of remaining) { - if (compare(remainingEvent, val)) { - remaining.delete(remainingEvent); - return false; - } - } - return true; - }); - } - - // Events that did occur have been removed from expected so if - // expected is empty, we're done. If we didn't see all the - // expected events and we're not looking for an exact match, - // then we just may not have seen the event yet, so return without - // failing and check() will be called again when a new event arrives. - if (expected.length == 0) { - events = remaining; - eventWaiter = null; - resolve(); - } else if (exact) { - fail(`Mismatched event: expecting ${JSON.stringify(expected[0])} but got ${JSON.stringify(Array.from(remaining)[0])}`); - } - } - eventWaiter = check; - check(); - }); - } - - browser.test.onMessage.addListener(async (msg, ...args) => { - let match = msg.match(/(\w+).request$/); - if (!match) { - return; - } - - let what = match[1]; - if (what == "waitForEvents") { - try { - await waitForEvents(...args); - browser.test.sendMessage("waitForEvents.done", {status: "success"}); - } catch (error) { - browser.test.sendMessage("waitForEvents.done", {status: "error", errmsg: error.message}); - } - } else if (what == "clearEvents") { - events = new Set(); - browser.test.sendMessage("clearEvents.done", {status: "success"}); - } else { - try { - let result = await browser.downloads[what](...args); - browser.test.sendMessage(`${what}.done`, {status: "success", result}); - } catch (error) { - browser.test.sendMessage(`${what}.done`, {status: "error", errmsg: error.message}); - } - } - }); - - browser.test.sendMessage("ready"); -} - -let downloadDir; -let extension; - -async function clearDownloads(callback) { - let list = await Downloads.getList(Downloads.ALL); - let downloads = await list.getAll(); - - await Promise.all(downloads.map(download => list.remove(download))); - - return downloads; -} - -function runInExtension(what, ...args) { - extension.sendMessage(`${what}.request`, ...args); - return extension.awaitMessage(`${what}.done`); -} - -// This is pretty simplistic, it looks for a progress update for a -// download of the given url in which the total bytes are exactly equal -// to the given value. Unless you know exactly how data will arrive from -// the server (eg see interruptible.sjs), it probably isn't very useful. -async function waitForProgress(url, bytes) { - let list = await Downloads.getList(Downloads.ALL); - - return new Promise(resolve => { - const view = { - onDownloadChanged(download) { - if (download.source.url == url && download.currentBytes == bytes) { - list.removeView(view); - resolve(); - } - }, - }; - list.addView(view); - }); -} - -add_task(function* setup() { - const nsIFile = Ci.nsIFile; - downloadDir = FileUtils.getDir("TmpD", ["downloads"]); - downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - do_print(`downloadDir ${downloadDir.path}`); - - Services.prefs.setIntPref("browser.download.folderList", 2); - Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir); - - do_register_cleanup(() => { - Services.prefs.clearUserPref("browser.download.folderList"); - Services.prefs.clearUserPref("browser.download.dir"); - downloadDir.remove(true); - - return clearDownloads(); - }); - - yield clearDownloads().then(downloads => { - do_print(`removed ${downloads.length} pre-existing downloads from history`); - }); - - extension = ExtensionTestUtils.loadExtension({ - background: backgroundScript, - manifest: { - permissions: ["downloads"], - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("ready"); -}); - -add_task(function* test_events() { - let msg = yield runInExtension("download", {url: TXT_URL}); - equal(msg.status, "success", "download() succeeded"); - const id = msg.result; - - msg = yield runInExtension("waitForEvents", [ - {type: "onCreated", data: {id, url: TXT_URL}}, - { - type: "onChanged", - data: { - id, - state: { - previous: "in_progress", - current: "complete", - }, - }, - }, - ]); - equal(msg.status, "success", "got onCreated and onChanged events"); -}); - -add_task(function* test_cancel() { - let url = getInterruptibleUrl(); - do_print(url); - let msg = yield runInExtension("download", {url}); - equal(msg.status, "success", "download() succeeded"); - const id = msg.result; - - let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); - - msg = yield runInExtension("waitForEvents", [ - {type: "onCreated", data: {id}}, - ]); - equal(msg.status, "success", "got created and changed events"); - - yield progressPromise; - do_print(`download reached ${INT_PARTIAL_LEN} bytes`); - - msg = yield runInExtension("cancel", id); - equal(msg.status, "success", "cancel() succeeded"); - - // This sequence of events is bogus (bug 1256243) - msg = yield runInExtension("waitForEvents", [ - { - type: "onChanged", - data: { - state: { - previous: "in_progress", - current: "interrupted", - }, - paused: { - previous: false, - current: true, - }, - }, - }, { - type: "onChanged", - data: { - id, - error: { - previous: null, - current: "USER_CANCELED", - }, - }, - }, { - type: "onChanged", - data: { - id, - paused: { - previous: true, - current: false, - }, - }, - }, - ]); - equal(msg.status, "success", "got onChanged events corresponding to cancel()"); - - msg = yield runInExtension("search", {error: "USER_CANCELED"}); - equal(msg.status, "success", "search() succeeded"); - equal(msg.result.length, 1, "search() found 1 download"); - equal(msg.result[0].id, id, "download.id is correct"); - equal(msg.result[0].state, "interrupted", "download.state is correct"); - equal(msg.result[0].paused, false, "download.paused is correct"); - equal(msg.result[0].canResume, false, "download.canResume is correct"); - equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); - equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); - equal(msg.result[0].exists, false, "download.exists is correct"); - - msg = yield runInExtension("pause", id); - equal(msg.status, "error", "cannot pause a canceled download"); - - msg = yield runInExtension("resume", id); - equal(msg.status, "error", "cannot resume a canceled download"); -}); - -add_task(function* test_pauseresume() { - let url = getInterruptibleUrl(); - let msg = yield runInExtension("download", {url}); - equal(msg.status, "success", "download() succeeded"); - const id = msg.result; - - let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); - - msg = yield runInExtension("waitForEvents", [ - {type: "onCreated", data: {id}}, - ]); - equal(msg.status, "success", "got created and changed events"); - - yield progressPromise; - do_print(`download reached ${INT_PARTIAL_LEN} bytes`); - - msg = yield runInExtension("pause", id); - equal(msg.status, "success", "pause() succeeded"); - - msg = yield runInExtension("waitForEvents", [ - { - type: "onChanged", - data: { - id, - state: { - previous: "in_progress", - current: "interrupted", - }, - paused: { - previous: false, - current: true, - }, - canResume: { - previous: false, - current: true, - }, - }, - }, { - type: "onChanged", - data: { - id, - error: { - previous: null, - current: "USER_CANCELED", - }, - }, - }]); - equal(msg.status, "success", "got onChanged event corresponding to pause"); - - msg = yield runInExtension("search", {paused: true}); - equal(msg.status, "success", "search() succeeded"); - equal(msg.result.length, 1, "search() found 1 download"); - equal(msg.result[0].id, id, "download.id is correct"); - equal(msg.result[0].state, "interrupted", "download.state is correct"); - equal(msg.result[0].paused, true, "download.paused is correct"); - equal(msg.result[0].canResume, true, "download.canResume is correct"); - equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); - equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct"); - equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); - equal(msg.result[0].exists, false, "download.exists is correct"); - - msg = yield runInExtension("search", {error: "USER_CANCELED"}); - equal(msg.status, "success", "search() succeeded"); - let found = msg.result.filter(item => item.id == id); - equal(found.length, 1, "search() by error found the paused download"); - - msg = yield runInExtension("pause", id); - equal(msg.status, "error", "cannot pause an already paused download"); - - msg = yield runInExtension("resume", id); - equal(msg.status, "success", "resume() succeeded"); - - msg = yield runInExtension("waitForEvents", [ - { - type: "onChanged", - data: { - id, - state: { - previous: "interrupted", - current: "in_progress", - }, - paused: { - previous: true, - current: false, - }, - canResume: { - previous: true, - current: false, - }, - error: { - previous: "USER_CANCELED", - current: null, - }, - }, - }, - { - type: "onChanged", - data: { - id, - state: { - previous: "in_progress", - current: "complete", - }, - }, - }, - ]); - equal(msg.status, "success", "got onChanged events for resume and complete"); - - msg = yield runInExtension("search", {id}); - equal(msg.status, "success", "search() succeeded"); - equal(msg.result.length, 1, "search() found 1 download"); - equal(msg.result[0].state, "complete", "download.state is correct"); - equal(msg.result[0].paused, false, "download.paused is correct"); - equal(msg.result[0].canResume, false, "download.canResume is correct"); - equal(msg.result[0].error, null, "download.error is correct"); - equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct"); - equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); - equal(msg.result[0].exists, true, "download.exists is correct"); - - msg = yield runInExtension("pause", id); - equal(msg.status, "error", "cannot pause a completed download"); - - msg = yield runInExtension("resume", id); - equal(msg.status, "error", "cannot resume a completed download"); -}); - -add_task(function* test_pausecancel() { - let url = getInterruptibleUrl(); - let msg = yield runInExtension("download", {url}); - equal(msg.status, "success", "download() succeeded"); - const id = msg.result; - - let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); - - msg = yield runInExtension("waitForEvents", [ - {type: "onCreated", data: {id}}, - ]); - equal(msg.status, "success", "got created and changed events"); - - yield progressPromise; - do_print(`download reached ${INT_PARTIAL_LEN} bytes`); - - msg = yield runInExtension("pause", id); - equal(msg.status, "success", "pause() succeeded"); - - msg = yield runInExtension("waitForEvents", [ - { - type: "onChanged", - data: { - id, - state: { - previous: "in_progress", - current: "interrupted", - }, - paused: { - previous: false, - current: true, - }, - canResume: { - previous: false, - current: true, - }, - }, - }, { - type: "onChanged", - data: { - id, - error: { - previous: null, - current: "USER_CANCELED", - }, - }, - }]); - equal(msg.status, "success", "got onChanged event corresponding to pause"); - - msg = yield runInExtension("search", {paused: true}); - equal(msg.status, "success", "search() succeeded"); - equal(msg.result.length, 1, "search() found 1 download"); - equal(msg.result[0].id, id, "download.id is correct"); - equal(msg.result[0].state, "interrupted", "download.state is correct"); - equal(msg.result[0].paused, true, "download.paused is correct"); - equal(msg.result[0].canResume, true, "download.canResume is correct"); - equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); - equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct"); - equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); - equal(msg.result[0].exists, false, "download.exists is correct"); - - msg = yield runInExtension("search", {error: "USER_CANCELED"}); - equal(msg.status, "success", "search() succeeded"); - let found = msg.result.filter(item => item.id == id); - equal(found.length, 1, "search() by error found the paused download"); - - msg = yield runInExtension("cancel", id); - equal(msg.status, "success", "cancel() succeeded"); - - msg = yield runInExtension("waitForEvents", [ - { - type: "onChanged", - data: { - id, - paused: { - previous: true, - current: false, - }, - canResume: { - previous: true, - current: false, - }, - }, - }, - ]); - equal(msg.status, "success", "got onChanged event for cancel"); - - msg = yield runInExtension("search", {id}); - equal(msg.status, "success", "search() succeeded"); - equal(msg.result.length, 1, "search() found 1 download"); - equal(msg.result[0].state, "interrupted", "download.state is correct"); - equal(msg.result[0].paused, false, "download.paused is correct"); - equal(msg.result[0].canResume, false, "download.canResume is correct"); - equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); - equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct"); - equal(msg.result[0].exists, false, "download.exists is correct"); -}); - -add_task(function* test_pause_resume_cancel_badargs() { - let BAD_ID = 1000; - - let msg = yield runInExtension("pause", BAD_ID); - equal(msg.status, "error", "pause() failed with a bad download id"); - ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); - - msg = yield runInExtension("resume", BAD_ID); - equal(msg.status, "error", "resume() failed with a bad download id"); - ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); - - msg = yield runInExtension("cancel", BAD_ID); - equal(msg.status, "error", "cancel() failed with a bad download id"); - ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); -}); - -add_task(function* test_file_removal() { - let msg = yield runInExtension("download", {url: TXT_URL}); - equal(msg.status, "success", "download() succeeded"); - const id = msg.result; - - msg = yield runInExtension("waitForEvents", [ - {type: "onCreated", data: {id, url: TXT_URL}}, - { - type: "onChanged", - data: { - id, - state: { - previous: "in_progress", - current: "complete", - }, - }, - }, - ]); - - equal(msg.status, "success", "got onCreated and onChanged events"); - - msg = yield runInExtension("removeFile", id); - equal(msg.status, "success", "removeFile() succeeded"); - - msg = yield runInExtension("removeFile", id); - equal(msg.status, "error", "removeFile() fails since the file was already removed."); - ok(/file doesn't exist/.test(msg.errmsg), "removeFile() failed on removed file."); - - msg = yield runInExtension("removeFile", 1000); - ok(/Invalid download id/.test(msg.errmsg), "removeFile() failed due to non-existent id"); -}); - -add_task(function* test_removal_of_incomplete_download() { - let url = getInterruptibleUrl(); - let msg = yield runInExtension("download", {url}); - equal(msg.status, "success", "download() succeeded"); - const id = msg.result; - - let progressPromise = waitForProgress(url, INT_PARTIAL_LEN); - - msg = yield runInExtension("waitForEvents", [ - {type: "onCreated", data: {id}}, - ]); - equal(msg.status, "success", "got created and changed events"); - - yield progressPromise; - do_print(`download reached ${INT_PARTIAL_LEN} bytes`); - - msg = yield runInExtension("pause", id); - equal(msg.status, "success", "pause() succeeded"); - - msg = yield runInExtension("waitForEvents", [ - { - type: "onChanged", - data: { - id, - state: { - previous: "in_progress", - current: "interrupted", - }, - paused: { - previous: false, - current: true, - }, - canResume: { - previous: false, - current: true, - }, - }, - }, { - type: "onChanged", - data: { - id, - error: { - previous: null, - current: "USER_CANCELED", - }, - }, - }]); - equal(msg.status, "success", "got onChanged event corresponding to pause"); - - msg = yield runInExtension("removeFile", id); - equal(msg.status, "error", "removeFile() on paused download failed"); - - ok(/Cannot remove incomplete download/.test(msg.errmsg), "removeFile() failed due to download being incomplete"); - - msg = yield runInExtension("resume", id); - equal(msg.status, "success", "resume() succeeded"); - - msg = yield runInExtension("waitForEvents", [ - { - type: "onChanged", - data: { - id, - state: { - previous: "interrupted", - current: "in_progress", - }, - paused: { - previous: true, - current: false, - }, - canResume: { - previous: true, - current: false, - }, - error: { - previous: "USER_CANCELED", - current: null, - }, - }, - }, - { - type: "onChanged", - data: { - id, - state: { - previous: "in_progress", - current: "complete", - }, - }, - }, - ]); - equal(msg.status, "success", "got onChanged events for resume and complete"); - - msg = yield runInExtension("removeFile", id); - equal(msg.status, "success", "removeFile() succeeded following completion of resumed download."); -}); - -// Test erase(). We don't do elaborate testing of the query handling -// since it uses the exact same engine as search() which is tested -// more thoroughly in test_chrome_ext_downloads_search.html -add_task(function* test_erase() { - yield clearDownloads(); - - yield runInExtension("clearEvents"); - - function* download() { - let msg = yield runInExtension("download", {url: TXT_URL}); - equal(msg.status, "success", "download succeeded"); - let id = msg.result; - - msg = yield runInExtension("waitForEvents", [{ - type: "onChanged", data: {id, state: {current: "complete"}}, - }], {exact: false}); - equal(msg.status, "success", "download finished"); - - return id; - } - - let ids = {}; - ids.dl1 = yield download(); - ids.dl2 = yield download(); - ids.dl3 = yield download(); - - let msg = yield runInExtension("search", {}); - equal(msg.status, "success", "search succeded"); - equal(msg.result.length, 3, "search found 3 downloads"); - - msg = yield runInExtension("clearEvents"); - - msg = yield runInExtension("erase", {id: ids.dl1}); - equal(msg.status, "success", "erase by id succeeded"); - - msg = yield runInExtension("waitForEvents", [ - {type: "onErased", data: ids.dl1}, - ]); - equal(msg.status, "success", "received onErased event"); - - msg = yield runInExtension("search", {}); - equal(msg.status, "success", "search succeded"); - equal(msg.result.length, 2, "search found 2 downloads"); - - msg = yield runInExtension("erase", {}); - equal(msg.status, "success", "erase everything succeeded"); - - msg = yield runInExtension("waitForEvents", [ - {type: "onErased", data: ids.dl2}, - {type: "onErased", data: ids.dl3}, - ], {inorder: false}); - equal(msg.status, "success", "received 2 onErased events"); - - msg = yield runInExtension("search", {}); - equal(msg.status, "success", "search succeded"); - equal(msg.result.length, 0, "search found 0 downloads"); -}); - -function loadImage(img, data) { - return new Promise((resolve) => { - img.src = data; - img.onload = resolve; - }); -} - -add_task(function* test_getFileIcon() { - let webNav = Services.appShell.createWindowlessBrowser(false); - let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell); - - let system = Services.scriptSecurityManager.getSystemPrincipal(); - docShell.createAboutBlankContentViewer(system); - - let img = webNav.document.createElement("img"); - - let msg = yield runInExtension("download", {url: TXT_URL}); - equal(msg.status, "success", "download() succeeded"); - const id = msg.result; - - msg = yield runInExtension("getFileIcon", id); - equal(msg.status, "success", "getFileIcon() succeeded"); - yield loadImage(img, msg.result); - equal(img.height, 32, "returns an icon with the right height"); - equal(img.width, 32, "returns an icon with the right width"); - - msg = yield runInExtension("waitForEvents", [ - {type: "onCreated", data: {id, url: TXT_URL}}, - {type: "onChanged"}, - ]); - equal(msg.status, "success", "got events"); - - msg = yield runInExtension("getFileIcon", id); - equal(msg.status, "success", "getFileIcon() succeeded"); - yield loadImage(img, msg.result); - equal(img.height, 32, "returns an icon with the right height after download"); - equal(img.width, 32, "returns an icon with the right width after download"); - - msg = yield runInExtension("getFileIcon", id + 100); - equal(msg.status, "error", "getFileIcon() failed"); - ok(msg.errmsg.includes("Invalid download id"), "download id is invalid"); - - msg = yield runInExtension("getFileIcon", id, {size: 127}); - equal(msg.status, "success", "getFileIcon() succeeded"); - yield loadImage(img, msg.result); - equal(img.height, 127, "returns an icon with the right custom height"); - equal(img.width, 127, "returns an icon with the right custom width"); - - msg = yield runInExtension("getFileIcon", id, {size: 1}); - equal(msg.status, "success", "getFileIcon() succeeded"); - yield loadImage(img, msg.result); - equal(img.height, 1, "returns an icon with the right custom height"); - equal(img.width, 1, "returns an icon with the right custom width"); - - msg = yield runInExtension("getFileIcon", id, {size: "foo"}); - equal(msg.status, "error", "getFileIcon() fails"); - ok(msg.errmsg.includes("Error processing size"), "size is not a number"); - - msg = yield runInExtension("getFileIcon", id, {size: 0}); - equal(msg.status, "error", "getFileIcon() fails"); - ok(msg.errmsg.includes("Error processing size"), "size is too small"); - - msg = yield runInExtension("getFileIcon", id, {size: 128}); - equal(msg.status, "error", "getFileIcon() fails"); - ok(msg.errmsg.includes("Error processing size"), "size is too big"); - - webNav.close(); -}); - -add_task(function* cleanup() { - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_search.js b/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_search.js deleted file mode 100644 index 4caa82456..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_downloads_search.js +++ /dev/null @@ -1,402 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -Cu.import("resource://gre/modules/Downloads.jsm"); - -const server = createHttpServer(); -server.registerDirectory("/data/", do_get_file("data")); - -const BASE = `http://localhost:${server.identity.primaryPort}/data`; -const TXT_FILE = "file_download.txt"; -const TXT_URL = BASE + "/" + TXT_FILE; -const TXT_LEN = 46; -const HTML_FILE = "file_download.html"; -const HTML_URL = BASE + "/" + HTML_FILE; -const HTML_LEN = 117; -const BIG_LEN = 1000; // something bigger both TXT_LEN and HTML_LEN - -function backgroundScript() { - let complete = new Map(); - - function waitForComplete(id) { - if (complete.has(id)) { - return complete.get(id).promise; - } - - let promise = new Promise(resolve => { - complete.set(id, {resolve}); - }); - complete.get(id).promise = promise; - return promise; - } - - browser.downloads.onChanged.addListener(change => { - if (change.state && change.state.current == "complete") { - // Make sure we have a promise. - waitForComplete(change.id); - complete.get(change.id).resolve(); - } - }); - - browser.test.onMessage.addListener(async (msg, ...args) => { - if (msg == "download.request") { - try { - let id = await browser.downloads.download(args[0]); - browser.test.sendMessage("download.done", {status: "success", id}); - } catch (error) { - browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}); - } - } else if (msg == "search.request") { - try { - let downloads = await browser.downloads.search(args[0]); - browser.test.sendMessage("search.done", {status: "success", downloads}); - } catch (error) { - browser.test.sendMessage("search.done", {status: "error", errmsg: error.message}); - } - } else if (msg == "waitForComplete.request") { - await waitForComplete(args[0]); - browser.test.sendMessage("waitForComplete.done"); - } - }); - - browser.test.sendMessage("ready"); -} - -async function clearDownloads(callback) { - let list = await Downloads.getList(Downloads.ALL); - let downloads = await list.getAll(); - - await Promise.all(downloads.map(download => list.remove(download))); - - return downloads; -} - -add_task(function* test_search() { - const nsIFile = Ci.nsIFile; - let downloadDir = FileUtils.getDir("TmpD", ["downloads"]); - downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - do_print(`downloadDir ${downloadDir.path}`); - - function downloadPath(filename) { - let path = downloadDir.clone(); - path.append(filename); - return path.path; - } - - Services.prefs.setIntPref("browser.download.folderList", 2); - Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir); - - do_register_cleanup(async () => { - Services.prefs.clearUserPref("browser.download.folderList"); - Services.prefs.clearUserPref("browser.download.dir"); - await cleanupDir(downloadDir); - await clearDownloads(); - }); - - yield clearDownloads().then(downloads => { - do_print(`removed ${downloads.length} pre-existing downloads from history`); - }); - - let extension = ExtensionTestUtils.loadExtension({ - background: backgroundScript, - manifest: { - permissions: ["downloads"], - }, - }); - - async function download(options) { - extension.sendMessage("download.request", options); - let result = await extension.awaitMessage("download.done"); - - if (result.status == "success") { - do_print(`wait for onChanged event to indicate ${result.id} is complete`); - extension.sendMessage("waitForComplete.request", result.id); - - await extension.awaitMessage("waitForComplete.done"); - } - - return result; - } - - function search(query) { - extension.sendMessage("search.request", query); - return extension.awaitMessage("search.done"); - } - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - // Do some downloads... - const time1 = new Date(); - - let downloadIds = {}; - let msg = yield download({url: TXT_URL}); - equal(msg.status, "success", "download() succeeded"); - downloadIds.txt1 = msg.id; - - const TXT_FILE2 = "NewFile.txt"; - msg = yield download({url: TXT_URL, filename: TXT_FILE2}); - equal(msg.status, "success", "download() succeeded"); - downloadIds.txt2 = msg.id; - - const time2 = new Date(); - - msg = yield download({url: HTML_URL}); - equal(msg.status, "success", "download() succeeded"); - downloadIds.html1 = msg.id; - - const HTML_FILE2 = "renamed.html"; - msg = yield download({url: HTML_URL, filename: HTML_FILE2}); - equal(msg.status, "success", "download() succeeded"); - downloadIds.html2 = msg.id; - - const time3 = new Date(); - - // Search for each individual download and check - // the corresponding DownloadItem. - function* checkDownloadItem(id, expect) { - let item = yield search({id}); - equal(item.status, "success", "search() succeeded"); - equal(item.downloads.length, 1, "search() found exactly 1 download"); - - Object.keys(expect).forEach(function(field) { - equal(item.downloads[0][field], expect[field], `DownloadItem.${field} is correct"`); - }); - } - yield checkDownloadItem(downloadIds.txt1, { - url: TXT_URL, - filename: downloadPath(TXT_FILE), - mime: "text/plain", - state: "complete", - bytesReceived: TXT_LEN, - totalBytes: TXT_LEN, - fileSize: TXT_LEN, - exists: true, - }); - - yield checkDownloadItem(downloadIds.txt2, { - url: TXT_URL, - filename: downloadPath(TXT_FILE2), - mime: "text/plain", - state: "complete", - bytesReceived: TXT_LEN, - totalBytes: TXT_LEN, - fileSize: TXT_LEN, - exists: true, - }); - - yield checkDownloadItem(downloadIds.html1, { - url: HTML_URL, - filename: downloadPath(HTML_FILE), - mime: "text/html", - state: "complete", - bytesReceived: HTML_LEN, - totalBytes: HTML_LEN, - fileSize: HTML_LEN, - exists: true, - }); - - yield checkDownloadItem(downloadIds.html2, { - url: HTML_URL, - filename: downloadPath(HTML_FILE2), - mime: "text/html", - state: "complete", - bytesReceived: HTML_LEN, - totalBytes: HTML_LEN, - fileSize: HTML_LEN, - exists: true, - }); - - function* checkSearch(query, expected, description, exact) { - let item = yield search(query); - equal(item.status, "success", "search() succeeded"); - equal(item.downloads.length, expected.length, `search() for ${description} found exactly ${expected.length} downloads`); - - let receivedIds = item.downloads.map(i => i.id); - if (exact) { - receivedIds.forEach((id, idx) => { - equal(id, downloadIds[expected[idx]], `search() for ${description} returned ${expected[idx]} in position ${idx}`); - }); - } else { - Object.keys(downloadIds).forEach(key => { - const id = downloadIds[key]; - const thisExpected = expected.includes(key); - equal(receivedIds.includes(id), thisExpected, - `search() for ${description} ${thisExpected ? "includes" : "does not include"} ${key}`); - }); - } - } - - // Check that search with an invalid id returns nothing. - // NB: for now ids are not persistent and we start numbering them at 1 - // so a sufficiently large number will be unused. - const INVALID_ID = 1000; - yield checkSearch({id: INVALID_ID}, [], "invalid id"); - - // Check that search on url works. - yield checkSearch({url: TXT_URL}, ["txt1", "txt2"], "url"); - - // Check that regexp on url works. - const HTML_REGEX = "[downlad]{8}\.html+$"; - yield checkSearch({urlRegex: HTML_REGEX}, ["html1", "html2"], "url regexp"); - - // Check that compatible url+regexp works - yield checkSearch({url: HTML_URL, urlRegex: HTML_REGEX}, ["html1", "html2"], "compatible url+urlRegex"); - - // Check that incompatible url+regexp works - yield checkSearch({url: TXT_URL, urlRegex: HTML_REGEX}, [], "incompatible url+urlRegex"); - - // Check that search on filename works. - yield checkSearch({filename: downloadPath(TXT_FILE)}, ["txt1"], "filename"); - - // Check that regexp on filename works. - yield checkSearch({filenameRegex: HTML_REGEX}, ["html1"], "filename regex"); - - // Check that compatible filename+regexp works - yield checkSearch({filename: downloadPath(HTML_FILE), filenameRegex: HTML_REGEX}, ["html1"], "compatible filename+filename regex"); - - // Check that incompatible filename+regexp works - yield checkSearch({filename: downloadPath(TXT_FILE), filenameRegex: HTML_REGEX}, [], "incompatible filename+filename regex"); - - // Check that simple positive search terms work. - yield checkSearch({query: ["file_download"]}, ["txt1", "txt2", "html1", "html2"], - "term file_download"); - yield checkSearch({query: ["NewFile"]}, ["txt2"], "term NewFile"); - - // Check that positive search terms work case-insensitive. - yield checkSearch({query: ["nEwfILe"]}, ["txt2"], "term nEwfiLe"); - - // Check that negative search terms work. - yield checkSearch({query: ["-txt"]}, ["html1", "html2"], "term -txt"); - - // Check that positive and negative search terms together work. - yield checkSearch({query: ["html", "-renamed"]}, ["html1"], "postive and negative terms"); - - function* checkSearchWithDate(query, expected, description) { - const fields = Object.keys(query); - if (fields.length != 1 || !(query[fields[0]] instanceof Date)) { - throw new Error("checkSearchWithDate expects exactly one Date field"); - } - const field = fields[0]; - const date = query[field]; - - let newquery = {}; - - // Check as a Date - newquery[field] = date; - yield checkSearch(newquery, expected, `${description} as Date`); - - // Check as numeric milliseconds - newquery[field] = date.valueOf(); - yield checkSearch(newquery, expected, `${description} as numeric ms`); - - // Check as stringified milliseconds - newquery[field] = date.valueOf().toString(); - yield checkSearch(newquery, expected, `${description} as string ms`); - - // Check as ISO string - newquery[field] = date.toISOString(); - yield checkSearch(newquery, expected, `${description} as iso string`); - } - - // Check startedBefore - yield checkSearchWithDate({startedBefore: time1}, [], "before time1"); - yield checkSearchWithDate({startedBefore: time2}, ["txt1", "txt2"], "before time2"); - yield checkSearchWithDate({startedBefore: time3}, ["txt1", "txt2", "html1", "html2"], "before time3"); - - // Check startedAfter - yield checkSearchWithDate({startedAfter: time1}, ["txt1", "txt2", "html1", "html2"], "after time1"); - yield checkSearchWithDate({startedAfter: time2}, ["html1", "html2"], "after time2"); - yield checkSearchWithDate({startedAfter: time3}, [], "after time3"); - - // Check simple search on totalBytes - yield checkSearch({totalBytes: TXT_LEN}, ["txt1", "txt2"], "totalBytes"); - yield checkSearch({totalBytes: HTML_LEN}, ["html1", "html2"], "totalBytes"); - - // Check simple test on totalBytes{Greater,Less} - // (NB: TXT_LEN < HTML_LEN < BIG_LEN) - yield checkSearch({totalBytesGreater: 0}, ["txt1", "txt2", "html1", "html2"], "totalBytesGreater than 0"); - yield checkSearch({totalBytesGreater: TXT_LEN}, ["html1", "html2"], `totalBytesGreater than ${TXT_LEN}`); - yield checkSearch({totalBytesGreater: HTML_LEN}, [], `totalBytesGreater than ${HTML_LEN}`); - yield checkSearch({totalBytesLess: TXT_LEN}, [], `totalBytesLess than ${TXT_LEN}`); - yield checkSearch({totalBytesLess: HTML_LEN}, ["txt1", "txt2"], `totalBytesLess than ${HTML_LEN}`); - yield checkSearch({totalBytesLess: BIG_LEN}, ["txt1", "txt2", "html1", "html2"], `totalBytesLess than ${BIG_LEN}`); - - // Check good combinations of totalBytes*. - yield checkSearch({totalBytes: HTML_LEN, totalBytesGreater: TXT_LEN}, ["html1", "html2"], "totalBytes and totalBytesGreater"); - yield checkSearch({totalBytes: TXT_LEN, totalBytesLess: HTML_LEN}, ["txt1", "txt2"], "totalBytes and totalBytesGreater"); - yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: BIG_LEN, totalBytesGreater: 0}, ["html1", "html2"], "totalBytes and totalBytesLess and totalBytesGreater"); - - // Check bad combination of totalBytes*. - yield checkSearch({totalBytesLess: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytesLess, totalBytesGreater combination"); - yield checkSearch({totalBytes: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytes, totalBytesGreater combination"); - yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: TXT_LEN}, [], "bad totalBytes, totalBytesLess combination"); - - // Check mime. - yield checkSearch({mime: "text/plain"}, ["txt1", "txt2"], "mime text/plain"); - yield checkSearch({mime: "text/html"}, ["html1", "html2"], "mime text/htmlplain"); - yield checkSearch({mime: "video/webm"}, [], "mime video/webm"); - - // Check fileSize. - yield checkSearch({fileSize: TXT_LEN}, ["txt1", "txt2"], "fileSize"); - yield checkSearch({fileSize: HTML_LEN}, ["html1", "html2"], "fileSize"); - - // Fields like bytesReceived, paused, state, exists are meaningful - // for downloads that are in progress but have not yet completed. - // todo: add tests for these when we have better support for in-progress - // downloads (e.g., after pause(), resume() and cancel() are implemented) - - // Check multiple query properties. - // We could make this testing arbitrarily complicated... - // We already tested combining fields with obvious interactions above - // (e.g., filename and filenameRegex or startTime and startedBefore/After) - // so now just throw as many fields as we can at a single search and - // make sure a simple case still works. - yield checkSearch({ - url: TXT_URL, - urlRegex: "download", - filename: downloadPath(TXT_FILE), - filenameRegex: "download", - query: ["download"], - startedAfter: time1.valueOf().toString(), - startedBefore: time2.valueOf().toString(), - totalBytes: TXT_LEN, - totalBytesGreater: 0, - totalBytesLess: BIG_LEN, - mime: "text/plain", - fileSize: TXT_LEN, - }, ["txt1"], "many properties"); - - // Check simple orderBy (forward and backward). - yield checkSearch({orderBy: ["startTime"]}, ["txt1", "txt2", "html1", "html2"], "orderBy startTime", true); - yield checkSearch({orderBy: ["-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy -startTime", true); - - // Check orderBy with multiple fields. - // NB: TXT_URL and HTML_URL differ only in extension and .html precedes .txt - yield checkSearch({orderBy: ["url", "-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy with multiple fields", true); - - // Check orderBy with limit. - yield checkSearch({orderBy: ["url"], limit: 1}, ["html1"], "orderBy with limit", true); - - // Check bad arguments. - function* checkBadSearch(query, pattern, description) { - let item = yield search(query); - equal(item.status, "error", "search() failed"); - ok(pattern.test(item.errmsg), `error message for ${description} was correct (${item.errmsg}).`); - } - - yield checkBadSearch("myquery", /Incorrect argument type/, "query is not an object"); - yield checkBadSearch({bogus: "boo"}, /Unexpected property/, "query contains an unknown field"); - yield checkBadSearch({query: "query string"}, /Expected array/, "query.query is a string"); - yield checkBadSearch({startedBefore: "i am not a time"}, /Type error/, "query.startedBefore is not a valid time"); - yield checkBadSearch({startedAfter: "i am not a time"}, /Type error/, "query.startedAfter is not a valid time"); - yield checkBadSearch({endedBefore: "i am not a time"}, /Type error/, "query.endedBefore is not a valid time"); - yield checkBadSearch({endedAfter: "i am not a time"}, /Type error/, "query.endedAfter is not a valid time"); - yield checkBadSearch({urlRegex: "["}, /Invalid urlRegex/, "query.urlRegexp is not a valid regular expression"); - yield checkBadSearch({filenameRegex: "["}, /Invalid filenameRegex/, "query.filenameRegexp is not a valid regular expression"); - yield checkBadSearch({orderBy: "startTime"}, /Expected array/, "query.orderBy is not an array"); - yield checkBadSearch({orderBy: ["bogus"]}, /Invalid orderBy field/, "query.orderBy references a non-existent field"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_experiments.js b/toolkit/components/webextensions/test/xpcshell/test_ext_experiments.js deleted file mode 100644 index bc6bfcd68..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_experiments.js +++ /dev/null @@ -1,175 +0,0 @@ -"use strict"; - -/* globals browser */ - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); - -function promiseAddonStartup() { - const {Management} = Cu.import("resource://gre/modules/Extension.jsm"); - - return new Promise(resolve => { - let listener = (evt, extension) => { - Management.off("startup", listener); - resolve(extension); - }; - - Management.on("startup", listener); - }); -} - -add_task(function* setup() { - yield ExtensionTestUtils.startAddonManager(); -}); - -add_task(function* test_experiments_api() { - let apiAddonFile = Extension.generateZipFile({ - "install.rdf": `<?xml version="1.0" encoding="UTF-8"?> - <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:em="http://www.mozilla.org/2004/em-rdf#"> - <Description about="urn:mozilla:install-manifest" - em:id="meh@experiments.addons.mozilla.org" - em:name="Meh Experiment" - em:type="256" - em:version="0.1" - em:description="Meh experiment" - em:creator="Mozilla"> - - <em:targetApplication> - <Description - em:id="xpcshell@tests.mozilla.org" - em:minVersion="48" - em:maxVersion="*"/> - </em:targetApplication> - </Description> - </RDF> - `, - - "api.js": String.raw` - Components.utils.import("resource://gre/modules/Services.jsm"); - - Services.obs.notifyObservers(null, "webext-api-loaded", ""); - - class API extends ExtensionAPI { - getAPI(context) { - return { - meh: { - hello(text) { - Services.obs.notifyObservers(null, "webext-api-hello", text); - } - } - } - } - } - `, - - "schema.json": [ - { - "namespace": "meh", - "description": "All full of meh.", - "permissions": ["experiments.meh"], - "functions": [ - { - "name": "hello", - "type": "function", - "description": "Hates you. This is all.", - "parameters": [ - {"type": "string", "name": "text"}, - ], - }, - ], - }, - ], - }); - - let addonFile = Extension.generateXPI({ - manifest: { - applications: {gecko: {id: "meh@web.extension"}}, - permissions: ["experiments.meh"], - }, - - background() { - // The test code below checks that hello() is called at the right - // time with the string "Here I am". Verify that the api schema is - // being correctly interpreted by calling hello() with bad arguments - // and only calling hello() with the magic string if the call with - // bad arguments throws. - try { - browser.meh.hello("I should not see this", "since two arguments are bad"); - } catch (err) { - browser.meh.hello("Here I am"); - } - }, - }); - - let boringAddonFile = Extension.generateXPI({ - manifest: { - applications: {gecko: {id: "boring@web.extension"}}, - }, - background() { - if (browser.meh) { - browser.meh.hello("Here I should not be"); - } - }, - }); - - do_register_cleanup(() => { - for (let file of [apiAddonFile, addonFile, boringAddonFile]) { - Services.obs.notifyObservers(file, "flush-cache-entry", null); - file.remove(false); - } - }); - - - let resolveHello; - let observer = (subject, topic, data) => { - if (topic == "webext-api-loaded") { - ok(!!resolveHello, "Should not see API loaded until dependent extension loads"); - } else if (topic == "webext-api-hello") { - resolveHello(data); - } - }; - - Services.obs.addObserver(observer, "webext-api-loaded", false); - Services.obs.addObserver(observer, "webext-api-hello", false); - do_register_cleanup(() => { - Services.obs.removeObserver(observer, "webext-api-loaded"); - Services.obs.removeObserver(observer, "webext-api-hello"); - }); - - - // Install API add-on. - let apiAddon = yield AddonManager.installTemporaryAddon(apiAddonFile); - - let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); - ok(APIs.apis.has("meh"), "Should have meh API."); - - - // Install boring WebExtension add-on. - let boringAddon = yield AddonManager.installTemporaryAddon(boringAddonFile); - yield promiseAddonStartup(); - - - // Install interesting WebExtension add-on. - let promise = new Promise(resolve => { - resolveHello = resolve; - }); - - let addon = yield AddonManager.installTemporaryAddon(addonFile); - yield promiseAddonStartup(); - - let hello = yield promise; - equal(hello, "Here I am", "Should get hello from add-on"); - - // Cleanup. - apiAddon.uninstall(); - - boringAddon.userDisabled = true; - yield new Promise(do_execute_soon); - - equal(addon.appDisabled, true, "Add-on should be app-disabled after its dependency is removed."); - - addon.uninstall(); - boringAddon.uninstall(); -}); - diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_extension.js b/toolkit/components/webextensions/test/xpcshell/test_ext_extension.js deleted file mode 100644 index f18845f6a..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_extension.js +++ /dev/null @@ -1,55 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_is_allowed_incognito_access() { - async function background() { - let allowed = await browser.extension.isAllowedIncognitoAccess(); - - browser.test.assertEq(true, allowed, "isAllowedIncognitoAccess is true"); - browser.test.notifyPass("isAllowedIncognitoAccess"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: {}, - }); - - yield extension.startup(); - yield extension.awaitFinish("isAllowedIncognitoAccess"); - yield extension.unload(); -}); - -add_task(function* test_in_incognito_context_false() { - function background() { - browser.test.assertEq(false, browser.extension.inIncognitoContext, "inIncognitoContext returned false"); - browser.test.notifyPass("inIncognitoContext"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: {}, - }); - - yield extension.startup(); - yield extension.awaitFinish("inIncognitoContext"); - yield extension.unload(); -}); - -add_task(function* test_is_allowed_file_scheme_access() { - async function background() { - let allowed = await browser.extension.isAllowedFileSchemeAccess(); - - browser.test.assertEq(false, allowed, "isAllowedFileSchemeAccess is false"); - browser.test.notifyPass("isAllowedFileSchemeAccess"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: {}, - }); - - yield extension.startup(); - yield extension.awaitFinish("isAllowedFileSchemeAccess"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_idle.js b/toolkit/components/webextensions/test/xpcshell/test_ext_idle.js deleted file mode 100644 index 89bcac217..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_idle.js +++ /dev/null @@ -1,202 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -Cu.import("resource://testing-common/MockRegistrar.jsm"); - -let idleService = { - _observers: new Set(), - _activity: { - addCalls: [], - removeCalls: [], - observerFires: [], - }, - _reset: function() { - this._observers.clear(); - this._activity.addCalls = []; - this._activity.removeCalls = []; - this._activity.observerFires = []; - }, - _fireObservers: function(state) { - for (let observer of this._observers.values()) { - observer.observe(observer, state, null); - this._activity.observerFires.push(state); - } - }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIIdleService]), - idleTime: 19999, - addIdleObserver: function(observer, time) { - this._observers.add(observer); - this._activity.addCalls.push(time); - }, - removeIdleObserver: function(observer, time) { - this._observers.delete(observer); - this._activity.removeCalls.push(time); - }, -}; - -function checkActivity(expectedActivity) { - let {expectedAdd, expectedRemove, expectedFires} = expectedActivity; - let {addCalls, removeCalls, observerFires} = idleService._activity; - equal(expectedAdd.length, addCalls.length, "idleService.addIdleObserver was called the expected number of times"); - equal(expectedRemove.length, removeCalls.length, "idleService.removeIdleObserver was called the expected number of times"); - equal(expectedFires.length, observerFires.length, "idle observer was fired the expected number of times"); - deepEqual(addCalls, expectedAdd, "expected interval passed to idleService.addIdleObserver"); - deepEqual(removeCalls, expectedRemove, "expected interval passed to idleService.removeIdleObserver"); - deepEqual(observerFires, expectedFires, "expected topic passed to idle observer"); -} - -add_task(function* setup() { - let fakeIdleService = MockRegistrar.register("@mozilla.org/widget/idleservice;1", idleService); - do_register_cleanup(() => { - MockRegistrar.unregister(fakeIdleService); - }); -}); - -add_task(function* testQueryStateActive() { - function background() { - browser.idle.queryState(20).then(status => { - browser.test.assertEq("active", status, "Idle status is active"); - browser.test.notifyPass("idle"); - }, - err => { - browser.test.fail(`Error: ${err} :: ${err.stack}`); - browser.test.notifyFail("idle"); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["idle"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("idle"); - yield extension.unload(); -}); - -add_task(function* testQueryStateIdle() { - function background() { - browser.idle.queryState(15).then(status => { - browser.test.assertEq("idle", status, "Idle status is idle"); - browser.test.notifyPass("idle"); - }, - err => { - browser.test.fail(`Error: ${err} :: ${err.stack}`); - browser.test.notifyFail("idle"); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["idle"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("idle"); - yield extension.unload(); -}); - -add_task(function* testOnlySetDetectionInterval() { - function background() { - browser.idle.setDetectionInterval(99); - browser.test.sendMessage("detectionIntervalSet"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["idle"], - }, - }); - - idleService._reset(); - yield extension.startup(); - yield extension.awaitMessage("detectionIntervalSet"); - idleService._fireObservers("idle"); - checkActivity({expectedAdd: [], expectedRemove: [], expectedFires: []}); - yield extension.unload(); -}); - -add_task(function* testSetDetectionIntervalBeforeAddingListener() { - function background() { - browser.idle.setDetectionInterval(99); - browser.idle.onStateChanged.addListener(newState => { - browser.test.assertEq("idle", newState, "listener fired with the expected state"); - browser.test.sendMessage("listenerFired"); - }); - browser.test.sendMessage("listenerAdded"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["idle"], - }, - }); - - idleService._reset(); - yield extension.startup(); - yield extension.awaitMessage("listenerAdded"); - idleService._fireObservers("idle"); - yield extension.awaitMessage("listenerFired"); - checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]}); - yield extension.unload(); -}); - -add_task(function* testSetDetectionIntervalAfterAddingListener() { - function background() { - browser.idle.onStateChanged.addListener(newState => { - browser.test.assertEq("idle", newState, "listener fired with the expected state"); - browser.test.sendMessage("listenerFired"); - }); - browser.idle.setDetectionInterval(99); - browser.test.sendMessage("detectionIntervalSet"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["idle"], - }, - }); - - idleService._reset(); - yield extension.startup(); - yield extension.awaitMessage("detectionIntervalSet"); - idleService._fireObservers("idle"); - yield extension.awaitMessage("listenerFired"); - checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]}); - yield extension.unload(); -}); - -add_task(function* testOnlyAddingListener() { - function background() { - browser.idle.onStateChanged.addListener(newState => { - browser.test.assertEq("active", newState, "listener fired with the expected state"); - browser.test.sendMessage("listenerFired"); - }); - browser.test.sendMessage("listenerAdded"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["idle"], - }, - }); - - idleService._reset(); - yield extension.startup(); - yield extension.awaitMessage("listenerAdded"); - idleService._fireObservers("active"); - yield extension.awaitMessage("listenerFired"); - // check that "idle-daily" topic does not cause a listener to fire - idleService._fireObservers("idle-daily"); - checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]}); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_json_parser.js b/toolkit/components/webextensions/test/xpcshell/test_ext_json_parser.js deleted file mode 100644 index 652f41315..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_json_parser.js +++ /dev/null @@ -1,37 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_json_parser() { - const ID = "json@test.web.extension"; - - let xpi = Extension.generateXPI({ - files: { - "manifest.json": String.raw`{ - // This is a manifest. - "applications": {"gecko": {"id": "${ID}"}}, - "name": "This \" is // not a comment", - "version": "0.1\\" // , "description": "This is not a description" - }`, - }, - }); - - let expectedManifest = { - "applications": {"gecko": {"id": ID}}, - "name": "This \" is // not a comment", - "version": "0.1\\", - }; - - let fileURI = Services.io.newFileURI(xpi); - let uri = NetUtil.newURI(`jar:${fileURI.spec}!/`); - - let extension = new ExtensionData(uri); - - yield extension.readManifest(); - - Assert.deepEqual(extension.rawManifest, expectedManifest, - "Manifest with correctly-filtered comments"); - - Services.obs.notifyObservers(xpi, "flush-cache-entry", null); - xpi.remove(false); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_legacy_extension_context.js b/toolkit/components/webextensions/test/xpcshell/test_ext_legacy_extension_context.js deleted file mode 100644 index 770851472..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_legacy_extension_context.js +++ /dev/null @@ -1,168 +0,0 @@ -"use strict"; - -/* globals browser */ - -Cu.import("resource://gre/modules/Extension.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -const {LegacyExtensionContext} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm"); - -/** - * This test case ensures that LegacyExtensionContext instances: - * - expose the expected API object and can join the messaging - * of a webextension given its addon id - * - the exposed API object can receive a port related to a `runtime.connect` - * Port created in the webextension's background page - * - the received Port instance can exchange messages with the background page - * - the received Port receive a disconnect event when the webextension is - * shutting down - */ -add_task(function* test_legacy_extension_context() { - function background() { - let bgURL = window.location.href; - - let extensionInfo = { - bgURL, - // Extract the assigned uuid from the background page url. - uuid: window.location.hostname, - }; - - browser.test.sendMessage("webextension-ready", extensionInfo); - - let port; - - browser.test.onMessage.addListener(async msg => { - if (msg == "do-send-message") { - let reply = await browser.runtime.sendMessage("webextension -> legacy_extension message"); - - browser.test.assertEq("legacy_extension -> webextension reply", reply, - "Got the expected message from the LegacyExtensionContext"); - browser.test.sendMessage("got-reply-message"); - } else if (msg == "do-connect") { - port = browser.runtime.connect(); - - port.onMessage.addListener(portMsg => { - browser.test.assertEq("legacy_extension -> webextension port message", portMsg, - "Got the expected message from the LegacyExtensionContext"); - port.postMessage("webextension -> legacy_extension port message"); - }); - } else if (msg == "do-disconnect") { - port.disconnect(); - } - }); - } - - let extensionData = { - background, - }; - - let extension = Extension.generate(extensionData); - - let waitForExtensionInfo = new Promise((resolve, reject) => { - extension.on("test-message", function testMessageListener(kind, msg, ...args) { - if (msg != "webextension-ready") { - reject(new Error(`Got an unexpected test-message: ${msg}`)); - } else { - extension.off("test-message", testMessageListener); - resolve(args[0]); - } - }); - }); - - // Connect to the target extension as an external context - // using the given custom sender info. - let legacyContext; - - extension.on("startup", function onStartup() { - extension.off("startup", onStartup); - legacyContext = new LegacyExtensionContext(extension); - extension.callOnClose({ - close: () => legacyContext.unload(), - }); - }); - - yield extension.startup(); - - let extensionInfo = yield waitForExtensionInfo; - - equal(legacyContext.envType, "legacy_extension", - "LegacyExtensionContext instance has the expected type"); - - ok(legacyContext.api, "Got the expected API object"); - ok(legacyContext.api.browser, "Got the expected browser property"); - - let waitMessage = new Promise(resolve => { - const {browser} = legacyContext.api; - browser.runtime.onMessage.addListener((singleMsg, msgSender) => { - resolve({singleMsg, msgSender}); - - // Send a reply to the sender. - return Promise.resolve("legacy_extension -> webextension reply"); - }); - }); - - extension.testMessage("do-send-message"); - - let {singleMsg, msgSender} = yield waitMessage; - equal(singleMsg, "webextension -> legacy_extension message", - "Got the expected message"); - ok(msgSender, "Got a message sender object"); - - equal(msgSender.id, extension.id, "The sender has the expected id property"); - equal(msgSender.url, extensionInfo.bgURL, "The sender has the expected url property"); - - // Wait confirmation that the reply has been received. - yield new Promise((resolve, reject) => { - extension.on("test-message", function testMessageListener(kind, msg, ...args) { - if (msg != "got-reply-message") { - reject(new Error(`Got an unexpected test-message: ${msg}`)); - } else { - extension.off("test-message", testMessageListener); - resolve(); - } - }); - }); - - let waitConnectPort = new Promise(resolve => { - let {browser} = legacyContext.api; - browser.runtime.onConnect.addListener(port => { - resolve(port); - }); - }); - - extension.testMessage("do-connect"); - - let port = yield waitConnectPort; - - ok(port, "Got the Port API object"); - ok(port.sender, "The port has a sender property"); - equal(port.sender.id, extension.id, - "The port sender has the expected id property"); - equal(port.sender.url, extensionInfo.bgURL, - "The port sender has the expected url property"); - - let waitPortMessage = new Promise(resolve => { - port.onMessage.addListener((msg) => { - resolve(msg); - }); - }); - - port.postMessage("legacy_extension -> webextension port message"); - - let msg = yield waitPortMessage; - - equal(msg, "webextension -> legacy_extension port message", - "LegacyExtensionContext received the expected message from the webextension"); - - let waitForDisconnect = new Promise(resolve => { - port.onDisconnect.addListener(resolve); - }); - - extension.testMessage("do-disconnect"); - - yield waitForDisconnect; - - do_print("Got the disconnect event on unload"); - - yield extension.shutdown(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_legacy_extension_embedding.js b/toolkit/components/webextensions/test/xpcshell/test_ext_legacy_extension_embedding.js deleted file mode 100644 index ea5d78524..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_legacy_extension_embedding.js +++ /dev/null @@ -1,188 +0,0 @@ -"use strict"; - -/* globals browser */ - -Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm"); - -// Import EmbeddedExtensionManager to be able to check that the -// tacked instances are cleared after the embedded extension shutdown. -const { - EmbeddedExtensionManager, -} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {}); - -/** - * This test case ensures that the LegacyExtensionsUtils.EmbeddedExtension: - * - load the embedded webextension resources from a "/webextension/" dir - * inside the XPI. - * - EmbeddedExtension.prototype.api returns an API object which exposes - * a working `runtime.onConnect` event object (e.g. the API can receive a port - * when the embedded webextension is started and it can exchange messages - * with the background page). - * - EmbeddedExtension.prototype.startup/shutdown methods manage the embedded - * webextension lifecycle as expected. - */ -add_task(function* test_embedded_webextension_utils() { - function backgroundScript() { - let port = browser.runtime.connect(); - - port.onMessage.addListener((msg) => { - if (msg == "legacy_extension -> webextension") { - port.postMessage("webextension -> legacy_extension"); - port.disconnect(); - } - }); - } - - const id = "@test.embedded.web.extension"; - - // Extensions.generateXPI is used here (and in the other hybrid addons tests in this same - // test dir) to be able to generate an xpi with the directory layout that we expect from - // an hybrid legacy+webextension addon (where all the embedded webextension resources are - // loaded from a 'webextension/' directory). - let fakeHybridAddonFile = Extension.generateZipFile({ - "webextension/manifest.json": { - applications: {gecko: {id}}, - name: "embedded webextension name", - manifest_version: 2, - version: "1.0", - background: { - scripts: ["bg.js"], - }, - }, - "webextension/bg.js": `new ${backgroundScript}`, - }); - - // Remove the generated xpi file and flush the its jar cache - // on cleanup. - do_register_cleanup(() => { - Services.obs.notifyObservers(fakeHybridAddonFile, "flush-cache-entry", null); - fakeHybridAddonFile.remove(false); - }); - - let fileURI = Services.io.newFileURI(fakeHybridAddonFile); - let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null); - - let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({ - id, resourceURI, - }); - - ok(embeddedExtension, "Got the embeddedExtension object"); - - equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1, - "Got the expected number of tracked embedded extension instances"); - - do_print("waiting embeddedExtension.startup"); - let embeddedExtensionAPI = yield embeddedExtension.startup(); - ok(embeddedExtensionAPI, "Got the embeddedExtensionAPI object"); - - let waitConnectPort = new Promise(resolve => { - let {browser} = embeddedExtensionAPI; - browser.runtime.onConnect.addListener(port => { - resolve(port); - }); - }); - - let port = yield waitConnectPort; - - ok(port, "Got the Port API object"); - - let waitPortMessage = new Promise(resolve => { - port.onMessage.addListener((msg) => { - resolve(msg); - }); - }); - - port.postMessage("legacy_extension -> webextension"); - - let msg = yield waitPortMessage; - - equal(msg, "webextension -> legacy_extension", - "LegacyExtensionContext received the expected message from the webextension"); - - let waitForDisconnect = new Promise(resolve => { - port.onDisconnect.addListener(resolve); - }); - - do_print("Wait for the disconnect port event"); - yield waitForDisconnect; - do_print("Got the disconnect port event"); - - yield embeddedExtension.shutdown(); - - equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0, - "EmbeddedExtension instances has been untracked from the EmbeddedExtensionManager"); -}); - -function* createManifestErrorTestCase(id, xpi, expectedError) { - // Remove the generated xpi file and flush the its jar cache - // on cleanup. - do_register_cleanup(() => { - Services.obs.notifyObservers(xpi, "flush-cache-entry", null); - xpi.remove(false); - }); - - let fileURI = Services.io.newFileURI(xpi); - let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null); - - let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({ - id, resourceURI, - }); - - yield Assert.rejects(embeddedExtension.startup(), expectedError, - "embedded extension startup rejected"); - - // Shutdown a "never-started" addon with an embedded webextension should not - // raise any exception, and if it does this test will fail. - yield embeddedExtension.shutdown(); -} - -add_task(function* test_startup_error_empty_manifest() { - const id = "empty-manifest@test.embedded.web.extension"; - const files = { - "webextension/manifest.json": ``, - }; - const expectedError = "(NS_BASE_STREAM_CLOSED)"; - - let fakeHybridAddonFile = Extension.generateZipFile(files); - - yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError); -}); - -add_task(function* test_startup_error_invalid_json_manifest() { - const id = "invalid-json-manifest@test.embedded.web.extension"; - const files = { - "webextension/manifest.json": `{ "name": }`, - }; - const expectedError = "JSON.parse:"; - - let fakeHybridAddonFile = Extension.generateZipFile(files); - - yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError); -}); - -add_task(function* test_startup_error_blocking_validation_errors() { - const id = "blocking-manifest-validation-error@test.embedded.web.extension"; - const files = { - "webextension/manifest.json": { - name: "embedded webextension name", - manifest_version: 2, - version: "1.0", - background: { - scripts: {}, - }, - }, - }; - - function expectedError(actual) { - if (actual.errors && actual.errors.length == 1 && - actual.errors[0].startsWith("Reading manifest:")) { - return true; - } - - return false; - } - - let fakeHybridAddonFile = Extension.generateZipFile(files); - - yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_localStorage.js b/toolkit/components/webextensions/test/xpcshell/test_ext_localStorage.js deleted file mode 100644 index 0f0b41085..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_localStorage.js +++ /dev/null @@ -1,50 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -function backgroundScript() { - let hasRun = localStorage.getItem("has-run"); - let result; - if (!hasRun) { - localStorage.setItem("has-run", "yup"); - localStorage.setItem("test-item", "item1"); - result = "item1"; - } else { - let data = localStorage.getItem("test-item"); - if (data == "item1") { - localStorage.setItem("test-item", "item2"); - result = "item2"; - } else if (data == "item2") { - localStorage.removeItem("test-item"); - result = "deleted"; - } else if (!data) { - localStorage.clear(); - result = "cleared"; - } - } - browser.test.sendMessage("result", result); - browser.test.notifyPass("localStorage"); -} - -const ID = "test-webextension@mozilla.com"; -let extensionData = { - manifest: {applications: {gecko: {id: ID}}}, - background: backgroundScript, -}; - -add_task(function* test_localStorage() { - const RESULTS = ["item1", "item2", "deleted", "cleared", "item1"]; - - for (let expected of RESULTS) { - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - - let actual = yield extension.awaitMessage("result"); - - yield extension.awaitFinish("localStorage"); - yield extension.unload(); - - equal(actual, expected, "got expected localStorage data"); - } -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_management.js b/toolkit/components/webextensions/test/xpcshell/test_ext_management.js deleted file mode 100644 index b19554a57..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_management.js +++ /dev/null @@ -1,20 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_management_schema() { - function background() { - browser.test.assertTrue(browser.management, "browser.management API exists"); - browser.test.notifyPass("management-schema"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["management"], - }, - background: `(${background})()`, - }); - yield extension.startup(); - yield extension.awaitFinish("management-schema"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_management_uninstall_self.js b/toolkit/components/webextensions/test/xpcshell/test_ext_management_uninstall_self.js deleted file mode 100644 index 7d80a9c23..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_management_uninstall_self.js +++ /dev/null @@ -1,135 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -Cu.import("resource://gre/modules/AddonManager.jsm"); -Cu.import("resource://testing-common/AddonTestUtils.jsm"); -Cu.import("resource://testing-common/MockRegistrar.jsm"); - -const {promiseAddonByID} = AddonTestUtils; -const id = "uninstall_self_test@tests.mozilla.com"; - -const manifest = { - applications: { - gecko: { - id, - }, - }, - name: "test extension name", - version: "1.0", -}; - -const waitForUninstalled = () => new Promise(resolve => { - const listener = { - onUninstalled: (addon) => { - equal(addon.id, id, "The expected add-on has been uninstalled"); - AddonManager.getAddonByID(addon.id, checkedAddon => { - equal(checkedAddon, null, "Add-on no longer exists"); - AddonManager.removeAddonListener(listener); - resolve(); - }); - }, - }; - AddonManager.addAddonListener(listener); -}); - -let promptService = { - _response: null, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]), - confirmEx: function(...args) { - this._confirmExArgs = args; - return this._response; - }, -}; - -add_task(function* setup() { - let fakePromptService = MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService); - do_register_cleanup(() => { - MockRegistrar.unregister(fakePromptService); - }); - yield ExtensionTestUtils.startAddonManager(); -}); - -add_task(function* test_management_uninstall_no_prompt() { - function background() { - browser.test.onMessage.addListener(msg => { - browser.management.uninstallSelf(); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest, - background, - useAddonManager: "temporary", - }); - - yield extension.startup(); - let addon = yield promiseAddonByID(id); - notEqual(addon, null, "Add-on is installed"); - extension.sendMessage("uninstall"); - yield waitForUninstalled(); - yield extension.markUnloaded(); - Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null); -}); - -add_task(function* test_management_uninstall_prompt_uninstall() { - promptService._response = 0; - - function background() { - browser.test.onMessage.addListener(msg => { - browser.management.uninstallSelf({showConfirmDialog: true}); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest, - background, - useAddonManager: "temporary", - }); - - yield extension.startup(); - let addon = yield promiseAddonByID(id); - notEqual(addon, null, "Add-on is installed"); - extension.sendMessage("uninstall"); - yield waitForUninstalled(); - yield extension.markUnloaded(); - - // Test localization strings - equal(promptService._confirmExArgs[1], `Uninstall ${manifest.name}`); - equal(promptService._confirmExArgs[2], - `The extension “${manifest.name}†is requesting to be uninstalled. What would you like to do?`); - equal(promptService._confirmExArgs[4], "Uninstall"); - equal(promptService._confirmExArgs[5], "Keep Installed"); - Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null); -}); - -add_task(function* test_management_uninstall_prompt_keep() { - promptService._response = 1; - - function background() { - browser.test.onMessage.addListener(async msg => { - await browser.test.assertRejects( - browser.management.uninstallSelf({showConfirmDialog: true}), - "User cancelled uninstall of extension", - "Expected rejection when user declines uninstall"); - - browser.test.sendMessage("uninstall-rejected"); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest, - background, - useAddonManager: "temporary", - }); - - yield extension.startup(); - let addon = yield promiseAddonByID(id); - notEqual(addon, null, "Add-on is installed"); - extension.sendMessage("uninstall"); - yield extension.awaitMessage("uninstall-rejected"); - addon = yield promiseAddonByID(id); - notEqual(addon, null, "Add-on remains installed"); - yield extension.unload(); - Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_content_security_policy.js b/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_content_security_policy.js deleted file mode 100644 index 2b0084980..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_content_security_policy.js +++ /dev/null @@ -1,30 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - - -add_task(function* test_manifest_csp() { - let normalized = yield ExtensionTestUtils.normalizeManifest({ - "content_security_policy": "script-src 'self'; object-src 'none'", - }); - - equal(normalized.error, undefined, "Should not have an error"); - equal(normalized.errors.length, 0, "Should not have warnings"); - equal(normalized.value.content_security_policy, - "script-src 'self'; object-src 'none'", - "Should have the expected poilcy string"); - - - normalized = yield ExtensionTestUtils.normalizeManifest({ - "content_security_policy": "object-src 'none'", - }); - - equal(normalized.error, undefined, "Should not have an error"); - - Assert.deepEqual(normalized.errors, - ["Error processing content_security_policy: SyntaxError: Policy is missing a required \u2018script-src\u2019 directive"], - "Should have the expected warning"); - - equal(normalized.value.content_security_policy, null, - "Invalid policy string should be omitted"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_incognito.js b/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_incognito.js deleted file mode 100644 index 94649692e..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_incognito.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - - -add_task(function* test_manifest_incognito() { - let normalized = yield ExtensionTestUtils.normalizeManifest({ - "incognito": "spanning", - }); - - equal(normalized.error, undefined, "Should not have an error"); - equal(normalized.errors.length, 0, "Should not have warnings"); - equal(normalized.value.incognito, - "spanning", - "Should have the expected incognito string"); - - normalized = yield ExtensionTestUtils.normalizeManifest({ - "incognito": "split", - }); - - equal(normalized.error, undefined, "Should not have an error"); - Assert.deepEqual(normalized.errors, - ['Error processing incognito: Invalid enumeration value "split"'], - "Should have the expected warning"); - equal(normalized.value.incognito, null, - "Invalid incognito string should be omitted"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js b/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js deleted file mode 100644 index fad5661bb..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js +++ /dev/null @@ -1,13 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - - -add_task(function* test_manifest_minimum_chrome_version() { - let normalized = yield ExtensionTestUtils.normalizeManifest({ - "minimum_chrome_version": "42", - }); - - equal(normalized.error, undefined, "Should not have an error"); - equal(normalized.errors.length, 0, "Should not have warnings"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js b/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js deleted file mode 100644 index 5a6b628f5..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js +++ /dev/null @@ -1,514 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -/* globals chrome */ - -const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes"; -const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes"; - -const ECHO_BODY = String.raw` - import struct - import sys - - while True: - rawlen = sys.stdin.read(4) - if len(rawlen) == 0: - sys.exit(0) - msglen = struct.unpack('@I', rawlen)[0] - msg = sys.stdin.read(msglen) - - sys.stdout.write(struct.pack('@I', msglen)) - sys.stdout.write(msg) -`; - -const INFO_BODY = String.raw` - import json - import os - import struct - import sys - - msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()}) - sys.stdout.write(struct.pack('@I', len(msg))) - sys.stdout.write(msg) - sys.exit(0) -`; - -const STDERR_LINES = ["hello stderr", "this should be a separate line"]; -let STDERR_MSG = STDERR_LINES.join("\\n"); - -const STDERR_BODY = String.raw` - import sys - sys.stderr.write("${STDERR_MSG}") -`; - -const SCRIPTS = [ - { - name: "echo", - description: "a native app that echoes back messages it receives", - script: ECHO_BODY.replace(/^ {2}/gm, ""), - }, - { - name: "info", - description: "a native app that gives some info about how it was started", - script: INFO_BODY.replace(/^ {2}/gm, ""), - }, - { - name: "stderr", - description: "a native app that writes to stderr and then exits", - script: STDERR_BODY.replace(/^ {2}/gm, ""), - }, -]; - -add_task(function* setup() { - yield setupHosts(SCRIPTS); -}); - -// Test the basic operation of native messaging with a simple -// script that echoes back whatever message is sent to it. -add_task(function* test_happy_path() { - function background() { - let port = browser.runtime.connectNative("echo"); - port.onMessage.addListener(msg => { - browser.test.sendMessage("message", msg); - }); - browser.test.onMessage.addListener((what, payload) => { - if (what == "send") { - if (payload._json) { - let json = payload._json; - payload.toJSON = () => json; - delete payload._json; - } - port.postMessage(payload); - } - }); - browser.test.sendMessage("ready"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - const tests = [ - { - data: "this is a string", - what: "simple string", - }, - { - data: "Ðто юникода", - what: "unicode string", - }, - { - data: {test: "hello"}, - what: "simple object", - }, - { - data: { - what: "An object with a few properties", - number: 123, - bool: true, - nested: {what: "another object"}, - }, - what: "object with several properties", - }, - - { - data: { - ignoreme: true, - _json: {data: "i have a tojson method"}, - }, - expected: {data: "i have a tojson method"}, - what: "object with toJSON() method", - }, - ]; - for (let test of tests) { - extension.sendMessage("send", test.data); - let response = yield extension.awaitMessage("message"); - let expected = test.expected || test.data; - deepEqual(response, expected, `Echoed a message of type ${test.what}`); - } - - let procCount = yield getSubprocessCount(); - equal(procCount, 1, "subprocess is still running"); - let exitPromise = waitForSubprocessExit(); - yield extension.unload(); - yield exitPromise; -}); - -if (AppConstants.platform == "win") { - // "relative.echo" has a relative path in the host manifest. - add_task(function* test_relative_path() { - function background() { - let port = browser.runtime.connectNative("relative.echo"); - let MSG = "test relative echo path"; - port.onMessage.addListener(msg => { - browser.test.assertEq(MSG, msg, "Got expected message back"); - browser.test.sendMessage("done"); - }); - port.postMessage(MSG); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("done"); - - let procCount = yield getSubprocessCount(); - equal(procCount, 1, "subprocess is still running"); - let exitPromise = waitForSubprocessExit(); - yield extension.unload(); - yield exitPromise; - }); -} - -// Test sendNativeMessage() -add_task(function* test_sendNativeMessage() { - async function background() { - let MSG = {test: "hello world"}; - - // Check error handling - await browser.test.assertRejects( - browser.runtime.sendNativeMessage("nonexistent", MSG), - /Attempt to postMessage on disconnected port/, - "sendNativeMessage() to a nonexistent app failed"); - - // Check regular message exchange - let reply = await browser.runtime.sendNativeMessage("echo", MSG); - - let expected = JSON.stringify(MSG); - let received = JSON.stringify(reply); - browser.test.assertEq(expected, received, "Received echoed native message"); - - browser.test.sendMessage("finished"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("finished"); - - // With sendNativeMessage(), the subprocess should be disconnected - // after exchanging a single message. - yield waitForSubprocessExit(); - - yield extension.unload(); -}); - -// Test calling Port.disconnect() -add_task(function* test_disconnect() { - function background() { - let port = browser.runtime.connectNative("echo"); - port.onMessage.addListener((msg, msgPort) => { - browser.test.assertEq(port, msgPort, "onMessage handler should receive the port as the second argument"); - browser.test.sendMessage("message", msg); - }); - port.onDisconnect.addListener(msgPort => { - browser.test.fail("onDisconnect should not be called for disconnect()"); - }); - browser.test.onMessage.addListener((what, payload) => { - if (what == "send") { - if (payload._json) { - let json = payload._json; - payload.toJSON = () => json; - delete payload._json; - } - port.postMessage(payload); - } else if (what == "disconnect") { - try { - port.disconnect(); - browser.test.sendMessage("disconnect-result", {success: true}); - } catch (err) { - browser.test.sendMessage("disconnect-result", { - success: false, - errmsg: err.message, - }); - } - } - }); - browser.test.sendMessage("ready"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - extension.sendMessage("send", "test"); - let response = yield extension.awaitMessage("message"); - equal(response, "test", "Echoed a string"); - - let procCount = yield getSubprocessCount(); - equal(procCount, 1, "subprocess is running"); - - extension.sendMessage("disconnect"); - response = yield extension.awaitMessage("disconnect-result"); - equal(response.success, true, "disconnect succeeded"); - - do_print("waiting for subprocess to exit"); - yield waitForSubprocessExit(); - procCount = yield getSubprocessCount(); - equal(procCount, 0, "subprocess is no longer running"); - - extension.sendMessage("disconnect"); - response = yield extension.awaitMessage("disconnect-result"); - equal(response.success, true, "second call to disconnect silently ignored"); - - yield extension.unload(); -}); - -// Test the limit on message size for writing -add_task(function* test_write_limit() { - Services.prefs.setIntPref(PREF_MAX_WRITE, 10); - function clearPref() { - Services.prefs.clearUserPref(PREF_MAX_WRITE); - } - do_register_cleanup(clearPref); - - function background() { - const PAYLOAD = "0123456789A"; - let port = browser.runtime.connectNative("echo"); - try { - port.postMessage(PAYLOAD); - browser.test.sendMessage("result", null); - } catch (ex) { - browser.test.sendMessage("result", ex.message); - } - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - - let errmsg = yield extension.awaitMessage("result"); - notEqual(errmsg, null, "native postMessage() failed for overly large message"); - - yield extension.unload(); - yield waitForSubprocessExit(); - - clearPref(); -}); - -// Test the limit on message size for reading -add_task(function* test_read_limit() { - Services.prefs.setIntPref(PREF_MAX_READ, 10); - function clearPref() { - Services.prefs.clearUserPref(PREF_MAX_READ); - } - do_register_cleanup(clearPref); - - function background() { - const PAYLOAD = "0123456789A"; - let port = browser.runtime.connectNative("echo"); - port.onDisconnect.addListener(msgPort => { - browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); - browser.test.assertEq("Native application tried to send a message of 13 bytes, which exceeds the limit of 10 bytes.", port.error && port.error.message); - browser.test.sendMessage("result", "disconnected"); - }); - port.onMessage.addListener(msg => { - browser.test.sendMessage("result", "message"); - }); - port.postMessage(PAYLOAD); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - - let result = yield extension.awaitMessage("result"); - equal(result, "disconnected", "native port disconnected on receiving large message"); - - yield extension.unload(); - yield waitForSubprocessExit(); - - clearPref(); -}); - -// Test that an extension without the nativeMessaging permission cannot -// use native messaging. -add_task(function* test_ext_permission() { - function background() { - browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission"); - browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission"); - browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission"); - browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission"); - browser.test.sendMessage("finished"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: {}, - }); - - yield extension.startup(); - yield extension.awaitMessage("finished"); - yield extension.unload(); -}); - -// Test that an extension that is not listed in allowed_extensions for -// a native application cannot use that application. -add_task(function* test_app_permission() { - function background() { - let port = browser.runtime.connectNative("echo"); - port.onDisconnect.addListener(msgPort => { - browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); - browser.test.assertEq("This extension does not have permission to use native application echo (or the application is not installed)", port.error && port.error.message); - browser.test.sendMessage("result", "disconnected"); - }); - port.onMessage.addListener(msg => { - browser.test.sendMessage("result", "message"); - }); - port.postMessage({test: "test"}); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - permissions: ["nativeMessaging"], - }, - }, "somethingelse@tests.mozilla.org"); - - yield extension.startup(); - - let result = yield extension.awaitMessage("result"); - equal(result, "disconnected", "connectNative() failed without native app permission"); - - yield extension.unload(); - - let procCount = yield getSubprocessCount(); - equal(procCount, 0, "No child process was started"); -}); - -// Test that the command-line arguments and working directory for the -// native application are as expected. -add_task(function* test_child_process() { - function background() { - let port = browser.runtime.connectNative("info"); - port.onMessage.addListener(msg => { - browser.test.sendMessage("result", msg); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - - let msg = yield extension.awaitMessage("result"); - equal(msg.args.length, 2, "Received one command line argument"); - equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest"); - equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path, - "Working directory is the directory containing the native appliation"); - - let exitPromise = waitForSubprocessExit(); - yield extension.unload(); - yield exitPromise; -}); - -add_task(function* test_stderr() { - function background() { - let port = browser.runtime.connectNative("stderr"); - port.onDisconnect.addListener(msgPort => { - browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); - browser.test.assertEq(null, port.error, "Normal application exit is not an error"); - browser.test.sendMessage("finished"); - }); - } - - let {messages} = yield promiseConsoleOutput(function* () { - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("finished"); - yield extension.unload(); - - yield waitForSubprocessExit(); - }); - - let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.message.includes(line))); - notEqual(lines[0], -1, "Saw first line of stderr output on the console"); - notEqual(lines[1], -1, "Saw second line of stderr output on the console"); - notEqual(lines[0], lines[1], "Stderr output lines are separated in the console"); -}); - -// Test that calling connectNative() multiple times works -// (bug 1313980 was a previous regression in this area) -add_task(function* test_multiple_connects() { - async function background() { - function once() { - return new Promise(resolve => { - let MSG = "hello"; - let port = browser.runtime.connectNative("echo"); - - port.onMessage.addListener(msg => { - browser.test.assertEq(MSG, msg, "Got expected message back"); - port.disconnect(); - resolve(); - }); - port.postMessage(MSG); - }); - } - - await once(); - await once(); - browser.test.notifyPass("multiple-connect"); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("multiple-connect"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging_perf.js b/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging_perf.js deleted file mode 100644 index 693f67dde..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging_perf.js +++ /dev/null @@ -1,128 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry", - "resource://testing-common/MockRegistry.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); - -Cu.import("resource://gre/modules/Subprocess.jsm"); - -const MAX_ROUND_TRIP_TIME_MS = AppConstants.DEBUG || AppConstants.ASAN ? 36 : 18; -const MAX_RETRIES = 5; - - -const ECHO_BODY = String.raw` - import struct - import sys - - while True: - rawlen = sys.stdin.read(4) - if len(rawlen) == 0: - sys.exit(0) - - msglen = struct.unpack('@I', rawlen)[0] - msg = sys.stdin.read(msglen) - - sys.stdout.write(struct.pack('@I', msglen)) - sys.stdout.write(msg) -`; - -const SCRIPTS = [ - { - name: "echo", - description: "A native app that echoes back messages it receives", - script: ECHO_BODY.replace(/^ {2}/gm, ""), - }, -]; - -add_task(function* setup() { - yield setupHosts(SCRIPTS); -}); - -add_task(function* test_round_trip_perf() { - let extension = ExtensionTestUtils.loadExtension({ - background() { - browser.test.onMessage.addListener(msg => { - if (msg != "run-tests") { - return; - } - - let port = browser.runtime.connectNative("echo"); - - function next() { - port.postMessage({ - "Lorem": { - "ipsum": { - "dolor": [ - "sit amet", - "consectetur adipiscing elit", - "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - ], - "Ut enim": [ - "ad minim veniam", - "quis nostrud exercitation ullamco", - "laboris nisi ut aliquip ex ea commodo consequat.", - ], - "Duis": [ - "aute irure dolor in reprehenderit in", - "voluptate velit esse cillum dolore eu", - "fugiat nulla pariatur.", - ], - "Excepteur": [ - "sint occaecat cupidatat non proident", - "sunt in culpa qui officia deserunt", - "mollit anim id est laborum.", - ], - }, - }, - }); - } - - const COUNT = 1000; - let now; - function finish() { - let roundTripTime = (Date.now() - now) / COUNT; - - port.disconnect(); - browser.test.sendMessage("result", roundTripTime); - } - - let count = 0; - port.onMessage.addListener(() => { - if (count == 0) { - // Skip the first round, since it includes the time it takes - // the app to start up. - now = Date.now(); - } - - if (count++ <= COUNT) { - next(); - } else { - finish(); - } - }); - - next(); - }); - }, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - - let roundTripTime = Infinity; - for (let i = 0; i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS; i++) { - extension.sendMessage("run-tests"); - roundTripTime = yield extension.awaitMessage("result"); - } - - yield extension.unload(); - - ok(roundTripTime <= MAX_ROUND_TRIP_TIME_MS, - `Expected round trip time (${roundTripTime}ms) to be less than ${MAX_ROUND_TRIP_TIME_MS}ms`); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging_unresponsive.js b/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging_unresponsive.js deleted file mode 100644 index a75a1d49d..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging_unresponsive.js +++ /dev/null @@ -1,82 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -const WONTDIE_BODY = String.raw` - import signal - import struct - import sys - import time - - signal.signal(signal.SIGTERM, signal.SIG_IGN) - - def spin(): - while True: - try: - signal.pause() - except AttributeError: - time.sleep(5) - - while True: - rawlen = sys.stdin.read(4) - if len(rawlen) == 0: - spin() - - msglen = struct.unpack('@I', rawlen)[0] - msg = sys.stdin.read(msglen) - - sys.stdout.write(struct.pack('@I', msglen)) - sys.stdout.write(msg) -`; - -const SCRIPTS = [ - { - name: "wontdie", - description: "a native app that does not exit when stdin closes or on SIGTERM", - script: WONTDIE_BODY.replace(/^ {2}/gm, ""), - }, -]; - -add_task(function* setup() { - yield setupHosts(SCRIPTS); -}); - - -// Test that an unresponsive native application still gets killed eventually -add_task(function* test_unresponsive_native_app() { - // XXX expose GRACEFUL_SHUTDOWN_TIME as a pref and reduce it - // just for this test? - - function background() { - let port = browser.runtime.connectNative("wontdie"); - - const MSG = "echo me"; - // bounce a message to make sure the process actually starts - port.onMessage.addListener(msg => { - browser.test.assertEq(msg, MSG, "Received echoed message"); - browser.test.sendMessage("ready"); - }); - port.postMessage(MSG); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - manifest: { - applications: {gecko: {id: ID}}, - permissions: ["nativeMessaging"], - }, - }); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - let procCount = yield getSubprocessCount(); - equal(procCount, 1, "subprocess is running"); - - let exitPromise = waitForSubprocessExit(); - yield extension.unload(); - yield exitPromise; - - procCount = yield getSubprocessCount(); - equal(procCount, 0, "subprocess was succesfully killed"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_onmessage_removelistener.js b/toolkit/components/webextensions/test/xpcshell/test_ext_onmessage_removelistener.js deleted file mode 100644 index 6f8b553fc..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_onmessage_removelistener.js +++ /dev/null @@ -1,30 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -function backgroundScript() { - function listener() { - browser.test.notifyFail("listener should not be invoked"); - } - - browser.runtime.onMessage.addListener(listener); - browser.runtime.onMessage.removeListener(listener); - browser.runtime.sendMessage("hello"); - - // Make sure that, if we somehow fail to remove the listener, then we'll run - // the listener before the test is marked as passing. - setTimeout(function() { - browser.test.notifyPass("onmessage_removelistener"); - }, 0); -} - -let extensionData = { - background: backgroundScript, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitFinish("onmessage_removelistener"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js deleted file mode 100644 index 2a1342cde..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js +++ /dev/null @@ -1,23 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_connect_without_listener() { - function background() { - let port = browser.runtime.connect(); - port.onDisconnect.addListener(() => { - browser.test.assertEq("Could not establish connection. Receiving end does not exist.", port.error && port.error.message); - browser.test.notifyPass("port.onDisconnect was called"); - }); - } - let extensionData = { - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitFinish("port.onDisconnect was called"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js deleted file mode 100644 index a280206fa..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -add_task(function* setup() { - ExtensionTestUtils.mockAppInfo(); -}); - -add_task(function* test_getBrowserInfo() { - async function background() { - let info = await browser.runtime.getBrowserInfo(); - - browser.test.assertEq(info.name, "XPCShell", "name is valid"); - browser.test.assertEq(info.vendor, "Mozilla", "vendor is Mozilla"); - browser.test.assertEq(info.version, "48", "version is correct"); - browser.test.assertEq(info.buildID, "20160315", "buildID is correct"); - - browser.test.notifyPass("runtime.getBrowserInfo"); - } - - const extension = ExtensionTestUtils.loadExtension({background}); - yield extension.startup(); - yield extension.awaitFinish("runtime.getBrowserInfo"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js deleted file mode 100644 index 29bad0c10..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js +++ /dev/null @@ -1,25 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -function backgroundScript() { - browser.runtime.getPlatformInfo(info => { - let validOSs = ["mac", "win", "android", "cros", "linux", "openbsd"]; - let validArchs = ["arm", "x86-32", "x86-64"]; - - browser.test.assertTrue(validOSs.indexOf(info.os) != -1, "OS is valid"); - browser.test.assertTrue(validArchs.indexOf(info.arch) != -1, "Architecture is valid"); - browser.test.notifyPass("runtime.getPlatformInfo"); - }); -} - -let extensionData = { - background: backgroundScript, -}; - -add_task(function* test_contentscript() { - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitFinish("runtime.getPlatformInfo"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js deleted file mode 100644 index fa6461412..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js +++ /dev/null @@ -1,337 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -XPCOMUtils.defineLazyGetter(this, "Management", () => { - const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); - return Management; -}); - -const { - createAppInfo, - createTempWebExtensionFile, - promiseAddonByID, - promiseAddonEvent, - promiseCompleteAllInstalls, - promiseFindAddonUpdates, - promiseRestartManager, - promiseShutdownManager, - promiseStartupManager, -} = AddonTestUtils; - -AddonTestUtils.init(this); - -// Allow for unsigned addons. -AddonTestUtils.overrideCertDB(); - -createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); - -function awaitEvent(eventName) { - return new Promise(resolve => { - let listener = (_eventName, ...args) => { - if (_eventName === eventName) { - Management.off(eventName, listener); - resolve(...args); - } - }; - - Management.on(eventName, listener); - }); -} - -function background() { - let onInstalledDetails = null; - let onStartupFired = false; - - browser.runtime.onInstalled.addListener(details => { - onInstalledDetails = details; - }); - - browser.runtime.onStartup.addListener(() => { - onStartupFired = true; - }); - - browser.test.onMessage.addListener(message => { - if (message === "get-on-installed-details") { - onInstalledDetails = onInstalledDetails || {fired: false}; - browser.test.sendMessage("on-installed-details", onInstalledDetails); - } else if (message === "did-on-startup-fire") { - browser.test.sendMessage("on-startup-fired", onStartupFired); - } else if (message === "reload-extension") { - browser.runtime.reload(); - } - }); - - browser.runtime.onUpdateAvailable.addListener(details => { - browser.test.sendMessage("reloading"); - browser.runtime.reload(); - }); -} - -function* expectEvents(extension, {onStartupFired, onInstalledFired, onInstalledReason}) { - extension.sendMessage("get-on-installed-details"); - let details = yield extension.awaitMessage("on-installed-details"); - if (onInstalledFired) { - equal(details.reason, onInstalledReason, "runtime.onInstalled fired with the correct reason"); - } else { - equal(details.fired, onInstalledFired, "runtime.onInstalled should not have fired"); - } - - extension.sendMessage("did-on-startup-fire"); - let fired = yield extension.awaitMessage("on-startup-fired"); - equal(fired, onStartupFired, `Expected runtime.onStartup to ${onStartupFired ? "" : "not "} fire`); -} - -add_task(function* test_should_fire_on_addon_update() { - const EXTENSION_ID = "test_runtime_on_installed_addon_update@tests.mozilla.org"; - - const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; - - // The test extension uses an insecure update url. - Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); - - const testServer = createHttpServer(); - const port = testServer.identity.primaryPort; - - let extension = ExtensionTestUtils.loadExtension({ - useAddonManager: "permanent", - manifest: { - "version": "1.0", - "applications": { - "gecko": { - "id": EXTENSION_ID, - "update_url": `http://localhost:${port}/test_update.json`, - }, - }, - }, - background, - }); - - testServer.registerPathHandler("/test_update.json", (request, response) => { - response.write(`{ - "addons": { - "${EXTENSION_ID}": { - "updates": [ - { - "version": "2.0", - "update_link": "http://localhost:${port}/addons/test_runtime_on_installed-2.0.xpi" - } - ] - } - } - }`); - }); - - let webExtensionFile = createTempWebExtensionFile({ - manifest: { - version: "2.0", - applications: { - gecko: { - id: EXTENSION_ID, - }, - }, - }, - background, - }); - - testServer.registerFile("/addons/test_runtime_on_installed-2.0.xpi", webExtensionFile); - - yield promiseStartupManager(); - - yield extension.startup(); - - yield expectEvents(extension, { - onStartupFired: false, - onInstalledFired: true, - onInstalledReason: "install", - }); - - let addon = yield promiseAddonByID(EXTENSION_ID); - equal(addon.version, "1.0", "The installed addon has the correct version"); - - let update = yield promiseFindAddonUpdates(addon); - let install = update.updateAvailable; - - let promiseInstalled = promiseAddonEvent("onInstalled"); - yield promiseCompleteAllInstalls([install]); - - yield extension.awaitMessage("reloading"); - - let startupPromise = awaitEvent("ready"); - - let [updated_addon] = yield promiseInstalled; - equal(updated_addon.version, "2.0", "The updated addon has the correct version"); - - extension.extension = yield startupPromise; - extension.attachListeners(); - - yield expectEvents(extension, { - onStartupFired: false, - onInstalledFired: true, - onInstalledReason: "update", - }); - - yield extension.unload(); - - yield updated_addon.uninstall(); - yield promiseShutdownManager(); -}); - -add_task(function* test_should_fire_on_browser_update() { - const EXTENSION_ID = "test_runtime_on_installed_browser_update@tests.mozilla.org"; - - yield promiseStartupManager(); - - let extension = ExtensionTestUtils.loadExtension({ - useAddonManager: "permanent", - manifest: { - "version": "1.0", - "applications": { - "gecko": { - "id": EXTENSION_ID, - }, - }, - }, - background, - }); - - yield extension.startup(); - - yield expectEvents(extension, { - onStartupFired: false, - onInstalledFired: true, - onInstalledReason: "install", - }); - - let startupPromise = awaitEvent("ready"); - yield promiseRestartManager("1"); - extension.extension = yield startupPromise; - extension.attachListeners(); - - yield expectEvents(extension, { - onStartupFired: true, - onInstalledFired: false, - }); - - // Update the browser. - startupPromise = awaitEvent("ready"); - yield promiseRestartManager("2"); - extension.extension = yield startupPromise; - extension.attachListeners(); - - yield expectEvents(extension, { - onStartupFired: true, - onInstalledFired: true, - onInstalledReason: "browser_update", - }); - - // Restart the browser. - startupPromise = awaitEvent("ready"); - yield promiseRestartManager("2"); - extension.extension = yield startupPromise; - extension.attachListeners(); - - yield expectEvents(extension, { - onStartupFired: true, - onInstalledFired: false, - }); - - // Update the browser again. - startupPromise = awaitEvent("ready"); - yield promiseRestartManager("3"); - extension.extension = yield startupPromise; - extension.attachListeners(); - - yield expectEvents(extension, { - onStartupFired: true, - onInstalledFired: true, - onInstalledReason: "browser_update", - }); - - yield extension.unload(); - - yield promiseShutdownManager(); -}); - -add_task(function* test_should_not_fire_on_reload() { - const EXTENSION_ID = "test_runtime_on_installed_reload@tests.mozilla.org"; - - yield promiseStartupManager(); - - let extension = ExtensionTestUtils.loadExtension({ - useAddonManager: "permanent", - manifest: { - "version": "1.0", - "applications": { - "gecko": { - "id": EXTENSION_ID, - }, - }, - }, - background, - }); - - yield extension.startup(); - - yield expectEvents(extension, { - onStartupFired: false, - onInstalledFired: true, - onInstalledReason: "install", - }); - - let startupPromise = awaitEvent("ready"); - extension.sendMessage("reload-extension"); - extension.extension = yield startupPromise; - extension.attachListeners(); - - yield expectEvents(extension, { - onStartupFired: false, - onInstalledFired: false, - }); - - yield extension.unload(); - yield promiseShutdownManager(); -}); - -add_task(function* test_should_not_fire_on_restart() { - const EXTENSION_ID = "test_runtime_on_installed_restart@tests.mozilla.org"; - - yield promiseStartupManager(); - - let extension = ExtensionTestUtils.loadExtension({ - useAddonManager: "permanent", - manifest: { - "version": "1.0", - "applications": { - "gecko": { - "id": EXTENSION_ID, - }, - }, - }, - background, - }); - - yield extension.startup(); - - yield expectEvents(extension, { - onStartupFired: false, - onInstalledFired: true, - onInstalledReason: "install", - }); - - let addon = yield promiseAddonByID(EXTENSION_ID); - addon.userDisabled = true; - - let startupPromise = awaitEvent("ready"); - addon.userDisabled = false; - extension.extension = yield startupPromise; - extension.attachListeners(); - - yield expectEvents(extension, { - onStartupFired: false, - onInstalledFired: false, - }); - - yield extension.markUnloaded(); - yield promiseShutdownManager(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage.js deleted file mode 100644 index fec8e13dd..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage.js +++ /dev/null @@ -1,79 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* tabsSendMessageReply() { - function background() { - browser.runtime.onMessage.addListener((msg, sender, respond) => { - if (msg == "respond-now") { - respond(msg); - } else if (msg == "respond-soon") { - setTimeout(() => { respond(msg); }, 0); - return true; - } else if (msg == "respond-promise") { - return Promise.resolve(msg); - } else if (msg == "respond-never") { - return; - } else if (msg == "respond-error") { - return Promise.reject(new Error(msg)); - } else if (msg == "throw-error") { - throw new Error(msg); - } - }); - - browser.runtime.onMessage.addListener((msg, sender, respond) => { - if (msg == "respond-now") { - respond("hello"); - } else if (msg == "respond-now-2") { - respond(msg); - } - }); - - let childFrame = document.createElement("iframe"); - childFrame.src = "extensionpage.html"; - document.body.appendChild(childFrame); - } - - function senderScript() { - Promise.all([ - browser.runtime.sendMessage("respond-now"), - browser.runtime.sendMessage("respond-now-2"), - new Promise(resolve => browser.runtime.sendMessage("respond-soon", resolve)), - browser.runtime.sendMessage("respond-promise"), - browser.runtime.sendMessage("respond-never"), - new Promise(resolve => { - browser.runtime.sendMessage("respond-never", response => { resolve(response); }); - }), - - browser.runtime.sendMessage("respond-error").catch(error => Promise.resolve({error})), - browser.runtime.sendMessage("throw-error").catch(error => Promise.resolve({error})), - ]).then(([respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondNever2, respondError, throwError]) => { - browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response"); - browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener"); - browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response"); - browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response"); - browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution"); - browser.test.assertEq(undefined, respondNever2, "Got the expected no-response resolution"); - - browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response"); - browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response"); - - browser.test.notifyPass("sendMessage"); - }).catch(e => { - browser.test.fail(`Error: ${e} :: ${e.stack}`); - browser.test.notifyFail("sendMessage"); - }); - } - - let extension = ExtensionTestUtils.loadExtension({ - background, - files: { - "senderScript.js": senderScript, - "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`, - }, - }); - - yield extension.startup(); - yield extension.awaitFinish("sendMessage"); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js deleted file mode 100644 index f1a8d5a36..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js +++ /dev/null @@ -1,59 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_sendMessage_error() { - async function background() { - let circ = {}; - circ.circ = circ; - let testCases = [ - // [arguments, expected error string], - [[], "runtime.sendMessage's message argument is missing"], - [[null, null, null, null], "runtime.sendMessage's last argument is not a function"], - [[null, null, 1], "runtime.sendMessage's options argument is invalid"], - [[1, null, null], "runtime.sendMessage's extensionId argument is invalid"], - [[null, null, null, null, null], "runtime.sendMessage received too many arguments"], - - // Even when the parameters are accepted, we still expect an error - // because there is no onMessage listener. - [[null, null, null], "Could not establish connection. Receiving end does not exist."], - - // Structural cloning doesn't work with DOM but we fall back - // JSON serialization, so we don't expect another error. - [[null, location, null], "Could not establish connection. Receiving end does not exist."], - - // Structured cloning supports cyclic self-references. - [[null, [circ, location], null], "cyclic object value"], - // JSON serialization does not support cyclic references. - [[null, circ, null], "Could not establish connection. Receiving end does not exist."], - // (the last two tests shows whether sendMessage is implemented as structured cloning). - ]; - - // Repeat all tests with the undefined value instead of null. - for (let [args, expectedError] of testCases.slice()) { - args = args.map(arg => arg === null ? undefined : arg); - testCases.push([args, expectedError]); - } - - for (let [args, expectedError] of testCases) { - let description = `runtime.sendMessage(${args.map(String).join(", ")})`; - - await browser.test.assertRejects( - browser.runtime.sendMessage(...args), - expectedError, - `expected error message for ${description}`); - } - - browser.test.notifyPass("sendMessage parameter validation"); - } - let extensionData = { - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitFinish("sendMessage parameter validation"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js deleted file mode 100644 index f906333d2..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js +++ /dev/null @@ -1,54 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_sendMessage_without_listener() { - async function background() { - await browser.test.assertRejects( - browser.runtime.sendMessage("msg"), - "Could not establish connection. Receiving end does not exist.", - "sendMessage callback was invoked"); - - browser.test.notifyPass("sendMessage callback was invoked"); - } - let extensionData = { - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitFinish("sendMessage callback was invoked"); - - yield extension.unload(); -}); - -add_task(function* test_chrome_sendMessage_without_listener() { - function background() { - /* globals chrome */ - browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call"); - let retval = chrome.runtime.sendMessage("msg"); - browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call"); - browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback"); - - let isAsyncCall = false; - retval = chrome.runtime.sendMessage("msg", reply => { - browser.test.assertEq(undefined, reply, "no reply"); - browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously"); - browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback"); - browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message); - browser.test.notifyPass("finished chrome.runtime.sendMessage"); - }); - isAsyncCall = true; - } - let extensionData = { - background, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitFinish("finished chrome.runtime.sendMessage"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_self.js b/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_self.js deleted file mode 100644 index e4f5e951f..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_runtime_sendMessage_self.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ - -"use strict"; - -add_task(function* test_sendMessage_to_self_should_not_trigger_onMessage() { - async function background() { - browser.runtime.onMessage.addListener(msg => { - browser.test.assertEq("msg from child", msg); - browser.test.notifyPass("sendMessage did not call same-frame onMessage"); - }); - - browser.test.onMessage.addListener(msg => { - browser.test.assertEq("sendMessage with a listener in another frame", msg); - browser.runtime.sendMessage("should only reach another frame"); - }); - - await browser.test.assertRejects( - browser.runtime.sendMessage("should not trigger same-frame onMessage"), - "Could not establish connection. Receiving end does not exist."); - - let anotherFrame = document.createElement("iframe"); - anotherFrame.src = browser.extension.getURL("extensionpage.html"); - document.body.appendChild(anotherFrame); - } - - function lastScript() { - browser.runtime.onMessage.addListener(msg => { - browser.test.assertEq("should only reach another frame", msg); - browser.runtime.sendMessage("msg from child"); - }); - browser.test.sendMessage("sendMessage callback called"); - } - - let extensionData = { - background, - files: { - "lastScript.js": lastScript, - "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="lastScript.js"></script>`, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - - yield extension.awaitMessage("sendMessage callback called"); - extension.sendMessage("sendMessage with a listener in another frame"); - yield extension.awaitFinish("sendMessage did not call same-frame onMessage"); - - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js b/toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js deleted file mode 100644 index d838be5b5..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js +++ /dev/null @@ -1,1427 +0,0 @@ -"use strict"; - -Components.utils.import("resource://gre/modules/Schemas.jsm"); -Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); -Components.utils.import("resource://gre/modules/ExtensionCommon.jsm"); - -let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon; - -let json = [ - {namespace: "testing", - - properties: { - PROP1: {value: 20}, - prop2: {type: "string"}, - prop3: { - $ref: "submodule", - }, - prop4: { - $ref: "submodule", - unsupported: true, - }, - }, - - types: [ - { - id: "type1", - type: "string", - "enum": ["value1", "value2", "value3"], - }, - - { - id: "type2", - type: "object", - properties: { - prop1: {type: "integer"}, - prop2: {type: "array", items: {"$ref": "type1"}}, - }, - }, - - { - id: "basetype1", - type: "object", - properties: { - prop1: {type: "string"}, - }, - }, - - { - id: "basetype2", - choices: [ - {type: "integer"}, - ], - }, - - { - $extend: "basetype1", - properties: { - prop2: {type: "string"}, - }, - }, - - { - $extend: "basetype2", - choices: [ - {type: "string"}, - ], - }, - - { - id: "submodule", - type: "object", - functions: [ - { - name: "sub_foo", - type: "function", - parameters: [], - returns: "integer", - }, - ], - }, - ], - - functions: [ - { - name: "foo", - type: "function", - parameters: [ - {name: "arg1", type: "integer", optional: true, default: 99}, - {name: "arg2", type: "boolean", optional: true}, - ], - }, - - { - name: "bar", - type: "function", - parameters: [ - {name: "arg1", type: "integer", optional: true}, - {name: "arg2", type: "boolean"}, - ], - }, - - { - name: "baz", - type: "function", - parameters: [ - {name: "arg1", type: "object", properties: { - prop1: {type: "string"}, - prop2: {type: "integer", optional: true}, - prop3: {type: "integer", unsupported: true}, - }}, - ], - }, - - { - name: "qux", - type: "function", - parameters: [ - {name: "arg1", "$ref": "type1"}, - ], - }, - - { - name: "quack", - type: "function", - parameters: [ - {name: "arg1", "$ref": "type2"}, - ], - }, - - { - name: "quora", - type: "function", - parameters: [ - {name: "arg1", type: "function"}, - ], - }, - - { - name: "quileute", - type: "function", - parameters: [ - {name: "arg1", type: "integer", optional: true}, - {name: "arg2", type: "integer"}, - ], - }, - - { - name: "queets", - type: "function", - unsupported: true, - parameters: [], - }, - - { - name: "quintuplets", - type: "function", - parameters: [ - {name: "obj", type: "object", properties: [], additionalProperties: {type: "integer"}}, - ], - }, - - { - name: "quasar", - type: "function", - parameters: [ - {name: "abc", type: "object", properties: { - func: {type: "function", parameters: [ - {name: "x", type: "integer"}, - ]}, - }}, - ], - }, - - { - name: "quosimodo", - type: "function", - parameters: [ - {name: "xyz", type: "object", additionalProperties: {type: "any"}}, - ], - }, - - { - name: "patternprop", - type: "function", - parameters: [ - { - name: "obj", - type: "object", - properties: {"prop1": {type: "string", pattern: "^\\d+$"}}, - patternProperties: { - "(?i)^prop\\d+$": {type: "string"}, - "^foo\\d+$": {type: "string"}, - }, - }, - ], - }, - - { - name: "pattern", - type: "function", - parameters: [ - {name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"}, - ], - }, - - { - name: "format", - type: "function", - parameters: [ - { - name: "arg", - type: "object", - properties: { - url: {type: "string", "format": "url", "optional": true}, - relativeUrl: {type: "string", "format": "relativeUrl", "optional": true}, - strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true}, - }, - }, - ], - }, - - { - name: "formatDate", - type: "function", - parameters: [ - { - name: "arg", - type: "object", - properties: { - date: {type: "string", format: "date", optional: true}, - }, - }, - ], - }, - - { - name: "deep", - type: "function", - parameters: [ - { - name: "arg", - type: "object", - properties: { - foo: { - type: "object", - properties: { - bar: { - type: "array", - items: { - type: "object", - properties: { - baz: { - type: "object", - properties: { - required: {type: "integer"}, - optional: {type: "string", optional: true}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - ], - }, - - { - name: "errors", - type: "function", - parameters: [ - { - name: "arg", - type: "object", - properties: { - warn: { - type: "string", - pattern: "^\\d+$", - optional: true, - onError: "warn", - }, - ignore: { - type: "string", - pattern: "^\\d+$", - optional: true, - onError: "ignore", - }, - default: { - type: "string", - pattern: "^\\d+$", - optional: true, - }, - }, - }, - ], - }, - - { - name: "localize", - type: "function", - parameters: [ - { - name: "arg", - type: "object", - properties: { - foo: {type: "string", "preprocess": "localize", "optional": true}, - bar: {type: "string", "optional": true}, - url: {type: "string", "preprocess": "localize", "format": "url", "optional": true}, - }, - }, - ], - }, - - { - name: "extended1", - type: "function", - parameters: [ - {name: "val", $ref: "basetype1"}, - ], - }, - - { - name: "extended2", - type: "function", - parameters: [ - {name: "val", $ref: "basetype2"}, - ], - }, - ], - - events: [ - { - name: "onFoo", - type: "function", - }, - - { - name: "onBar", - type: "function", - extraParameters: [{ - name: "filter", - type: "integer", - optional: true, - default: 1, - }], - }, - ], - }, - { - namespace: "foreign", - properties: { - foreignRef: {$ref: "testing.submodule"}, - }, - }, - { - namespace: "inject", - properties: { - PROP1: {value: "should inject"}, - }, - }, - { - namespace: "do-not-inject", - properties: { - PROP1: {value: "should not inject"}, - }, - }, -]; - -let tallied = null; - -function tally(kind, ns, name, args) { - tallied = [kind, ns, name, args]; -} - -function verify(...args) { - do_check_eq(JSON.stringify(tallied), JSON.stringify(args)); - tallied = null; -} - -let talliedErrors = []; - -function checkErrors(errors) { - do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors"); - for (let [i, error] of errors.entries()) { - do_check_true(i in talliedErrors && talliedErrors[i].includes(error), - `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`); - } - - talliedErrors.length = 0; -} - -let permissions = new Set(); - -class TallyingAPIImplementation extends SchemaAPIInterface { - constructor(namespace, name) { - super(); - this.namespace = namespace; - this.name = name; - } - - callFunction(args) { - tally("call", this.namespace, this.name, args); - } - - callFunctionNoReturn(args) { - tally("call", this.namespace, this.name, args); - } - - getProperty() { - tally("get", this.namespace, this.name); - } - - setProperty(value) { - tally("set", this.namespace, this.name, value); - } - - addListener(listener, args) { - tally("addListener", this.namespace, this.name, [listener, args]); - } - - removeListener(listener) { - tally("removeListener", this.namespace, this.name, [listener]); - } - - hasListener(listener) { - tally("hasListener", this.namespace, this.name, [listener]); - } -} - -let wrapper = { - url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/", - - checkLoadURL(url) { - return !url.startsWith("chrome:"); - }, - - preprocessors: { - localize(value, context) { - return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`); - }, - }, - - logError(message) { - talliedErrors.push(message); - }, - - hasPermission(permission) { - return permissions.has(permission); - }, - - shouldInject(ns) { - return ns != "do-not-inject"; - }, - - getImplementation(namespace, name) { - return new TallyingAPIImplementation(namespace, name); - }, -}; - -add_task(function* () { - let url = "data:," + JSON.stringify(json); - yield Schemas.load(url); - - let root = {}; - tallied = null; - Schemas.inject(root, wrapper); - do_check_eq(tallied, null); - - do_check_eq(root.testing.PROP1, 20, "simple value property"); - do_check_eq(root.testing.type1.VALUE1, "value1", "enum type"); - do_check_eq(root.testing.type1.VALUE2, "value2", "enum type"); - - do_check_eq("inject" in root, true, "namespace 'inject' should be injected"); - do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected"); - - root.testing.foo(11, true); - verify("call", "testing", "foo", [11, true]); - - root.testing.foo(true); - verify("call", "testing", "foo", [99, true]); - - root.testing.foo(null, true); - verify("call", "testing", "foo", [99, true]); - - root.testing.foo(undefined, true); - verify("call", "testing", "foo", [99, true]); - - root.testing.foo(11); - verify("call", "testing", "foo", [11, null]); - - Assert.throws(() => root.testing.bar(11), - /Incorrect argument types/, - "should throw without required arg"); - - Assert.throws(() => root.testing.bar(11, true, 10), - /Incorrect argument types/, - "should throw with too many arguments"); - - root.testing.bar(true); - verify("call", "testing", "bar", [null, true]); - - root.testing.baz({prop1: "hello", prop2: 22}); - verify("call", "testing", "baz", [{prop1: "hello", prop2: 22}]); - - root.testing.baz({prop1: "hello"}); - verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]); - - root.testing.baz({prop1: "hello", prop2: null}); - verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]); - - Assert.throws(() => root.testing.baz({prop2: 12}), - /Property "prop1" is required/, - "should throw without required property"); - - Assert.throws(() => root.testing.baz({prop1: "hi", prop3: 12}), - /Property "prop3" is unsupported by Firefox/, - "should throw with unsupported property"); - - Assert.throws(() => root.testing.baz({prop1: "hi", prop4: 12}), - /Unexpected property "prop4"/, - "should throw with unexpected property"); - - Assert.throws(() => root.testing.baz({prop1: 12}), - /Expected string instead of 12/, - "should throw with wrong type"); - - root.testing.qux("value2"); - verify("call", "testing", "qux", ["value2"]); - - Assert.throws(() => root.testing.qux("value4"), - /Invalid enumeration value "value4"/, - "should throw for invalid enum value"); - - root.testing.quack({prop1: 12, prop2: ["value1", "value3"]}); - verify("call", "testing", "quack", [{prop1: 12, prop2: ["value1", "value3"]}]); - - Assert.throws(() => root.testing.quack({prop1: 12, prop2: ["value1", "value3", "value4"]}), - /Invalid enumeration value "value4"/, - "should throw for invalid array type"); - - function f() {} - root.testing.quora(f); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"])); - do_check_eq(tallied[3][0], f); - tallied = null; - - let g = () => 0; - root.testing.quora(g); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"])); - do_check_eq(tallied[3][0], g); - tallied = null; - - root.testing.quileute(10); - verify("call", "testing", "quileute", [null, 10]); - - Assert.throws(() => root.testing.queets(), - /queets is not a function/, - "should throw for unsupported functions"); - - root.testing.quintuplets({a: 10, b: 20, c: 30}); - verify("call", "testing", "quintuplets", [{a: 10, b: 20, c: 30}]); - - Assert.throws(() => root.testing.quintuplets({a: 10, b: 20, c: 30, d: "hi"}), - /Expected integer instead of "hi"/, - "should throw for wrong additionalProperties type"); - - root.testing.quasar({func: f}); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quasar"])); - do_check_eq(tallied[3][0].func, f); - tallied = null; - - root.testing.quosimodo({a: 10, b: 20, c: 30}); - verify("call", "testing", "quosimodo", [{a: 10, b: 20, c: 30}]); - tallied = null; - - Assert.throws(() => root.testing.quosimodo(10), - /Incorrect argument types/, - "should throw for wrong type"); - - root.testing.patternprop({prop1: "12", prop2: "42", Prop3: "43", foo1: "x"}); - verify("call", "testing", "patternprop", [{prop1: "12", prop2: "42", Prop3: "43", foo1: "x"}]); - tallied = null; - - root.testing.patternprop({prop1: "12"}); - verify("call", "testing", "patternprop", [{prop1: "12"}]); - tallied = null; - - Assert.throws(() => root.testing.patternprop({prop1: "12", foo1: null}), - /Expected string instead of null/, - "should throw for wrong property type"); - - Assert.throws(() => root.testing.patternprop({prop1: "xx", prop2: "yy"}), - /String "xx" must match \/\^\\d\+\$\//, - "should throw for wrong property type"); - - Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: 42}), - /Expected string instead of 42/, - "should throw for wrong property type"); - - Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: null}), - /Expected string instead of null/, - "should throw for wrong property type"); - - Assert.throws(() => root.testing.patternprop({prop1: "12", propx: "42"}), - /Unexpected property "propx"/, - "should throw for unexpected property"); - - Assert.throws(() => root.testing.patternprop({prop1: "12", Foo1: "x"}), - /Unexpected property "Foo1"/, - "should throw for unexpected property"); - - root.testing.pattern("DEADbeef"); - verify("call", "testing", "pattern", ["DEADbeef"]); - tallied = null; - - Assert.throws(() => root.testing.pattern("DEADcow"), - /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/, - "should throw for non-match"); - - root.testing.format({url: "http://foo/bar", - relativeUrl: "http://foo/bar"}); - verify("call", "testing", "format", [{url: "http://foo/bar", - relativeUrl: "http://foo/bar", - strictRelativeUrl: null}]); - tallied = null; - - root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"}); - verify("call", "testing", "format", [{url: null, - relativeUrl: `${wrapper.url}foo.html`, - strictRelativeUrl: `${wrapper.url}foo.html`}]); - tallied = null; - - for (let format of ["url", "relativeUrl"]) { - Assert.throws(() => root.testing.format({[format]: "chrome://foo/content/"}), - /Access denied/, - "should throw for access denied"); - } - - for (let urlString of ["//foo.html", "http://foo/bar.html"]) { - Assert.throws(() => root.testing.format({strictRelativeUrl: urlString}), - /must be a relative URL/, - "should throw for non-relative URL"); - } - - const dates = [ - "2016-03-04", - "2016-03-04T08:00:00Z", - "2016-03-04T08:00:00.000Z", - "2016-03-04T08:00:00-08:00", - "2016-03-04T08:00:00.000-08:00", - "2016-03-04T08:00:00+08:00", - "2016-03-04T08:00:00.000+08:00", - "2016-03-04T08:00:00+0800", - "2016-03-04T08:00:00-0800", - ]; - dates.forEach(str => { - root.testing.formatDate({date: str}); - verify("call", "testing", "formatDate", [{date: str}]); - }); - - // Make sure that a trivial change to a valid date invalidates it. - dates.forEach(str => { - Assert.throws(() => root.testing.formatDate({date: "0" + str}), - /Invalid date string/, - "should throw for invalid iso date string"); - Assert.throws(() => root.testing.formatDate({date: str + "0"}), - /Invalid date string/, - "should throw for invalid iso date string"); - }); - - const badDates = [ - "I do not look anything like a date string", - "2016-99-99", - "2016-03-04T25:00:00Z", - ]; - badDates.forEach(str => { - Assert.throws(() => root.testing.formatDate({date: str}), - /Invalid date string/, - "should throw for invalid iso date string"); - }); - - root.testing.deep({foo: {bar: [{baz: {required: 12, optional: "42"}}]}}); - verify("call", "testing", "deep", [{foo: {bar: [{baz: {required: 12, optional: "42"}}]}}]); - tallied = null; - - Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {optional: "42"}}]}}), - /Type error for parameter arg \(Error processing foo\.bar\.0\.baz: Property "required" is required\) for testing\.deep/, - "should throw with the correct object path"); - - Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {required: 12, optional: 42}}]}}), - /Type error for parameter arg \(Error processing foo\.bar\.0\.baz\.optional: Expected string instead of 42\) for testing\.deep/, - "should throw with the correct object path"); - - - talliedErrors.length = 0; - - root.testing.errors({warn: "0123", ignore: "0123", default: "0123"}); - verify("call", "testing", "errors", [{warn: "0123", ignore: "0123", default: "0123"}]); - checkErrors([]); - - root.testing.errors({warn: "0123", ignore: "x123", default: "0123"}); - verify("call", "testing", "errors", [{warn: "0123", ignore: null, default: "0123"}]); - checkErrors([]); - - root.testing.errors({warn: "x123", ignore: "0123", default: "0123"}); - verify("call", "testing", "errors", [{warn: null, ignore: "0123", default: "0123"}]); - checkErrors([ - 'String "x123" must match /^\\d+$/', - ]); - - - root.testing.onFoo.addListener(f); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"])); - do_check_eq(tallied[3][0], f); - do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([])); - tallied = null; - - root.testing.onFoo.removeListener(f); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["removeListener", "testing", "onFoo"])); - do_check_eq(tallied[3][0], f); - tallied = null; - - root.testing.onFoo.hasListener(f); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["hasListener", "testing", "onFoo"])); - do_check_eq(tallied[3][0], f); - tallied = null; - - Assert.throws(() => root.testing.onFoo.addListener(10), - /Invalid listener/, - "addListener with non-function should throw"); - - root.testing.onBar.addListener(f, 10); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"])); - do_check_eq(tallied[3][0], f); - do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([10])); - tallied = null; - - root.testing.onBar.addListener(f); - do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"])); - do_check_eq(tallied[3][0], f); - do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([1])); - tallied = null; - - Assert.throws(() => root.testing.onBar.addListener(f, "hi"), - /Incorrect argument types/, - "addListener with wrong extra parameter should throw"); - - let target = {prop1: 12, prop2: ["value1", "value3"]}; - let proxy = new Proxy(target, {}); - Assert.throws(() => root.testing.quack(proxy), - /Expected a plain JavaScript object, got a Proxy/, - "should throw when passing a Proxy"); - - if (Symbol.toStringTag) { - let stringTarget = {prop1: 12, prop2: ["value1", "value3"]}; - stringTarget[Symbol.toStringTag] = () => "[object Object]"; - let stringProxy = new Proxy(stringTarget, {}); - Assert.throws(() => root.testing.quack(stringProxy), - /Expected a plain JavaScript object, got a Proxy/, - "should throw when passing a Proxy"); - } - - - root.testing.localize({foo: "__MSG_foo__", bar: "__MSG_foo__", url: "__MSG_http://example.com/__"}); - verify("call", "testing", "localize", [{foo: "FOO", bar: "__MSG_foo__", url: "http://example.com/"}]); - tallied = null; - - - Assert.throws(() => root.testing.localize({url: "__MSG_/foo/bar__"}), - /\/FOO\/BAR is not a valid URL\./, - "should throw for invalid URL"); - - - root.testing.extended1({prop1: "foo", prop2: "bar"}); - verify("call", "testing", "extended1", [{prop1: "foo", prop2: "bar"}]); - tallied = null; - - Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: 12}), - /Expected string instead of 12/, - "should throw for wrong property type"); - - Assert.throws(() => root.testing.extended1({prop1: "foo"}), - /Property "prop2" is required/, - "should throw for missing property"); - - Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: "bar", prop3: "xxx"}), - /Unexpected property "prop3"/, - "should throw for extra property"); - - - root.testing.extended2("foo"); - verify("call", "testing", "extended2", ["foo"]); - tallied = null; - - root.testing.extended2(12); - verify("call", "testing", "extended2", [12]); - tallied = null; - - Assert.throws(() => root.testing.extended2(true), - /Incorrect argument types/, - "should throw for wrong argument type"); - - root.testing.prop3.sub_foo(); - verify("call", "testing.prop3", "sub_foo", []); - tallied = null; - - Assert.throws(() => root.testing.prop4.sub_foo(), - /root.testing.prop4 is undefined/, - "should throw for unsupported submodule"); - - root.foreign.foreignRef.sub_foo(); - verify("call", "foreign.foreignRef", "sub_foo", []); - tallied = null; -}); - -let deprecatedJson = [ - {namespace: "deprecated", - - properties: { - accessor: { - type: "string", - writable: true, - deprecated: "This is not the property you are looking for", - }, - }, - - types: [ - { - "id": "Type", - "type": "string", - }, - ], - - functions: [ - { - name: "property", - type: "function", - parameters: [ - { - name: "arg", - type: "object", - properties: { - foo: { - type: "string", - }, - }, - additionalProperties: { - type: "any", - deprecated: "Unknown property", - }, - }, - ], - }, - - { - name: "value", - type: "function", - parameters: [ - { - name: "arg", - choices: [ - { - type: "integer", - }, - { - type: "string", - deprecated: "Please use an integer, not ${value}", - }, - ], - }, - ], - }, - - { - name: "choices", - type: "function", - parameters: [ - { - name: "arg", - deprecated: "You have no choices", - choices: [ - { - type: "integer", - }, - ], - }, - ], - }, - - { - name: "ref", - type: "function", - parameters: [ - { - name: "arg", - choices: [ - { - $ref: "Type", - deprecated: "Deprecated alias", - }, - ], - }, - ], - }, - - { - name: "method", - type: "function", - deprecated: "Do not call this method", - parameters: [ - ], - }, - ], - - events: [ - { - name: "onDeprecated", - type: "function", - deprecated: "This event does not work", - }, - ], - }, -]; - -add_task(function* testDeprecation() { - let url = "data:," + JSON.stringify(deprecatedJson); - yield Schemas.load(url); - - let root = {}; - Schemas.inject(root, wrapper); - - talliedErrors.length = 0; - - - root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"}); - verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]); - checkErrors([ - "Error processing xxx: Unknown property", - "Error processing yyy: Unknown property", - ]); - - root.deprecated.value(12); - verify("call", "deprecated", "value", [12]); - checkErrors([]); - - root.deprecated.value("12"); - verify("call", "deprecated", "value", ["12"]); - checkErrors(["Please use an integer, not \"12\""]); - - root.deprecated.choices(12); - verify("call", "deprecated", "choices", [12]); - checkErrors(["You have no choices"]); - - root.deprecated.ref("12"); - verify("call", "deprecated", "ref", ["12"]); - checkErrors(["Deprecated alias"]); - - root.deprecated.method(); - verify("call", "deprecated", "method", []); - checkErrors(["Do not call this method"]); - - - void root.deprecated.accessor; - verify("get", "deprecated", "accessor", null); - checkErrors(["This is not the property you are looking for"]); - - root.deprecated.accessor = "x"; - verify("set", "deprecated", "accessor", "x"); - checkErrors(["This is not the property you are looking for"]); - - - root.deprecated.onDeprecated.addListener(() => {}); - checkErrors(["This event does not work"]); - - root.deprecated.onDeprecated.removeListener(() => {}); - checkErrors(["This event does not work"]); - - root.deprecated.onDeprecated.hasListener(() => {}); - checkErrors(["This event does not work"]); -}); - - -let choicesJson = [ - {namespace: "choices", - - types: [ - ], - - functions: [ - { - name: "meh", - type: "function", - parameters: [ - { - name: "arg", - choices: [ - { - type: "string", - enum: ["foo", "bar", "baz"], - }, - { - type: "string", - pattern: "florg.*meh", - }, - { - type: "integer", - minimum: 12, - maximum: 42, - }, - ], - }, - ], - }, - - { - name: "foo", - type: "function", - parameters: [ - { - name: "arg", - choices: [ - { - type: "object", - properties: { - blurg: { - type: "string", - unsupported: true, - optional: true, - }, - }, - additionalProperties: { - type: "string", - }, - }, - { - type: "string", - }, - { - type: "array", - minItems: 2, - maxItems: 3, - items: { - type: "integer", - }, - }, - ], - }, - ], - }, - - { - name: "bar", - type: "function", - parameters: [ - { - name: "arg", - choices: [ - { - type: "object", - properties: { - baz: { - type: "string", - }, - }, - }, - { - type: "array", - items: { - type: "integer", - }, - }, - ], - }, - ], - }, - ]}, -]; - -add_task(function* testChoices() { - let url = "data:," + JSON.stringify(choicesJson); - yield Schemas.load(url); - - let root = {}; - Schemas.inject(root, wrapper); - - talliedErrors.length = 0; - - Assert.throws(() => root.choices.meh("frog"), - /Value must either: be one of \["foo", "bar", "baz"\], match the pattern \/florg\.\*meh\/, or be an integer value/); - - Assert.throws(() => root.choices.meh(4), - /be a string value, or be at least 12/); - - Assert.throws(() => root.choices.meh(43), - /be a string value, or be no greater than 42/); - - - Assert.throws(() => root.choices.foo([]), - /be an object value, be a string value, or have at least 2 items/); - - Assert.throws(() => root.choices.foo([1, 2, 3, 4]), - /be an object value, be a string value, or have at most 3 items/); - - Assert.throws(() => root.choices.foo({foo: 12}), - /.foo must be a string value, be a string value, or be an array value/); - - Assert.throws(() => root.choices.foo({blurg: "foo"}), - /not contain an unsupported "blurg" property, be a string value, or be an array value/); - - - Assert.throws(() => root.choices.bar({}), - /contain the required "baz" property, or be an array value/); - - Assert.throws(() => root.choices.bar({baz: "x", quux: "y"}), - /not contain an unexpected "quux" property, or be an array value/); - - Assert.throws(() => root.choices.bar({baz: "x", quux: "y", foo: "z"}), - /not contain the unexpected properties \[foo, quux\], or be an array value/); -}); - - -let permissionsJson = [ - {namespace: "noPerms", - - types: [], - - functions: [ - { - name: "noPerms", - type: "function", - parameters: [], - }, - - { - name: "fooPerm", - type: "function", - permissions: ["foo"], - parameters: [], - }, - ]}, - - {namespace: "fooPerm", - - permissions: ["foo"], - - types: [], - - functions: [ - { - name: "noPerms", - type: "function", - parameters: [], - }, - - { - name: "fooBarPerm", - type: "function", - permissions: ["foo.bar"], - parameters: [], - }, - ]}, -]; - -add_task(function* testPermissions() { - let url = "data:," + JSON.stringify(permissionsJson); - yield Schemas.load(url); - - let root = {}; - Schemas.inject(root, wrapper); - - equal(typeof root.noPerms, "object", "noPerms namespace should exist"); - equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist"); - - ok(!("fooPerm" in root.noPerms), "noPerms.fooPerm should not method exist"); - - ok(!("fooPerm" in root), "fooPerm namespace should not exist"); - - - do_print('Add "foo" permission'); - permissions.add("foo"); - - root = {}; - Schemas.inject(root, wrapper); - - equal(typeof root.noPerms, "object", "noPerms namespace should exist"); - equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist"); - equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist"); - - equal(typeof root.fooPerm, "object", "fooPerm namespace should exist"); - equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist"); - - ok(!("fooBarPerm" in root.fooPerm), "fooPerm.fooBarPerm method should not exist"); - - - do_print('Add "foo.bar" permission'); - permissions.add("foo.bar"); - - root = {}; - Schemas.inject(root, wrapper); - - equal(typeof root.noPerms, "object", "noPerms namespace should exist"); - equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist"); - equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist"); - - equal(typeof root.fooPerm, "object", "fooPerm namespace should exist"); - equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist"); - equal(typeof root.fooPerm.fooBarPerm, "function", "noPerms.fooBarPerm method should exist"); -}); - -let nestedNamespaceJson = [ - { - "namespace": "nested.namespace", - "types": [ - { - "id": "CustomType", - "type": "object", - "events": [ - { - "name": "onEvent", - }, - ], - "properties": { - "url": { - "type": "string", - }, - }, - "functions": [ - { - "name": "functionOnCustomType", - "type": "function", - "parameters": [ - { - "name": "title", - "type": "string", - }, - ], - }, - ], - }, - ], - "properties": { - "instanceOfCustomType": { - "$ref": "CustomType", - }, - }, - "functions": [ - { - "name": "create", - "type": "function", - "parameters": [ - { - "name": "title", - "type": "string", - }, - ], - }, - ], - }, -]; - -add_task(function* testNestedNamespace() { - let url = "data:," + JSON.stringify(nestedNamespaceJson); - - yield Schemas.load(url); - - let root = {}; - Schemas.inject(root, wrapper); - - talliedErrors.length = 0; - - ok(root.nested, "The root object contains the first namespace level"); - ok(root.nested.namespace, "The first level object contains the second namespace level"); - - ok(root.nested.namespace.create, "Got the expected function in the nested namespace"); - do_check_eq(typeof root.nested.namespace.create, "function", - "The property is a function as expected"); - - let {instanceOfCustomType} = root.nested.namespace; - - ok(instanceOfCustomType, - "Got the expected instance of the CustomType defined in the schema"); - ok(instanceOfCustomType.functionOnCustomType, - "Got the expected method in the CustomType instance"); - - // TODO: test support events and properties in a SubModuleType defined in the schema, - // once implemented, e.g.: - // - // ok(instanceOfCustomType.url, - // "Got the expected property defined in the CustomType instance) - // - // ok(instanceOfCustomType.onEvent && - // instanceOfCustomType.onEvent.addListener && - // typeof instanceOfCustomType.onEvent.addListener == "function", - // "Got the expected event defined in the CustomType instance"); -}); - -add_task(function* testLocalAPIImplementation() { - let countGet2 = 0; - let countProp3 = 0; - let countProp3SubFoo = 0; - - let testingApiObj = { - get PROP1() { - // PROP1 is a schema-defined constant. - throw new Error("Unexpected get PROP1"); - }, - get prop2() { - ++countGet2; - return "prop2 val"; - }, - get prop3() { - throw new Error("Unexpected get prop3"); - }, - set prop3(v) { - // prop3 is a submodule, defined as a function, so the API should not pass - // through assignment to prop3. - throw new Error("Unexpected set prop3"); - }, - }; - let submoduleApiObj = { - get sub_foo() { - ++countProp3; - return () => { - return ++countProp3SubFoo; - }; - }, - }; - - let localWrapper = { - shouldInject(ns) { - return ns == "testing" || ns == "testing.prop3"; - }, - getImplementation(ns, name) { - do_check_true(ns == "testing" || ns == "testing.prop3"); - if (ns == "testing.prop3" && name == "sub_foo") { - // It is fine to use `null` here because we don't call async functions. - return new LocalAPIImplementation(submoduleApiObj, name, null); - } - // It is fine to use `null` here because we don't call async functions. - return new LocalAPIImplementation(testingApiObj, name, null); - }, - }; - - let root = {}; - Schemas.inject(root, localWrapper); - do_check_eq(countGet2, 0); - do_check_eq(countProp3, 0); - do_check_eq(countProp3SubFoo, 0); - - do_check_eq(root.testing.PROP1, 20); - - do_check_eq(root.testing.prop2, "prop2 val"); - do_check_eq(countGet2, 1); - - do_check_eq(root.testing.prop2, "prop2 val"); - do_check_eq(countGet2, 2); - - do_print(JSON.stringify(root.testing)); - do_check_eq(root.testing.prop3.sub_foo(), 1); - do_check_eq(countProp3, 1); - do_check_eq(countProp3SubFoo, 1); - - do_check_eq(root.testing.prop3.sub_foo(), 2); - do_check_eq(countProp3, 2); - do_check_eq(countProp3SubFoo, 2); - - root.testing.prop3.sub_foo = () => { return "overwritten"; }; - do_check_eq(root.testing.prop3.sub_foo(), "overwritten"); - - root.testing.prop3 = {sub_foo() { return "overwritten again"; }}; - do_check_eq(root.testing.prop3.sub_foo(), "overwritten again"); - do_check_eq(countProp3SubFoo, 2); -}); - - -let defaultsJson = [ - {namespace: "defaultsJson", - - types: [], - - functions: [ - { - name: "defaultFoo", - type: "function", - parameters: [ - {name: "arg", type: "object", optional: true, properties: { - prop1: {type: "integer", optional: true}, - }, default: {prop1: 1}}, - ], - returns: { - type: "object", - }, - }, - ]}, -]; - -add_task(function* testDefaults() { - let url = "data:," + JSON.stringify(defaultsJson); - yield Schemas.load(url); - - let testingApiObj = { - defaultFoo: function(arg) { - if (Object.keys(arg) != "prop1") { - throw new Error(`Received the expected default object, default: ${JSON.stringify(arg)}`); - } - arg.newProp = 1; - return arg; - }, - }; - - let localWrapper = { - shouldInject(ns) { - return true; - }, - getImplementation(ns, name) { - return new LocalAPIImplementation(testingApiObj, name, null); - }, - }; - - let root = {}; - Schemas.inject(root, localWrapper); - - deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1}); - deepEqual(root.defaultsJson.defaultFoo({prop1: 2}), {prop1: 2, newProp: 1}); - deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1}); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_allowed_contexts.js b/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_allowed_contexts.js deleted file mode 100644 index 606459764..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_allowed_contexts.js +++ /dev/null @@ -1,147 +0,0 @@ -"use strict"; - -Components.utils.import("resource://gre/modules/Schemas.jsm"); - -let schemaJson = [ - { - namespace: "noAllowedContexts", - properties: { - prop1: {type: "object"}, - prop2: {type: "object", allowedContexts: ["test_zero", "test_one"]}, - prop3: {type: "number", value: 1}, - prop4: {type: "number", value: 1, allowedContexts: ["numeric_one"]}, - }, - }, - { - namespace: "defaultContexts", - defaultContexts: ["test_two"], - properties: { - prop1: {type: "object"}, - prop2: {type: "object", allowedContexts: ["test_three"]}, - prop3: {type: "number", value: 1}, - prop4: {type: "number", value: 1, allowedContexts: ["numeric_two"]}, - }, - }, - { - namespace: "withAllowedContexts", - allowedContexts: ["test_four"], - properties: { - prop1: {type: "object"}, - prop2: {type: "object", allowedContexts: ["test_five"]}, - prop3: {type: "number", value: 1}, - prop4: {type: "number", value: 1, allowedContexts: ["numeric_three"]}, - }, - }, - { - namespace: "withAllowedContextsAndDefault", - allowedContexts: ["test_six"], - defaultContexts: ["test_seven"], - properties: { - prop1: {type: "object"}, - prop2: {type: "object", allowedContexts: ["test_eight"]}, - prop3: {type: "number", value: 1}, - prop4: {type: "number", value: 1, allowedContexts: ["numeric_four"]}, - }, - }, - { - namespace: "with_submodule", - defaultContexts: ["test_nine"], - types: [{ - id: "subtype", - type: "object", - functions: [{ - name: "noAllowedContexts", - type: "function", - parameters: [], - }, { - name: "allowedContexts", - allowedContexts: ["test_ten"], - type: "function", - parameters: [], - }], - }], - properties: { - prop1: {$ref: "subtype"}, - prop2: {$ref: "subtype", allowedContexts: ["test_eleven"]}, - }, - }, -]; -add_task(function* testRestrictions() { - let url = "data:," + JSON.stringify(schemaJson); - yield Schemas.load(url); - let results = {}; - let localWrapper = { - shouldInject(ns, name, allowedContexts) { - name = name === null ? ns : ns + "." + name; - results[name] = allowedContexts.join(","); - return true; - }, - getImplementation() { - // The actual implementation is not significant for this test. - // Let's take this opportunity to see if schema generation is free of - // exceptions even when somehow getImplementation does not return an - // implementation. - }, - }; - - let root = {}; - Schemas.inject(root, localWrapper); - - function verify(path, expected) { - let obj = root; - for (let thing of path.split(".")) { - try { - obj = obj[thing]; - } catch (e) { - // Blech. - } - } - - let result = results[path]; - equal(result, expected); - } - - verify("noAllowedContexts", ""); - verify("noAllowedContexts.prop1", ""); - verify("noAllowedContexts.prop2", "test_zero,test_one"); - verify("noAllowedContexts.prop3", ""); - verify("noAllowedContexts.prop4", "numeric_one"); - - verify("defaultContexts", ""); - verify("defaultContexts.prop1", "test_two"); - verify("defaultContexts.prop2", "test_three"); - verify("defaultContexts.prop3", "test_two"); - verify("defaultContexts.prop4", "numeric_two"); - - verify("withAllowedContexts", "test_four"); - verify("withAllowedContexts.prop1", ""); - verify("withAllowedContexts.prop2", "test_five"); - verify("withAllowedContexts.prop3", ""); - verify("withAllowedContexts.prop4", "numeric_three"); - - verify("withAllowedContextsAndDefault", "test_six"); - verify("withAllowedContextsAndDefault.prop1", "test_seven"); - verify("withAllowedContextsAndDefault.prop2", "test_eight"); - verify("withAllowedContextsAndDefault.prop3", "test_seven"); - verify("withAllowedContextsAndDefault.prop4", "numeric_four"); - - verify("with_submodule", ""); - verify("with_submodule.prop1", "test_nine"); - verify("with_submodule.prop1.noAllowedContexts", "test_nine"); - verify("with_submodule.prop1.allowedContexts", "test_ten"); - verify("with_submodule.prop2", "test_eleven"); - // Note: test_nine inherits allowed contexts from the namespace, not from - // submodule. There is no "defaultContexts" for submodule types to not - // complicate things. - verify("with_submodule.prop1.noAllowedContexts", "test_nine"); - verify("with_submodule.prop1.allowedContexts", "test_ten"); - - // This is a constant, so it does not matter that getImplementation does not - // return an implementation since the API injector should take care of it. - equal(root.noAllowedContexts.prop3, 1); - - Assert.throws(() => root.noAllowedContexts.prop1, - /undefined/, - "Should throw when the implementation is absent."); -}); - diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_api_injection.js b/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_api_injection.js deleted file mode 100644 index 36d88d722..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_api_injection.js +++ /dev/null @@ -1,102 +0,0 @@ -"use strict"; - -Components.utils.import("resource://gre/modules/ExtensionCommon.jsm"); -Components.utils.import("resource://gre/modules/Schemas.jsm"); - -let { - BaseContext, - SchemaAPIManager, -} = ExtensionCommon; - -let nestedNamespaceJson = [ - { - "namespace": "backgroundAPI.testnamespace", - "functions": [ - { - "name": "create", - "type": "function", - "parameters": [ - { - "name": "title", - "type": "string", - }, - ], - "returns": { - "type": "string", - }, - }, - ], - }, - { - "namespace": "noBackgroundAPI.testnamespace", - "functions": [ - { - "name": "create", - "type": "function", - "parameters": [ - { - "name": "title", - "type": "string", - }, - ], - }, - ], - }, -]; - -let global = this; -class StubContext extends BaseContext { - constructor() { - let fakeExtension = {id: "test@web.extension"}; - super("addon_child", fakeExtension); - this.sandbox = Cu.Sandbox(global); - this.viewType = "background"; - } - - get cloneScope() { - return this.sandbox; - } -} - -add_task(function* testSchemaAPIInjection() { - let url = "data:," + JSON.stringify(nestedNamespaceJson); - - // Load the schema of the fake APIs. - yield Schemas.load(url); - - let apiManager = new SchemaAPIManager("addon"); - - // Register an API that will skip the background page. - apiManager.registerSchemaAPI("noBackgroundAPI.testnamespace", "addon_child", context => { - // This API should not be available in this context, return null so that - // the schema wrapper is removed as well. - return null; - }); - - // Register an API that will skip any but the background page. - apiManager.registerSchemaAPI("backgroundAPI.testnamespace", "addon_child", context => { - if (context.viewType === "background") { - return { - backgroundAPI: { - testnamespace: { - create(title) { - return title; - }, - }, - }, - }; - } - - // This API should not be available in this context, return null so that - // the schema wrapper is removed as well. - return null; - }); - - let context = new StubContext(); - let browserObj = {}; - apiManager.generateAPIs(context, browserObj); - - do_check_eq(browserObj.noBackgroundAPI, undefined); - const res = browserObj.backgroundAPI.testnamespace.create("param-value"); - do_check_eq(res, "param-value"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_async.js b/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_async.js deleted file mode 100644 index 6397d1f96..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas_async.js +++ /dev/null @@ -1,232 +0,0 @@ -"use strict"; - -Components.utils.import("resource://gre/modules/ExtensionCommon.jsm"); -Components.utils.import("resource://gre/modules/Schemas.jsm"); - -let {BaseContext, LocalAPIImplementation} = ExtensionCommon; - -let schemaJson = [ - { - namespace: "testnamespace", - functions: [{ - name: "one_required", - type: "function", - parameters: [{ - name: "first", - type: "function", - parameters: [], - }], - }, { - name: "one_optional", - type: "function", - parameters: [{ - name: "first", - type: "function", - parameters: [], - optional: true, - }], - }, { - name: "async_required", - type: "function", - async: "first", - parameters: [{ - name: "first", - type: "function", - parameters: [], - }], - }, { - name: "async_optional", - type: "function", - async: "first", - parameters: [{ - name: "first", - type: "function", - parameters: [], - optional: true, - }], - }], - }, -]; - -const global = this; -class StubContext extends BaseContext { - constructor() { - let fakeExtension = {id: "test@web.extension"}; - super("testEnv", fakeExtension); - this.sandbox = Cu.Sandbox(global); - } - - get cloneScope() { - return this.sandbox; - } - - get principal() { - return Cu.getObjectPrincipal(this.sandbox); - } -} - -let context; - -function generateAPIs(extraWrapper, apiObj) { - context = new StubContext(); - let localWrapper = { - shouldInject() { - return true; - }, - getImplementation(namespace, name) { - return new LocalAPIImplementation(apiObj, name, context); - }, - }; - Object.assign(localWrapper, extraWrapper); - - let root = {}; - Schemas.inject(root, localWrapper); - return root.testnamespace; -} - -add_task(function* testParameterValidation() { - yield Schemas.load("data:," + JSON.stringify(schemaJson)); - - let testnamespace; - function assertThrows(name, ...args) { - Assert.throws(() => testnamespace[name](...args), - /Incorrect argument types/, - `Expected testnamespace.${name}(${args.map(String).join(", ")}) to throw.`); - } - function assertNoThrows(name, ...args) { - try { - testnamespace[name](...args); - } catch (e) { - do_print(`testnamespace.${name}(${args.map(String).join(", ")}) unexpectedly threw.`); - throw new Error(e); - } - } - let cb = () => {}; - - for (let isChromeCompat of [true, false]) { - do_print(`Testing API validation with isChromeCompat=${isChromeCompat}`); - testnamespace = generateAPIs({ - isChromeCompat, - }, { - one_required() {}, - one_optional() {}, - async_required() {}, - async_optional() {}, - }); - - assertThrows("one_required"); - assertThrows("one_required", null); - assertNoThrows("one_required", cb); - assertThrows("one_required", cb, null); - assertThrows("one_required", cb, cb); - - assertNoThrows("one_optional"); - assertNoThrows("one_optional", null); - assertNoThrows("one_optional", cb); - assertThrows("one_optional", cb, null); - assertThrows("one_optional", cb, cb); - - // Schema-based validation happens before an async method is called, so - // errors should be thrown synchronously. - - // The parameter was declared as required, but there was also an "async" - // attribute with the same value as the parameter name, so the callback - // parameter is actually optional. - assertNoThrows("async_required"); - assertNoThrows("async_required", null); - assertNoThrows("async_required", cb); - assertThrows("async_required", cb, null); - assertThrows("async_required", cb, cb); - - assertNoThrows("async_optional"); - assertNoThrows("async_optional", null); - assertNoThrows("async_optional", cb); - assertThrows("async_optional", cb, null); - assertThrows("async_optional", cb, cb); - } -}); - -add_task(function* testAsyncResults() { - yield Schemas.load("data:," + JSON.stringify(schemaJson)); - function* runWithCallback(func) { - do_print(`Calling testnamespace.${func.name}, expecting callback with result`); - return yield new Promise(resolve => { - let result = "uninitialized value"; - let returnValue = func(reply => { - result = reply; - resolve(result); - }); - // When a callback is given, the return value must be missing. - do_check_eq(returnValue, undefined); - // Callback must be called asynchronously. - do_check_eq(result, "uninitialized value"); - }); - } - - function* runFailCallback(func) { - do_print(`Calling testnamespace.${func.name}, expecting callback with error`); - return yield new Promise(resolve => { - func(reply => { - do_check_eq(reply, undefined); - resolve(context.lastError.message); // eslint-disable-line no-undef - }); - }); - } - - for (let isChromeCompat of [true, false]) { - do_print(`Testing API invocation with isChromeCompat=${isChromeCompat}`); - let testnamespace = generateAPIs({ - isChromeCompat, - }, { - async_required(cb) { - do_check_eq(cb, undefined); - return Promise.resolve(1); - }, - async_optional(cb) { - do_check_eq(cb, undefined); - return Promise.resolve(2); - }, - }); - if (!isChromeCompat) { // No promises for chrome. - do_print("testnamespace.async_required should be a Promise"); - let promise = testnamespace.async_required(); - do_check_true(promise instanceof context.cloneScope.Promise); - do_check_eq(yield promise, 1); - - do_print("testnamespace.async_optional should be a Promise"); - promise = testnamespace.async_optional(); - do_check_true(promise instanceof context.cloneScope.Promise); - do_check_eq(yield promise, 2); - } - - do_check_eq(yield* runWithCallback(testnamespace.async_required), 1); - do_check_eq(yield* runWithCallback(testnamespace.async_optional), 2); - - let otherSandbox = Cu.Sandbox(null, {}); - let errorFactories = [ - msg => { throw new context.cloneScope.Error(msg); }, - msg => context.cloneScope.Promise.reject({message: msg}), - msg => Cu.evalInSandbox(`throw new Error("${msg}")`, otherSandbox), - msg => Cu.evalInSandbox(`Promise.reject({message: "${msg}"})`, otherSandbox), - ]; - for (let makeError of errorFactories) { - do_print(`Testing callback/promise with error caused by: ${makeError}`); - testnamespace = generateAPIs({ - isChromeCompat, - }, { - async_required() { return makeError("ONE"); }, - async_optional() { return makeError("TWO"); }, - }); - - if (!isChromeCompat) { // No promises for chrome. - yield Assert.rejects(testnamespace.async_required(), /ONE/, - "should reject testnamespace.async_required()").catch(() => {}); - yield Assert.rejects(testnamespace.async_optional(), /TWO/, - "should reject testnamespace.async_optional()").catch(() => {}); - } - - do_check_eq(yield* runFailCallback(testnamespace.async_required), "ONE"); - do_check_eq(yield* runFailCallback(testnamespace.async_optional), "TWO"); - } - } -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_simple.js b/toolkit/components/webextensions/test/xpcshell/test_ext_simple.js deleted file mode 100644 index 91b10354c..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_simple.js +++ /dev/null @@ -1,69 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -add_task(function* test_simple() { - let extensionData = { - manifest: { - "name": "Simple extension test", - "version": "1.0", - "manifest_version": 2, - "description": "", - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.unload(); -}); - -add_task(function* test_background() { - function background() { - browser.test.log("running background script"); - - browser.test.onMessage.addListener((x, y) => { - browser.test.assertEq(x, 10, "x is 10"); - browser.test.assertEq(y, 20, "y is 20"); - - browser.test.notifyPass("background test passed"); - }); - - browser.test.sendMessage("running", 1); - } - - let extensionData = { - background, - manifest: { - "name": "Simple extension test", - "version": "1.0", - "manifest_version": 2, - "description": "", - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]); - equal(x, 1, "got correct value from extension"); - - extension.sendMessage(10, 20); - yield extension.awaitFinish(); - yield extension.unload(); -}); - -add_task(function* test_extensionTypes() { - let extensionData = { - background: function() { - browser.test.assertEq(typeof browser.extensionTypes, "object", "browser.extensionTypes exists"); - browser.test.assertEq(typeof browser.extensionTypes.RunAt, "object", "browser.extensionTypes.RunAt exists"); - browser.test.notifyPass("extentionTypes test passed"); - }, - }; - - let extension = ExtensionTestUtils.loadExtension(extensionData); - - yield extension.startup(); - yield extension.awaitFinish(); - yield extension.unload(); -}); - diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_storage.js b/toolkit/components/webextensions/test/xpcshell/test_ext_storage.js deleted file mode 100644 index df46dfb63..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_storage.js +++ /dev/null @@ -1,334 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled"; -Cu.import("resource://gre/modules/Preferences.jsm"); - -/** - * Utility function to ensure that all supported APIs for getting are - * tested. - * - * @param {string} areaName - * either "local" or "sync" according to what we want to test - * @param {string} prop - * "key" to look up using the storage API - * @param {Object} value - * "value" to compare against - */ -async function checkGetImpl(areaName, prop, value) { - let storage = browser.storage[areaName]; - - let data = await storage.get(null); - browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`); - - data = await storage.get(prop); - browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`); - - data = await storage.get([prop]); - browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`); - - data = await storage.get({[prop]: undefined}); - browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`); -} - -add_task(function* test_local_cache_invalidation() { - function background(checkGet) { - browser.test.onMessage.addListener(async msg => { - if (msg === "set-initial") { - await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"}); - browser.test.sendMessage("set-initial-done"); - } else if (msg === "check") { - await checkGet("local", "test-prop1", "value1"); - await checkGet("local", "test-prop2", "value2"); - browser.test.sendMessage("check-done"); - } - }); - - browser.test.sendMessage("ready"); - } - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["storage"], - }, - background: `(${background})(${checkGetImpl})`, - }); - - yield extension.startup(); - yield extension.awaitMessage("ready"); - - extension.sendMessage("set-initial"); - yield extension.awaitMessage("set-initial-done"); - - Services.obs.notifyObservers(null, "extension-invalidate-storage-cache", ""); - - extension.sendMessage("check"); - yield extension.awaitMessage("check-done"); - - yield extension.unload(); -}); - -add_task(function* test_config_flag_needed() { - function background() { - let promises = []; - let apiTests = [ - {method: "get", args: ["foo"]}, - {method: "set", args: [{foo: "bar"}]}, - {method: "remove", args: ["foo"]}, - {method: "clear", args: []}, - ]; - apiTests.forEach(testDef => { - promises.push(browser.test.assertRejects( - browser.storage.sync[testDef.method](...testDef.args), - "Please set webextensions.storage.sync.enabled to true in about:config", - `storage.sync.${testDef.method} is behind a flag`)); - }); - - Promise.all(promises).then(() => browser.test.notifyPass("flag needed")); - } - - ok(!Preferences.get(STORAGE_SYNC_PREF)); - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["storage"], - }, - background: `(${background})(${checkGetImpl})`, - }); - - yield extension.startup(); - yield extension.awaitFinish("flag needed"); - yield extension.unload(); -}); - -add_task(function* test_reloading_extensions_works() { - // Just some random extension ID that we can re-use - const extensionId = "my-extension-id@1"; - - function loadExtension() { - function background() { - browser.storage.sync.set({"a": "b"}).then(() => { - browser.test.notifyPass("set-works"); - }); - } - - return ExtensionTestUtils.loadExtension({ - manifest: { - permissions: ["storage"], - }, - background: `(${background})()`, - }, extensionId); - } - - Preferences.set(STORAGE_SYNC_PREF, true); - - let extension1 = loadExtension(); - - yield extension1.startup(); - yield extension1.awaitFinish("set-works"); - yield extension1.unload(); - - let extension2 = loadExtension(); - - yield extension2.startup(); - yield extension2.awaitFinish("set-works"); - yield extension2.unload(); - - Preferences.reset(STORAGE_SYNC_PREF); -}); - -do_register_cleanup(() => { - Preferences.reset(STORAGE_SYNC_PREF); -}); - -add_task(function* test_backgroundScript() { - async function backgroundScript(checkGet) { - let globalChanges, gResolve; - function clearGlobalChanges() { - globalChanges = new Promise(resolve => { gResolve = resolve; }); - } - clearGlobalChanges(); - let expectedAreaName; - - browser.storage.onChanged.addListener((changes, areaName) => { - browser.test.assertEq(expectedAreaName, areaName, - "Expected area name received by listener"); - gResolve(changes); - }); - - async function checkChanges(areaName, changes, message) { - function checkSub(obj1, obj2) { - for (let prop in obj1) { - browser.test.assertTrue(obj1[prop] !== undefined, - `checkChanges ${areaName} ${prop} is missing (${message})`); - browser.test.assertTrue(obj2[prop] !== undefined, - `checkChanges ${areaName} ${prop} is missing (${message})`); - browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue, - `checkChanges ${areaName} ${prop} old (${message})`); - browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue, - `checkChanges ${areaName} ${prop} new (${message})`); - } - } - - const recentChanges = await globalChanges; - checkSub(changes, recentChanges); - checkSub(recentChanges, changes); - clearGlobalChanges(); - } - - /* eslint-disable dot-notation */ - async function runTests(areaName) { - expectedAreaName = areaName; - let storage = browser.storage[areaName]; - // Set some data and then test getters. - try { - await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); - await checkChanges(areaName, - {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}}, - "set (a)"); - - await checkGet(areaName, "test-prop1", "value1"); - await checkGet(areaName, "test-prop2", "value2"); - - let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"}); - browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)"); - browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)"); - browser.test.assertEq("default", data["other"], "other correct"); - - data = await storage.get(["test-prop1", "test-prop2", "other"]); - browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)"); - browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)"); - browser.test.assertFalse("other" in data, "other correct"); - - // Remove data in various ways. - await storage.remove("test-prop1"); - await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string"); - - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)"); - browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)"); - - await storage.set({"test-prop1": "value1"}); - await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)"); - - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)"); - browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)"); - - await storage.remove(["test-prop1", "test-prop2"]); - await checkChanges(areaName, - {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, - "remove array"); - - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)"); - browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)"); - - // test storage.clear - await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); - // Make sure that set() handler happened before we clear the - // promise again. - await globalChanges; - - clearGlobalChanges(); - await storage.clear(); - - await checkChanges(areaName, - {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}}, - "clear"); - data = await storage.get(["test-prop1", "test-prop2"]); - browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)"); - browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)"); - - // Make sure we can store complex JSON data. - // known previous values - await storage.set({"test-prop1": "value1", "test-prop2": "value2"}); - - // Make sure the set() handler landed. - await globalChanges; - - clearGlobalChanges(); - await storage.set({ - "test-prop1": { - str: "hello", - bool: true, - null: null, - undef: undefined, - obj: {}, - arr: [1, 2], - date: new Date(0), - regexp: /regexp/, - func: function func() {}, - window, - }, - }); - - await storage.set({"test-prop2": function func() {}}); - const recentChanges = await globalChanges; - - browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct"); - browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj"); - clearGlobalChanges(); - - data = await storage.get({"test-prop1": undefined, "test-prop2": undefined}); - let obj = data["test-prop1"]; - - browser.test.assertEq("hello", obj.str, "string part correct"); - browser.test.assertEq(true, obj.bool, "bool part correct"); - browser.test.assertEq(null, obj.null, "null part correct"); - browser.test.assertEq(undefined, obj.undef, "undefined part correct"); - browser.test.assertEq(undefined, obj.func, "function part correct"); - browser.test.assertEq(undefined, obj.window, "window part correct"); - browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct"); - browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct"); - browser.test.assertEq("object", typeof(obj.obj), "object part correct"); - browser.test.assertTrue(Array.isArray(obj.arr), "array part present"); - browser.test.assertEq(1, obj.arr[0], "arr[0] part correct"); - browser.test.assertEq(2, obj.arr[1], "arr[1] part correct"); - browser.test.assertEq(2, obj.arr.length, "arr.length part correct"); - - obj = data["test-prop2"]; - - browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object"); - browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object"); - } catch (e) { - browser.test.fail(`Error: ${e} :: ${e.stack}`); - browser.test.notifyFail("storage"); - } - } - - browser.test.onMessage.addListener(msg => { - let promise; - if (msg === "test-local") { - promise = runTests("local"); - } else if (msg === "test-sync") { - promise = runTests("sync"); - } - promise.then(() => browser.test.sendMessage("test-finished")); - }); - - browser.test.sendMessage("ready"); - } - - let extensionData = { - background: `(${backgroundScript})(${checkGetImpl})`, - manifest: { - permissions: ["storage"], - }, - }; - - Preferences.set(STORAGE_SYNC_PREF, true); - - let extension = ExtensionTestUtils.loadExtension(extensionData); - yield extension.startup(); - yield extension.awaitMessage("ready"); - - extension.sendMessage("test-local"); - yield extension.awaitMessage("test-finished"); - - extension.sendMessage("test-sync"); - yield extension.awaitMessage("test-finished"); - - Preferences.reset(STORAGE_SYNC_PREF); - yield extension.unload(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_topSites.js b/toolkit/components/webextensions/test/xpcshell/test_ext_topSites.js deleted file mode 100644 index eb3f552ed..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_ext_topSites.js +++ /dev/null @@ -1,85 +0,0 @@ -"use strict"; - -Cu.import("resource://gre/modules/NewTabUtils.jsm"); - - -function TestProvider(getLinksFn) { - this.getLinks = getLinksFn; - this._observers = new Set(); -} - -TestProvider.prototype = { - addObserver: function(observer) { - this._observers.add(observer); - }, - notifyLinkChanged: function(link, index = -1, deleted = false) { - this._notifyObservers("onLinkChanged", link, index, deleted); - }, - notifyManyLinksChanged: function() { - this._notifyObservers("onManyLinksChanged"); - }, - _notifyObservers: function(observerMethodName, ...args) { - args.unshift(this); - for (let obs of this._observers) { - if (obs[observerMethodName]) { - obs[observerMethodName].apply(NewTabUtils.links, args); - } - } - }, -}; - -function makeLinks(links) { - // Important: To avoid test failures due to clock jitter on Windows XP, call - // Date.now() once here, not each time through the loop. - let frecency = 0; - let now = Date.now() * 1000; - let places = []; - links.map((link, i) => { - places.push({ - url: link.url, - title: link.title, - lastVisitDate: now - i, - frecency: frecency++, - }); - }); - return places; -} - -add_task(function* test_topSites() { - let expect = [{url: "http://example.com/", title: "site#-1"}, - {url: "http://example0.com/", title: "site#0"}, - {url: "http://example1.com/", title: "site#1"}, - {url: "http://example2.com/", title: "site#2"}, - {url: "http://example3.com/", title: "site#3"}]; - - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - "permissions": [ - "topSites", - ], - }, - background() { - browser.topSites.get(result => { - browser.test.sendMessage("done", result); - }); - }, - }); - - - let expectedLinks = makeLinks(expect); - let provider = new TestProvider(done => done(expectedLinks)); - - NewTabUtils.initWithoutProviders(); - NewTabUtils.links.addProvider(provider); - - yield NewTabUtils.links.populateCache(); - - yield extension.startup(); - - let result = yield extension.awaitMessage("done"); - Assert.deepEqual(expect, result, "got topSites"); - - yield extension.unload(); - - NewTabUtils.links.removeProvider(provider); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_getAPILevelForWindow.js b/toolkit/components/webextensions/test/xpcshell/test_getAPILevelForWindow.js deleted file mode 100644 index 68741a6cc..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_getAPILevelForWindow.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; - -Cu.import("resource://gre/modules/ExtensionManagement.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -function createWindowWithAddonId(addonId) { - let baseURI = Services.io.newURI("about:blank", null, null); - let originAttributes = {addonId}; - let principal = Services.scriptSecurityManager - .createCodebasePrincipal(baseURI, originAttributes); - let chromeNav = Services.appShell.createWindowlessBrowser(true); - let interfaceRequestor = chromeNav.QueryInterface(Ci.nsIInterfaceRequestor); - let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell); - docShell.createAboutBlankContentViewer(principal); - - return {chromeNav, window: docShell.contentViewer.DOMDocument.defaultView}; -} - -add_task(function* test_eventpages() { - const {getAPILevelForWindow, getAddonIdForWindow} = ExtensionManagement; - const {NO_PRIVILEGES, FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS; - const FAKE_ADDON_ID = "fakeAddonId"; - const OTHER_ADDON_ID = "otherFakeAddonId"; - const EMPTY_ADDON_ID = ""; - - let fakeAddonId = createWindowWithAddonId(FAKE_ADDON_ID); - equal(getAddonIdForWindow(fakeAddonId.window), FAKE_ADDON_ID, - "the window has the expected addonId"); - - let apiLevel = getAPILevelForWindow(fakeAddonId.window, FAKE_ADDON_ID); - equal(apiLevel, FULL_PRIVILEGES, - "apiLevel for the window with the right addonId should be FULL_PRIVILEGES"); - - apiLevel = getAPILevelForWindow(fakeAddonId.window, OTHER_ADDON_ID); - equal(apiLevel, NO_PRIVILEGES, - "apiLevel for the window with a different addonId should be NO_PRIVILEGES"); - - fakeAddonId.chromeNav.close(); - - // NOTE: check that window with an empty addon Id (which are window that are - // not Extensions pages) always get no WebExtensions APIs. - let emptyAddonId = createWindowWithAddonId(EMPTY_ADDON_ID); - equal(getAddonIdForWindow(emptyAddonId.window), EMPTY_ADDON_ID, - "the window has the expected addonId"); - - apiLevel = getAPILevelForWindow(emptyAddonId.window, EMPTY_ADDON_ID); - equal(apiLevel, NO_PRIVILEGES, - "apiLevel for empty addonId should be NO_PRIVILEGES"); - - apiLevel = getAPILevelForWindow(emptyAddonId.window, OTHER_ADDON_ID); - equal(apiLevel, NO_PRIVILEGES, - "apiLevel for an 'empty addonId' window should be always NO_PRIVILEGES"); - - emptyAddonId.chromeNav.close(); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_locale_converter.js b/toolkit/components/webextensions/test/xpcshell/test_locale_converter.js deleted file mode 100644 index c8b1ee92b..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_locale_converter.js +++ /dev/null @@ -1,133 +0,0 @@ -"use strict"; - -const convService = Cc["@mozilla.org/streamConverters;1"] - .getService(Ci.nsIStreamConverterService); - -const UUID = "72b61ee3-aceb-476c-be1b-0822b036c9f1"; -const ADDON_ID = "test@web.extension"; -const URI = NetUtil.newURI(`moz-extension://${UUID}/file.css`); - -const FROM_TYPE = "application/vnd.mozilla.webext.unlocalized"; -const TO_TYPE = "text/css"; - - -function StringStream(string) { - let stream = Cc["@mozilla.org/io/string-input-stream;1"] - .createInstance(Ci.nsIStringInputStream); - - stream.data = string; - return stream; -} - - -// Initialize the policy service with a stub localizer for our -// add-on ID. -add_task(function* init() { - const aps = Cc["@mozilla.org/addons/policy-service;1"] - .getService(Ci.nsIAddonPolicyService).wrappedJSObject; - - let oldCallback = aps.setExtensionURIToAddonIdCallback(uri => { - if (uri.host == UUID) { - return ADDON_ID; - } - }); - - aps.setAddonLocalizeCallback(ADDON_ID, string => { - return string.replace(/__MSG_(.*?)__/g, "<localized-$1>"); - }); - - do_register_cleanup(() => { - aps.setExtensionURIToAddonIdCallback(oldCallback); - aps.setAddonLocalizeCallback(ADDON_ID, null); - }); -}); - - -// Test that the synchronous converter works as expected with a -// simple string. -add_task(function* testSynchronousConvert() { - let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz"); - - let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI); - - let result = NetUtil.readInputStreamToString(resultStream, resultStream.available()); - - equal(result, "Foo <localized-xxx> bar <localized-yyy> baz"); -}); - - -// Test that the asynchronous converter works as expected with input -// split into multiple chunks, and a boundary in the middle of a -// replacement token. -add_task(function* testAsyncConvert() { - let listener; - let awaitResult = new Promise((resolve, reject) => { - listener = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]), - - onDataAvailable(request, context, inputStream, offset, count) { - this.resultParts.push(NetUtil.readInputStreamToString(inputStream, count)); - }, - - onStartRequest() { - ok(!("resultParts" in this)); - this.resultParts = []; - }, - - onStopRequest(request, context, statusCode) { - if (!Components.isSuccessCode(statusCode)) { - reject(new Error(statusCode)); - } - - resolve(this.resultParts.join("\n")); - }, - }; - }); - - let parts = ["Foo __MSG_x", "xx__ bar __MSG_yyy__ baz"]; - - let converter = convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, URI); - converter.onStartRequest(null, null); - - for (let part of parts) { - converter.onDataAvailable(null, null, StringStream(part), 0, part.length); - } - - converter.onStopRequest(null, null, Cr.NS_OK); - - - let result = yield awaitResult; - equal(result, "Foo <localized-xxx> bar <localized-yyy> baz"); -}); - - -// Test that attempting to initialize a converter with the URI of a -// nonexistent WebExtension fails. -add_task(function* testInvalidUUID() { - let uri = NetUtil.newURI("moz-extension://eb4f3be8-41c9-4970-aa6d-b84d1ecc02b2/file.css"); - let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz"); - - // Assert.throws raise a TypeError exception when the expected param - // is an arrow function. (See Bug 1237961 for rationale) - let expectInvalidContextException = function(e) { - return e.result === Cr.NS_ERROR_INVALID_ARG && /Invalid context/.test(e); - }; - - Assert.throws(() => { - convService.convert(stream, FROM_TYPE, TO_TYPE, uri); - }, expectInvalidContextException); - - Assert.throws(() => { - let listener = {QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])}; - - convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, uri); - }, expectInvalidContextException); -}); - - -// Test that an empty stream does not throw an NS_ERROR_ILLEGAL_VALUE. -add_task(function* testEmptyStream() { - let stream = StringStream(""); - let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI); - equal(resultStream.data, ""); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_locale_data.js b/toolkit/components/webextensions/test/xpcshell/test_locale_data.js deleted file mode 100644 index c3cd44e57..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_locale_data.js +++ /dev/null @@ -1,130 +0,0 @@ -"use strict"; - -Cu.import("resource://gre/modules/Extension.jsm"); - -/* globals ExtensionData */ - -const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); - -function* generateAddon(data) { - let id = uuidGenerator.generateUUID().number; - - data = Object.assign({embedded: true}, data); - data.manifest = Object.assign({applications: {gecko: {id}}}, data.manifest); - - let xpi = Extension.generateXPI(data); - do_register_cleanup(() => { - Services.obs.notifyObservers(xpi, "flush-cache-entry", null); - xpi.remove(false); - }); - - let fileURI = Services.io.newFileURI(xpi); - let jarURI = NetUtil.newURI(`jar:${fileURI.spec}!/webextension/`); - - let extension = new ExtensionData(jarURI); - yield extension.readManifest(); - - return extension; -} - -add_task(function* testMissingDefaultLocale() { - let extension = yield generateAddon({ - "files": { - "_locales/en_US/messages.json": {}, - }, - }); - - equal(extension.errors.length, 0, "No errors reported"); - - yield extension.initAllLocales(); - - equal(extension.errors.length, 1, "One error reported"); - - do_print(`Got error: ${extension.errors[0]}`); - - ok(extension.errors[0].includes('"default_locale" property is required'), - "Got missing default_locale error"); -}); - - -add_task(function* testInvalidDefaultLocale() { - let extension = yield generateAddon({ - "manifest": { - "default_locale": "en", - }, - - "files": { - "_locales/en_US/messages.json": {}, - }, - }); - - equal(extension.errors.length, 1, "One error reported"); - - do_print(`Got error: ${extension.errors[0]}`); - - ok(extension.errors[0].includes("Loading locale file _locales/en/messages.json"), - "Got invalid default_locale error"); - - yield extension.initAllLocales(); - - equal(extension.errors.length, 2, "Two errors reported"); - - do_print(`Got error: ${extension.errors[1]}`); - - ok(extension.errors[1].includes('"default_locale" property must correspond'), - "Got invalid default_locale error"); -}); - - -add_task(function* testUnexpectedDefaultLocale() { - let extension = yield generateAddon({ - "manifest": { - "default_locale": "en_US", - }, - }); - - equal(extension.errors.length, 1, "One error reported"); - - do_print(`Got error: ${extension.errors[0]}`); - - ok(extension.errors[0].includes("Loading locale file _locales/en-US/messages.json"), - "Got invalid default_locale error"); - - yield extension.initAllLocales(); - - equal(extension.errors.length, 2, "One error reported"); - - do_print(`Got error: ${extension.errors[1]}`); - - ok(extension.errors[1].includes('"default_locale" property must correspond'), - "Got unexpected default_locale error"); -}); - - -add_task(function* testInvalidSyntax() { - let extension = yield generateAddon({ - "manifest": { - "default_locale": "en_US", - }, - - "files": { - "_locales/en_US/messages.json": '{foo: {message: "bar", description: "baz"}}', - }, - }); - - equal(extension.errors.length, 1, "No errors reported"); - - do_print(`Got error: ${extension.errors[0]}`); - - ok(extension.errors[0].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"), - "Got syntax error"); - - yield extension.initAllLocales(); - - equal(extension.errors.length, 2, "One error reported"); - - do_print(`Got error: ${extension.errors[1]}`); - - ok(extension.errors[1].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"), - "Got syntax error"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_native_messaging.js b/toolkit/components/webextensions/test/xpcshell/test_native_messaging.js deleted file mode 100644 index 1fcb7799e..000000000 --- a/toolkit/components/webextensions/test/xpcshell/test_native_messaging.js +++ /dev/null @@ -1,302 +0,0 @@ -"use strict"; - -/* global OS, HostManifestManager, NativeApp */ -Cu.import("resource://gre/modules/AppConstants.jsm"); -Cu.import("resource://gre/modules/AsyncShutdown.jsm"); -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); -Cu.import("resource://gre/modules/FileUtils.jsm"); -Cu.import("resource://gre/modules/Schemas.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm"); -Cu.import("resource://gre/modules/NativeMessaging.jsm"); -Cu.import("resource://gre/modules/osfile.jsm"); - -let registry = null; -if (AppConstants.platform == "win") { - Cu.import("resource://testing-common/MockRegistry.jsm"); - registry = new MockRegistry(); - do_register_cleanup(() => { - registry.shutdown(); - }); -} - -const REGPATH = "Software\\Mozilla\\NativeMessagingHosts"; - -const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json"; - -let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]); -dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - -let userDir = dir.clone(); -userDir.append("user"); -userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - -let globalDir = dir.clone(); -globalDir.append("global"); -globalDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - -let dirProvider = { - getFile(property) { - if (property == "XREUserNativeMessaging") { - return userDir.clone(); - } else if (property == "XRESysNativeMessaging") { - return globalDir.clone(); - } - return null; - }, -}; - -Services.dirsvc.registerProvider(dirProvider); - -do_register_cleanup(() => { - Services.dirsvc.unregisterProvider(dirProvider); - dir.remove(true); -}); - -function writeManifest(path, manifest) { - if (typeof manifest != "string") { - manifest = JSON.stringify(manifest); - } - return OS.File.writeAtomic(path, manifest); -} - -let PYTHON; -add_task(function* setup() { - yield Schemas.load(BASE_SCHEMA); - - PYTHON = yield Subprocess.pathSearch("python2.7"); - if (PYTHON == null) { - PYTHON = yield Subprocess.pathSearch("python"); - } - notEqual(PYTHON, null, "Found a suitable python interpreter"); -}); - -let global = this; - -// Test of HostManifestManager.lookupApplication() begin here... -let context = { - url: null, - jsonStringify(...args) { return JSON.stringify(...args); }, - cloneScope: global, - logError() {}, - preprocessors: {}, - callOnClose: () => {}, - forgetOnClose: () => {}, -}; - -class MockContext extends ExtensionCommon.BaseContext { - constructor(extensionId) { - let fakeExtension = {id: extensionId}; - super("testEnv", fakeExtension); - this.sandbox = Cu.Sandbox(global); - } - - get cloneScope() { - return global; - } - - get principal() { - return Cu.getObjectPrincipal(this.sandbox); - } -} - -let templateManifest = { - name: "test", - description: "this is only a test", - path: "/bin/cat", - type: "stdio", - allowed_extensions: ["extension@tests.mozilla.org"], -}; - -add_task(function* test_nonexistent_manifest() { - let result = yield HostManifestManager.lookupApplication("test", context); - equal(result, null, "lookupApplication returns null for non-existent application"); -}); - -const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json"); - -add_task(function* test_good_manifest() { - yield writeManifest(USER_TEST_JSON, templateManifest); - if (registry) { - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - `${REGPATH}\\test`, "", USER_TEST_JSON); - } - - let result = yield HostManifestManager.lookupApplication("test", context); - notEqual(result, null, "lookupApplication finds a good manifest"); - equal(result.path, USER_TEST_JSON, "lookupApplication returns the correct path"); - deepEqual(result.manifest, templateManifest, "lookupApplication returns the manifest contents"); -}); - -add_task(function* test_invalid_json() { - yield writeManifest(USER_TEST_JSON, "this is not valid json"); - let result = yield HostManifestManager.lookupApplication("test", context); - equal(result, null, "lookupApplication ignores bad json"); -}); - -add_task(function* test_invalid_name() { - let manifest = Object.assign({}, templateManifest); - manifest.name = "../test"; - yield writeManifest(USER_TEST_JSON, manifest); - let result = yield HostManifestManager.lookupApplication("test", context); - equal(result, null, "lookupApplication ignores an invalid name"); -}); - -add_task(function* test_name_mismatch() { - let manifest = Object.assign({}, templateManifest); - manifest.name = "not test"; - yield writeManifest(USER_TEST_JSON, manifest); - let result = yield HostManifestManager.lookupApplication("test", context); - let what = (AppConstants.platform == "win") ? "registry key" : "json filename"; - equal(result, null, `lookupApplication ignores mistmatch between ${what} and name property`); -}); - -add_task(function* test_missing_props() { - const PROPS = [ - "name", - "description", - "path", - "type", - "allowed_extensions", - ]; - for (let prop of PROPS) { - let manifest = Object.assign({}, templateManifest); - delete manifest[prop]; - - yield writeManifest(USER_TEST_JSON, manifest); - let result = yield HostManifestManager.lookupApplication("test", context); - equal(result, null, `lookupApplication ignores missing ${prop}`); - } -}); - -add_task(function* test_invalid_type() { - let manifest = Object.assign({}, templateManifest); - manifest.type = "bogus"; - yield writeManifest(USER_TEST_JSON, manifest); - let result = yield HostManifestManager.lookupApplication("test", context); - equal(result, null, "lookupApplication ignores invalid type"); -}); - -add_task(function* test_no_allowed_extensions() { - let manifest = Object.assign({}, templateManifest); - manifest.allowed_extensions = []; - yield writeManifest(USER_TEST_JSON, manifest); - let result = yield HostManifestManager.lookupApplication("test", context); - equal(result, null, "lookupApplication ignores manifest with no allowed_extensions"); -}); - -const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json"); -let globalManifest = Object.assign({}, templateManifest); -globalManifest.description = "This manifest is from the systemwide directory"; - -add_task(function* good_manifest_system_dir() { - yield OS.File.remove(USER_TEST_JSON); - yield writeManifest(GLOBAL_TEST_JSON, globalManifest); - if (registry) { - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - `${REGPATH}\\test`, "", null); - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, - `${REGPATH}\\test`, "", GLOBAL_TEST_JSON); - } - - let where = (AppConstants.platform == "win") ? "registry location" : "directory"; - let result = yield HostManifestManager.lookupApplication("test", context); - notEqual(result, null, `lookupApplication finds a manifest in the system-wide ${where}`); - equal(result.path, GLOBAL_TEST_JSON, `lookupApplication returns path in the system-wide ${where}`); - deepEqual(result.manifest, globalManifest, `lookupApplication returns manifest contents from the system-wide ${where}`); -}); - -add_task(function* test_user_dir_precedence() { - yield writeManifest(USER_TEST_JSON, templateManifest); - if (registry) { - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - `${REGPATH}\\test`, "", USER_TEST_JSON); - } - // global test.json and LOCAL_MACHINE registry key on windows are - // still present from the previous test - - let result = yield HostManifestManager.lookupApplication("test", context); - notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide locations"); - equal(result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist"); - deepEqual(result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist"); -}); - -// Test shutdown handling in NativeApp -add_task(function* test_native_app_shutdown() { - const SCRIPT = String.raw` -import signal -import struct -import sys - -signal.signal(signal.SIGTERM, signal.SIG_IGN) - -while True: - rawlen = sys.stdin.read(4) - if len(rawlen) == 0: - signal.pause() - msglen = struct.unpack('@I', rawlen)[0] - msg = sys.stdin.read(msglen) - - sys.stdout.write(struct.pack('@I', msglen)) - sys.stdout.write(msg) - `; - - let scriptPath = OS.Path.join(userDir.path, "wontdie.py"); - let manifestPath = OS.Path.join(userDir.path, "wontdie.json"); - - const ID = "native@tests.mozilla.org"; - let manifest = { - name: "wontdie", - description: "test async shutdown of native apps", - type: "stdio", - allowed_extensions: [ID], - }; - - if (AppConstants.platform == "win") { - yield OS.File.writeAtomic(scriptPath, SCRIPT); - - let batPath = OS.Path.join(userDir.path, "wontdie.bat"); - let batBody = `@ECHO OFF\n${PYTHON} -u "${scriptPath}" %*\n`; - yield OS.File.writeAtomic(batPath, batBody); - yield OS.File.setPermissions(batPath, {unixMode: 0o755}); - - manifest.path = batPath; - yield writeManifest(manifestPath, manifest); - - registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - `${REGPATH}\\wontdie`, "", manifestPath); - } else { - yield OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`); - yield OS.File.setPermissions(scriptPath, {unixMode: 0o755}); - manifest.path = scriptPath; - yield writeManifest(manifestPath, manifest); - } - - let mockContext = new MockContext(ID); - let app = new NativeApp(mockContext, "wontdie"); - - // send a message and wait for the reply to make sure the app is running - let MSG = "test"; - let recvPromise = new Promise(resolve => { - let listener = (what, msg) => { - equal(msg, MSG, "Received test message"); - app.off("message", listener); - resolve(); - }; - app.on("message", listener); - }); - - let buffer = NativeApp.encodeMessage(mockContext, MSG); - app.send(buffer); - yield recvPromise; - - app._cleanup(); - - do_print("waiting for async shutdown"); - Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); - AsyncShutdown.profileBeforeChange._trigger(); - Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); - - let procs = yield SubprocessImpl.Process.getWorker().call("getProcesses", []); - equal(procs.size, 0, "native process exited"); -}); diff --git a/toolkit/components/webextensions/test/xpcshell/xpcshell.ini b/toolkit/components/webextensions/test/xpcshell/xpcshell.ini deleted file mode 100644 index d2c6fd5d0..000000000 --- a/toolkit/components/webextensions/test/xpcshell/xpcshell.ini +++ /dev/null @@ -1,69 +0,0 @@ -[DEFAULT] -head = head.js -tail = -firefox-appdir = browser -skip-if = appname == "thunderbird" -support-files = - data/** head_sync.js -tags = webextensions - -[test_csp_custom_policies.js] -[test_csp_validator.js] -[test_ext_alarms.js] -[test_ext_alarms_does_not_fire.js] -[test_ext_alarms_periodic.js] -[test_ext_alarms_replaces.js] -[test_ext_apimanager.js] -[test_ext_api_permissions.js] -[test_ext_background_generated_load_events.js] -[test_ext_background_generated_reload.js] -[test_ext_background_global_history.js] -skip-if = os == "android" # Android does not use Places for history. -[test_ext_background_private_browsing.js] -[test_ext_background_runtime_connect_params.js] -[test_ext_background_sub_windows.js] -[test_ext_background_window_properties.js] -skip-if = os == "android" -[test_ext_contexts.js] -[test_ext_downloads.js] -[test_ext_downloads_download.js] -skip-if = os == "android" -[test_ext_downloads_misc.js] -skip-if = os == "android" -[test_ext_downloads_search.js] -skip-if = os == "android" -[test_ext_experiments.js] -skip-if = release_or_beta -[test_ext_extension.js] -[test_ext_idle.js] -[test_ext_json_parser.js] -[test_ext_localStorage.js] -[test_ext_management.js] -[test_ext_management_uninstall_self.js] -[test_ext_manifest_content_security_policy.js] -[test_ext_manifest_incognito.js] -[test_ext_manifest_minimum_chrome_version.js] -[test_ext_onmessage_removelistener.js] -[test_ext_runtime_connect_no_receiver.js] -[test_ext_runtime_getBrowserInfo.js] -[test_ext_runtime_getPlatformInfo.js] -[test_ext_runtime_onInstalled_and_onStartup.js] -[test_ext_runtime_sendMessage.js] -[test_ext_runtime_sendMessage_errors.js] -[test_ext_runtime_sendMessage_no_receiver.js] -[test_ext_runtime_sendMessage_self.js] -[test_ext_schemas.js] -[test_ext_schemas_api_injection.js] -[test_ext_schemas_async.js] -[test_ext_schemas_allowed_contexts.js] -[test_ext_simple.js] -[test_ext_storage.js] -[test_ext_topSites.js] -skip-if = os == "android" -[test_getAPILevelForWindow.js] -[test_ext_legacy_extension_context.js] -[test_ext_legacy_extension_embedding.js] -[test_locale_converter.js] -[test_locale_data.js] -[test_native_messaging.js] -skip-if = os == "android" diff --git a/toolkit/content/widgets/browser.xml b/toolkit/content/widgets/browser.xml index a30ff1c43..9c72e86d8 100644 --- a/toolkit/content/widgets/browser.xml +++ b/toolkit/content/widgets/browser.xml @@ -43,15 +43,11 @@ <parameter name="fn"/> <body> <![CDATA[ - if (!this.isRemoteBrowser) { - this.inLoadURI = true; - try { - fn(); - } finally { - this.inLoadURI = false; - } - } else { + this.inLoadURI = true; + try { fn(); + } finally { + this.inLoadURI = false; } ]]> </body> @@ -307,7 +303,7 @@ <method name="preserveLayers"> <parameter name="preserve"/> <body> - // Only useful for remote browsers. + <!-- Only useful for remote browsers. --> </body> </method> @@ -338,8 +334,9 @@ </getter> </property> + <!-- stubbed to false until all callers are removed --> <property name="isRemoteBrowser" - onget="return (this.getAttribute('remote') == 'true');" + onget="return false;" readonly="true"/> <property name="messageManager" @@ -899,6 +896,10 @@ <field name="mIconURL">null</field> + <property name="isAuthDOSProtected" + onget="return (this.getAttribute('authdosprotected') == 'true');" + readonly="true"/> + <!-- This is managed by the tabbrowser --> <field name="lastURI">null</field> @@ -916,7 +917,7 @@ os.addObserver(this, "browser:purge-session-history", true); // enable global history if we weren't told otherwise - if (!this.hasAttribute("disableglobalhistory") && !this.isRemoteBrowser) { + if (!this.hasAttribute("disableglobalhistory")) { try { this.docShell.useGlobalHistory = true; } catch (ex) { @@ -945,9 +946,7 @@ this.relatedBrowser = relatedBrowser; } - if (!this.isRemoteBrowser) { - this.addEventListener("pagehide", this.onPageHide, true); - } + this.addEventListener("pagehide", this.onPageHide, true); if (this.messageManager) { this.messageManager.addMessageListener("PopupBlocking:UpdateBlockedPopups", this); @@ -996,9 +995,7 @@ this.lastURI = null; - if (!this.isRemoteBrowser) { - this.removeEventListener("pagehide", this.onPageHide, true); - } + this.removeEventListener("pagehide", this.onPageHide, true); if (this._autoScrollNeedsCleanup) { // we polluted the global scope, so clean it up @@ -1284,9 +1281,6 @@ <parameter name="aOtherBrowser"/> <body> <![CDATA[ - if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser) - throw new Error("Can only swap docshells between browsers in the same process."); - // Give others a chance to swap state. // IMPORTANT: Since a swapDocShells call does not swap the messageManager // instances attached to a browser to aOtherBrowser, others @@ -1320,28 +1314,6 @@ "_webNavigation" ]; - if (this.isRemoteBrowser) { - fieldsToSwap.push(...[ - "_remoteWebNavigation", - "_remoteWebNavigationImpl", - "_remoteWebProgressManager", - "_remoteWebProgress", - "_remoteFinder", - "_securityUI", - "_documentURI", - "_documentContentType", - "_contentTitle", - "_characterSet", - "_contentPrincipal", - "_imageDocument", - "_fullZoom", - "_textZoom", - "_isSyntheticDocument", - "_innerWindowID", - "_manifestURI", - ]); - } - var ourFieldValues = {}; var otherFieldValues = {}; for (let field of fieldsToSwap) { @@ -1364,27 +1336,10 @@ aOtherBrowser[field] = ourFieldValues[field]; } - if (!this.isRemoteBrowser) { - // Null the current nsITypeAheadFind instances so that they're - // lazily re-created on access. We need to do this because they - // might have attached the wrong docShell. - this._fastFind = aOtherBrowser._fastFind = null; - } - else { - // Rewire the remote listeners - this._remoteWebNavigationImpl.swapBrowser(this); - aOtherBrowser._remoteWebNavigationImpl.swapBrowser(aOtherBrowser); - - if (this._remoteWebProgressManager && aOtherBrowser._remoteWebProgressManager) { - this._remoteWebProgressManager.swapBrowser(this); - aOtherBrowser._remoteWebProgressManager.swapBrowser(aOtherBrowser); - } - - if (this._remoteFinder) - this._remoteFinder.swapBrowser(this); - if (aOtherBrowser._remoteFinder) - aOtherBrowser._remoteFinder.swapBrowser(aOtherBrowser); - } + // Null the current nsITypeAheadFind instances so that they're + // lazily re-created on access. We need to do this because they + // might have attached the wrong docShell. + this._fastFind = aOtherBrowser._fastFind = null; event = new CustomEvent("EndSwapDocShells", {"detail": aOtherBrowser}); this.dispatchEvent(event); @@ -1536,11 +1491,6 @@ event.preventDefault(); } - // No need to handle "dragover" in e10s, since nsDocShellTreeOwner.cpp in the child process - // handles that case using "@mozilla.org/content/dropped-link-handler;1" service. - if (this.isRemoteBrowser) - return; - let linkHandler = Components.classes["@mozilla.org/content/dropped-link-handler;1"]. getService(Components.interfaces.nsIDroppedLinkHandler); if (linkHandler.canDropLink(event, false)) @@ -1549,9 +1499,7 @@ </handler> <handler event="drop" group="system"> <![CDATA[ - // No need to handle "drop" in e10s, since nsDocShellTreeOwner.cpp in the child process - // handles that case using "@mozilla.org/content/dropped-link-handler;1" service. - if (!this.droppedLinkHandler || event.defaultPrevented || this.isRemoteBrowser) + if (!this.droppedLinkHandler || event.defaultPrevented) return; let name = { }; diff --git a/toolkit/jetpack/moz.build b/toolkit/jetpack/moz.build index 22be010d6..6d27f8a3f 100644 --- a/toolkit/jetpack/moz.build +++ b/toolkit/jetpack/moz.build @@ -127,11 +127,6 @@ EXTRA_JS_MODULES.commonjs += [ 'test.js', ] -if CONFIG['MOZ_WEBEXTENSIONS']: - EXTRA_JS_MODULES.commonjs.sdk += [ - 'sdk/webextension.js', - ] - EXTRA_JS_MODULES.commonjs.dev += [ 'dev/debuggee.js', 'dev/frame-script.js', @@ -228,6 +223,7 @@ EXTRA_PP_JS_MODULES.commonjs.sdk += [ ] EXTRA_JS_MODULES.commonjs.sdk.addon += [ + 'sdk/addon/bootstrap.js', 'sdk/addon/events.js', 'sdk/addon/host.js', 'sdk/addon/installer.js', @@ -236,10 +232,6 @@ EXTRA_JS_MODULES.commonjs.sdk.addon += [ 'sdk/addon/window.js', ] -EXTRA_PP_JS_MODULES.commonjs.sdk.addon += [ - 'sdk/addon/bootstrap.js', -] - EXTRA_JS_MODULES.commonjs.sdk.browser += [ 'sdk/browser/events.js', ] diff --git a/toolkit/jetpack/sdk/addon/bootstrap.js b/toolkit/jetpack/sdk/addon/bootstrap.js index 6c5827f1e..a6055bd10 100644 --- a/toolkit/jetpack/sdk/addon/bootstrap.js +++ b/toolkit/jetpack/sdk/addon/bootstrap.js @@ -134,11 +134,6 @@ Bootstrap.prototype = { const main = command === "test" ? "sdk/test/runner" : null; const prefsURI = `${baseURI}defaults/preferences/prefs.js`; -#ifdef MOZ_WEBEXTENSIONS - // Init the 'sdk/webextension' module from the bootstrap addon parameter. - require("sdk/webextension").initFromBootstrapAddonParam(addon); -#endif - const { startup } = require("sdk/addon/runner"); startup(reason, {loader, main, prefsURI}); }.bind(this)).catch(error => { diff --git a/toolkit/jetpack/sdk/webextension.js b/toolkit/jetpack/sdk/webextension.js deleted file mode 100644 index d1c4385e2..000000000 --- a/toolkit/jetpack/sdk/webextension.js +++ /dev/null @@ -1,43 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -module.metadata = { - "stability": "experimental" -}; - -let webExtension; -let waitForWebExtensionAPI; - -module.exports = { - initFromBootstrapAddonParam(data) { - if (webExtension) { - throw new Error("'sdk/webextension' module has been already initialized"); - } - - webExtension = data.webExtension; - }, - - startup() { - if (!webExtension) { - return Promise.reject(new Error( - "'sdk/webextension' module is currently disabled. " + - "('hasEmbeddedWebExtension' option is missing or set to false)" - )); - } - - // NOTE: calling `startup` more than once raises an "Embedded Extension already started" - // error, but given that SDK addons are going to have access to the startup method through - // an SDK module that can be required in any part of the addon, it will be nicer if any - // additional startup calls return the startup promise instead of raising an exception, - // so that the SDK addon can access the API object in the other addon modules without the - // need to manually pass this promise around. - if (!waitForWebExtensionAPI) { - waitForWebExtensionAPI = webExtension.startup(); - } - - return waitForWebExtensionAPI; - } -}; diff --git a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd index b5086032c..febc18dfd 100644 --- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd +++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd @@ -24,13 +24,6 @@ <!ENTITY cmd.back.tooltip "Go back one page"> <!ENTITY cmd.forward.tooltip "Go forward one page"> -#ifdef MOZ_WEBEXTENSIONS -<!ENTITY showUnsignedExtensions.button.label "Some extensions could not be verified"> -<!ENTITY showAllExtensions.button.label "Show all extensions"> -<!ENTITY debugAddons.label "Debug Add-ons"> -<!ENTITY debugAddons.accesskey "B"> -#endif - <!-- global warnings --> <!ENTITY warning.safemode.label "All add-ons have been disabled by safe mode."> <!ENTITY warning.checkcompatibility.label "Add-on compatibility checking is disabled. You may have incompatible add-ons."> @@ -242,23 +235,3 @@ <!ENTITY experiment.info.changetelemetry.accesskey "T"> <!ENTITY setting.learnmore "Learn More…"> - -#ifdef MOZ_WEBEXTENSIONS -<!ENTITY disabledUnsigned.heading "Some add-ons have been disabled"> -<!-- LOCALIZATION NOTE (disabledUnsigned.description.start, disabledUnsigned.description.findAddonsLink, disabledUnsigned.description.end): - These entities form a sentence, with - disabledUnsigned.description.findAddonsLink being a link to an external site. --> -<!ENTITY disabledUnsigned.description.start "The following add-ons have not been verified for use in &brandShortName;. You can "> -<!ENTITY disabledUnsigned.description.findAddonsLink "find replacements"> -<!ENTITY disabledUnsigned.description.end " or ask the developer to get them verified."> -<!ENTITY disabledUnsigned.learnMore "Learn more about our efforts to help keep you safe online."> -<!-- LOCALIZATION NOTE (disabledUnsigned.devInfo.start, disabledUnsigned.devInfo.linkToManual, disabledUnsigned.devInfo.end): - These entities form a sentence, with disabledUnsigned.devInfo.linkToManual - being a link to an external site. --> -<!ENTITY disabledUnsigned.devInfo.start "Developers interested in getting their add-ons verified can continue by reading our "> -<!ENTITY disabledUnsigned.devInfo.linkToManual "manual"> -<!ENTITY disabledUnsigned.devInfo.end "."> - -<!ENTITY pluginDeprecation.description "Missing something? Some plugins are no longer supported by &brandShortName;."> -<!ENTITY pluginDeprecation.learnMore "Learn More."> -#endif diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index e92e10599..99c16de1d 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -107,7 +107,7 @@ locale/@AB_CD@/mozapps/downloads/settingsChange.dtd (%chrome/mozapps/downloads/settingsChange.dtd) locale/@AB_CD@/mozapps/downloads/downloads.dtd (%chrome/mozapps/downloads/downloads.dtd) locale/@AB_CD@/mozapps/downloads/downloads.properties (%chrome/mozapps/downloads/downloads.properties) -* locale/@AB_CD@/mozapps/extensions/extensions.dtd (%chrome/mozapps/extensions/extensions.dtd) + locale/@AB_CD@/mozapps/extensions/extensions.dtd (%chrome/mozapps/extensions/extensions.dtd) locale/@AB_CD@/mozapps/extensions/extensions.properties (%chrome/mozapps/extensions/extensions.properties) locale/@AB_CD@/mozapps/extensions/blocklist.dtd (%chrome/mozapps/extensions/blocklist.dtd) locale/@AB_CD@/mozapps/extensions/about.dtd (%chrome/mozapps/extensions/about.dtd) diff --git a/toolkit/modules/AppConstants.jsm b/toolkit/modules/AppConstants.jsm index 7c8a046e9..1e2204f7e 100644 --- a/toolkit/modules/AppConstants.jsm +++ b/toolkit/modules/AppConstants.jsm @@ -180,13 +180,6 @@ MOZ_SAFE_BROWSING: false, #endif - MOZ_MAINTENANCE_SERVICE: -#ifdef MOZ_MAINTENANCE_SERVICE - true, -#else - false, -#endif - DEBUG: #ifdef DEBUG true, diff --git a/toolkit/modules/Deprecated.jsm b/toolkit/modules/Deprecated.jsm index 7491a4938..981cd13ab 100644 --- a/toolkit/modules/Deprecated.jsm +++ b/toolkit/modules/Deprecated.jsm @@ -9,15 +9,21 @@ this.EXPORTED_SYMBOLS = [ "Deprecated" ]; const Cu = Components.utils; const Ci = Components.interfaces; const PREF_DEPRECATION_WARNINGS = "devtools.errorconsole.deprecation_warnings"; +const PREF_PERFORMANCE_WARNINGS = "devtools.errorconsole.performance_warnings"; Cu.import("resource://gre/modules/Services.jsm"); // A flag that indicates whether deprecation warnings should be logged. -var logWarnings = Services.prefs.getBoolPref(PREF_DEPRECATION_WARNINGS); +var logDepWarnings = Services.prefs.getBoolPref(PREF_DEPRECATION_WARNINGS); +var logPerfWarnings = Services.prefs.getBoolPref(PREF_PERFORMANCE_WARNINGS); Services.prefs.addObserver(PREF_DEPRECATION_WARNINGS, function (aSubject, aTopic, aData) { - logWarnings = Services.prefs.getBoolPref(PREF_DEPRECATION_WARNINGS); + logDepWarnings = Services.prefs.getBoolPref(PREF_DEPRECATION_WARNINGS); + }, false); +Services.prefs.addObserver(PREF_PERFORMANCE_WARNINGS, + function (aSubject, aTopic, aData) { + logPerfWarnings = Services.prefs.getBoolPref(PREF_PERFORMANCE_WARNINGS); }, false); /** @@ -58,7 +64,7 @@ this.Deprecated = { * logged. */ warning: function (aText, aUrl, aStack) { - if (!logWarnings) { + if (!logDepWarnings) { return; } @@ -71,7 +77,42 @@ this.Deprecated = { let textMessage = "DEPRECATION WARNING: " + aText + "\nYou may find more details about this deprecation at: " + - aUrl + "\n" + + aUrl + "\nCallstack:\n" + + // Append a callstack part to the deprecation message. + stringifyCallstack(aStack); + + // Report deprecation warning. + Cu.reportError(textMessage); + }, + + /** + * Log a performance warning. + * + * @param string aText + * Performance issue warning text. + * @param string aUrl + * A URL pointing to documentation describing performance + * issue and the way to address it. + * @param nsIStackFrame aStack + * An optional callstack. If it is not provided a + * snapshot of the current JavaScript callstack will be + * logged. + */ + perfWarning: function (aText, aUrl, aStack) { + if (!logPerfWarnings) { + return; + } + + // If URL is not provided, report an error. + if (!aUrl) { + Cu.reportError("Error in Deprecated.perfWarning: warnings must " + + "provide a URL documenting this performance issue."); + return; + } + + let textMessage = "PERFORMANCE WARNING: " + aText + + "\nYou may find more details about this problem at: " + + aUrl + "\nCallstack:\n" + // Append a callstack part to the deprecation message. stringifyCallstack(aStack); diff --git a/toolkit/modules/GMPInstallManager.jsm b/toolkit/modules/GMPInstallManager.jsm deleted file mode 100644 index b5987ca55..000000000 --- a/toolkit/modules/GMPInstallManager.jsm +++ /dev/null @@ -1,523 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = []; - -const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = - Components; -// 1 day default -const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24; - -var GMPInstallFailureReason = { - GMP_INVALID: 1, - GMP_HIDDEN: 2, - GMP_DISABLED: 3, - GMP_UPDATE_DISABLED: 4, -}; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FileUtils.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/osfile.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/GMPUtils.jsm"); -Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); - -this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader", - "GMPAddon"]; - -// Shared code for suppressing bad cert dialogs -XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() { - let temp = { }; - Cu.import("resource://gre/modules/CertUtils.jsm", temp); - return temp; -}); - -XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", - "resource://gre/modules/UpdateUtils.jsm"); - -function getScopedLogger(prefix) { - // `PARENT_LOGGER_ID.` being passed here effectively links this logger - // to the parentLogger. - return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); -} - -/** - * Provides an easy API for downloading and installing GMP Addons - */ -function GMPInstallManager() { -} -/** - * Temp file name used for downloading - */ -GMPInstallManager.prototype = { - /** - * Obtains a URL with replacement of vars - */ - _getURL: function() { - let log = getScopedLogger("GMPInstallManager._getURL"); - // Use the override URL if it is specified. The override URL is just like - // the normal URL but it does not check the cert. - let url = GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE); - if (url) { - log.info("Using override url: " + url); - } else { - url = GMPPrefs.get(GMPPrefs.KEY_URL); - log.info("Using url: " + url); - } - - url = UpdateUtils.formatUpdateURL(url); - - log.info("Using url (with replacement): " + url); - return url; - }, - /** - * Performs an addon check. - * @return a promise which will be resolved or rejected. - * The promise is resolved with an object with properties: - * gmpAddons: array of GMPAddons - * usedFallback: whether the data was collected from online or - * from fallback data within the build - * The promise is rejected with an object with properties: - * target: The XHR request object - * status: The HTTP status code - * type: Sometimes specifies type of rejection - */ - checkForAddons: function() { - let log = getScopedLogger("GMPInstallManager.checkForAddons"); - if (this._deferred) { - log.error("checkForAddons already called"); - return Promise.reject({type: "alreadycalled"}); - } - this._deferred = Promise.defer(); - let url = this._getURL(); - - let allowNonBuiltIn = true; - let certs = null; - if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE)) { - allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, true); - if (GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { - certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); - } - } - - let addonPromise = ProductAddonChecker - .getProductAddonList(url, allowNonBuiltIn, certs); - - addonPromise.then(res => { - if (!res || !res.gmpAddons) { - this._deferred.resolve({gmpAddons: []}); - } - else { - res.gmpAddons = res.gmpAddons.map(a => new GMPAddon(a)); - this._deferred.resolve(res); - } - delete this._deferred; - }, (ex) => { - this._deferred.reject(ex); - delete this._deferred; - }); - - return this._deferred.promise; - }, - /** - * Installs the specified addon and calls a callback when done. - * @param gmpAddon The GMPAddon object to install - * @return a promise which will be resolved or rejected - * The promise will resolve with an array of paths that were extracted - * The promise will reject with an error object: - * target: The XHR request object - * status: The HTTP status code - * type: A string to represent the type of error - * downloaderr, verifyerr or previouserrorencountered - */ - installAddon: function(gmpAddon) { - if (this._deferred) { - log.error("previous error encountered"); - return Promise.reject({type: "previouserrorencountered"}); - } - this.gmpDownloader = new GMPDownloader(gmpAddon); - return this.gmpDownloader.start(); - }, - _getTimeSinceLastCheck: function() { - let now = Math.round(Date.now() / 1000); - // Default to 0 here because `now - 0` will be returned later if that case - // is hit. We want a large value so a check will occur. - let lastCheck = GMPPrefs.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0); - // Handle clock jumps, return now since we want it to represent - // a lot of time has passed since the last check. - if (now < lastCheck) { - return now; - } - return now - lastCheck; - }, - get _isEMEEnabled() { - return GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true); - }, - _isAddonEnabled: function(aAddon) { - return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon); - }, - _isAddonUpdateEnabled: function(aAddon) { - return this._isAddonEnabled(aAddon) && - GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon); - }, - _updateLastCheck: function() { - let now = Math.round(Date.now() / 1000); - GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now); - }, - _versionchangeOccurred: function() { - let savedBuildID = GMPPrefs.get(GMPPrefs.KEY_BUILDID, null); - let buildID = Services.appinfo.platformBuildID; - if (savedBuildID == buildID) { - return false; - } - GMPPrefs.set(GMPPrefs.KEY_BUILDID, buildID); - return true; - }, - /** - * Wrapper for checkForAddons and installAddon. - * Will only install if not already installed and will log the results. - * This will only install/update the OpenH264 and EME plugins - * @return a promise which will be resolved if all addons could be installed - * successfully, rejected otherwise. - */ - simpleCheckAndInstall: Task.async(function*() { - let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall"); - - if (this._versionchangeOccurred()) { - log.info("A version change occurred. Ignoring " + - "media.gmp-manager.lastCheck to check immediately for " + - "new or updated GMPs."); - } else { - let secondsBetweenChecks = - GMPPrefs.get(GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS, - DEFAULT_SECONDS_BETWEEN_CHECKS) - let secondsSinceLast = this._getTimeSinceLastCheck(); - log.info("Last check was: " + secondsSinceLast + - " seconds ago, minimum seconds: " + secondsBetweenChecks); - if (secondsBetweenChecks > secondsSinceLast) { - log.info("Will not check for updates."); - return {status: "too-frequent-no-check"}; - } - } - - try { - let {usedFallback, gmpAddons} = yield this.checkForAddons(); - this._updateLastCheck(); - log.info("Found " + gmpAddons.length + " addons advertised."); - let addonsToInstall = gmpAddons.filter(function(gmpAddon) { - log.info("Found addon: " + gmpAddon.toString()); - - if (!gmpAddon.isValid) { - log.info("Addon |" + gmpAddon.id + "| is invalid."); - return false; - } - - if (GMPUtils.isPluginHidden(gmpAddon)) { - log.info("Addon |" + gmpAddon.id + "| has been hidden."); - return false; - } - - if (gmpAddon.isInstalled) { - log.info("Addon |" + gmpAddon.id + "| already installed."); - return false; - } - - // Do not install from fallback if already installed as it - // may be a downgrade - if (usedFallback && gmpAddon.isUpdate) { - log.info("Addon |" + gmpAddon.id + "| not installing updates based " + - "on fallback."); - return false; - } - - let addonUpdateEnabled = false; - if (GMP_PLUGIN_IDS.indexOf(gmpAddon.id) >= 0) { - if (!this._isAddonEnabled(gmpAddon.id)) { - log.info("GMP |" + gmpAddon.id + "| has been disabled; skipping check."); - } else if (!this._isAddonUpdateEnabled(gmpAddon.id)) { - log.info("Auto-update is off for " + gmpAddon.id + - ", skipping check."); - } else { - addonUpdateEnabled = true; - } - } else { - // Currently, we only support installs of OpenH264 and EME plugins. - log.info("Auto-update is off for unknown plugin '" + gmpAddon.id + - "', skipping check."); - } - - return addonUpdateEnabled; - }, this); - - if (!addonsToInstall.length) { - log.info("No new addons to install, returning"); - return {status: "nothing-new-to-install"}; - } - - let installResults = []; - let failureEncountered = false; - for (let addon of addonsToInstall) { - try { - yield this.installAddon(addon); - installResults.push({ - id: addon.id, - result: "succeeded", - }); - } catch (e) { - failureEncountered = true; - installResults.push({ - id: addon.id, - result: "failed", - }); - } - } - if (failureEncountered) { - throw {status: "failed", - results: installResults}; - } - return {status: "succeeded", - results: installResults}; - } catch (e) { - log.error("Could not check for addons", e); - throw e; - } - }), - - /** - * Makes sure everything is cleaned up - */ - uninit: function() { - let log = getScopedLogger("GMPInstallManager.uninit"); - if (this._request) { - log.info("Aborting request"); - this._request.abort(); - } - if (this._deferred) { - log.info("Rejecting deferred"); - this._deferred.reject({type: "uninitialized"}); - } - log.info("Done cleanup"); - }, - - /** - * If set to true, specifies to leave the temporary downloaded zip file. - * This is useful for tests. - */ - overrideLeaveDownloadedZip: false, -}; - -/** - * Used to construct a single GMP addon - * GMPAddon objects are returns from GMPInstallManager.checkForAddons - * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon - * - * @param addon The ProductAddonChecker `addon` object - */ -function GMPAddon(addon) { - let log = getScopedLogger("GMPAddon.constructor"); - for (let name of Object.keys(addon)) { - this[name] = addon[name]; - } - log.info ("Created new addon: " + this.toString()); -} - -GMPAddon.prototype = { - /** - * Returns a string representation of the addon - */ - toString: function() { - return this.id + " (" + - "isValid: " + this.isValid + - ", isInstalled: " + this.isInstalled + - ", hashFunction: " + this.hashFunction+ - ", hashValue: " + this.hashValue + - (this.size !== undefined ? ", size: " + this.size : "" ) + - ")"; - }, - /** - * If all the fields aren't specified don't consider this addon valid - * @return true if the addon is parsed and valid - */ - get isValid() { - return this.id && this.URL && this.version && - this.hashFunction && !!this.hashValue; - }, - get isInstalled() { - return this.version && - GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === this.version; - }, - get isEME() { - return this.id == "gmp-widevinecdm" || this.id.indexOf("gmp-eme-") == 0; - }, - /** - * @return true if the addon has been previously installed and this is - * a new version, if this is a fresh install return false - */ - get isUpdate() { - return this.version && - GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, false, this.id); - }, -}; -/** - * Constructs a GMPExtractor object which is used to extract a GMP zip - * into the specified location. (Which typically leties per platform) - * @param zipPath The path on disk of the zip file to extract - */ -function GMPExtractor(zipPath, installToDirPath) { - this.zipPath = zipPath; - this.installToDirPath = installToDirPath; -} -GMPExtractor.prototype = { - /** - * Obtains a list of all the entries in a zipfile in the format of *.*. - * This also includes files inside directories. - * - * @param zipReader the nsIZipReader to check - * @return An array of string name entries which can be used - * in nsIZipReader.extract - */ - _getZipEntries: function(zipReader) { - let entries = []; - let enumerator = zipReader.findEntries("*.*"); - while (enumerator.hasMore()) { - entries.push(enumerator.getNext()); - } - return entries; - }, - /** - * Installs the this.zipPath contents into the directory used to store GMP - * addons for the current platform. - * - * @return a promise which will be resolved or rejected - * See GMPInstallManager.installAddon for resolve/rejected info - */ - install: function() { - try { - let log = getScopedLogger("GMPExtractor.install"); - this._deferred = Promise.defer(); - log.info("Installing " + this.zipPath + "..."); - // Get the input zip file - let zipFile = Cc["@mozilla.org/file/local;1"]. - createInstance(Ci.nsIFile); - zipFile.initWithPath(this.zipPath); - - // Initialize a zipReader and obtain the entries - var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. - createInstance(Ci.nsIZipReader); - zipReader.open(zipFile) - let entries = this._getZipEntries(zipReader); - let extractedPaths = []; - - let destDir = Cc["@mozilla.org/file/local;1"]. - createInstance(Ci.nsILocalFile); - destDir.initWithPath(this.installToDirPath); - // Make sure the destination exists - if (!destDir.exists()) { - destDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); - } - - // Extract each of the entries - entries.forEach(entry => { - // We don't need these types of files - if (entry.includes("__MACOSX") || - entry == "_metadata/verified_contents.json" || - entry == "imgs/icon-128x128.png") { - return; - } - let outFile = destDir.clone(); - // Do not extract into directories. Extract all files to the same - // directory. DO NOT use |OS.Path.basename()| here, as in Windows it - // does not work properly with forward slashes (which we must use here). - let outBaseName = entry.slice(entry.lastIndexOf("/") + 1); - outFile.appendRelativePath(outBaseName); - - zipReader.extract(entry, outFile); - extractedPaths.push(outFile.path); - // Ensure files are writable and executable. Otherwise we may be unable to - // execute or uninstall them. - outFile.permissions |= parseInt("0700", 8); - log.info(entry + " was successfully extracted to: " + - outFile.path); - }); - zipReader.close(); - if (!GMPInstallManager.overrideLeaveDownloadedZip) { - zipFile.remove(false); - } - - log.info(this.zipPath + " was installed successfully"); - this._deferred.resolve(extractedPaths); - } catch (e) { - if (zipReader) { - zipReader.close(); - } - this._deferred.reject({ - target: this, - status: e, - type: "exception" - }); - } - return this._deferred.promise; - } -}; - - -/** - * Constructs an object which downloads and initiates an install of - * the specified GMPAddon object. - * @param gmpAddon The addon to install. - */ -function GMPDownloader(gmpAddon) -{ - this._gmpAddon = gmpAddon; -} - -GMPDownloader.prototype = { - /** - * Starts the download process for an addon. - * @return a promise which will be resolved or rejected - * See GMPInstallManager.installAddon for resolve/rejected info - */ - start: function() { - let log = getScopedLogger("GMPDownloader"); - let gmpAddon = this._gmpAddon; - - if (!gmpAddon.isValid) { - log.info("gmpAddon is not valid, will not continue"); - return Promise.reject({ - target: this, - status: status, - type: "downloaderr" - }); - } - - return ProductAddonChecker.downloadAddon(gmpAddon).then((zipPath) => { - let path = OS.Path.join(OS.Constants.Path.profileDir, - gmpAddon.id, - gmpAddon.version); - log.info("install to directory path: " + path); - let gmpInstaller = new GMPExtractor(zipPath, path); - let installPromise = gmpInstaller.install(); - return installPromise.then(extractedPaths => { - // Success, set the prefs - let now = Math.round(Date.now() / 1000); - GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id); - // Remember our ABI, so that if the profile is migrated to another - // platform or from 32 -> 64 bit, we notice and don't try to load the - // unexecutable plugin library. - GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ABI, UpdateUtils.ABI, gmpAddon.id); - // Setting the version pref signals installation completion to consumers, - // if you need to set other prefs etc. do it before this. - GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version, - gmpAddon.id); - return extractedPaths; - }); - }); - }, -}; diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index e288d11c8..e0acdb19e 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -46,8 +46,6 @@ EXTRA_JS_MODULES += [ 'FinderIterator.jsm', 'FormLikeFactory.jsm', 'Geometry.jsm', - 'GMPInstallManager.jsm', - 'GMPUtils.jsm', 'Http.jsm', 'InlineSpellChecker.jsm', 'InlineSpellCheckerContent.jsm', @@ -105,11 +103,9 @@ EXTRA_JS_MODULES.sessionstore += ['sessionstore/Utils.jsm'] EXTRA_PP_JS_MODULES += [ 'NewTabUtils.jsm', 'Troubleshoot.jsm', + 'UpdateChannel.jsm', ] -if not CONFIG['MOZ_WEBEXTENSIONS']: - EXTRA_PP_JS_MODULES += ['UpdateChannel.jsm'] - if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'): DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1 diff --git a/toolkit/moz.build b/toolkit/moz.build index 778f1c0de..158e760dc 100644 --- a/toolkit/moz.build +++ b/toolkit/moz.build @@ -22,15 +22,12 @@ DIRS += [ if CONFIG['MOZ_JETPACK']: DIRS += ['jetpack'] -if CONFIG['MOZ_WEBEXTENSIONS']: - DIRS += ['mozapps/webextensions'] -else: - DIRS += ['mozapps/extensions'] +DIRS += ['mozapps/extensions'] if CONFIG['MOZ_UPDATER'] and CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android': DIRS += ['mozapps/update'] -if CONFIG['MOZ_MAINTENANCE_SERVICE'] or CONFIG['MOZ_UPDATER'] and CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': +if CONFIG['MOZ_UPDATER'] and CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': # Including mozapps/update/common-standalone allows the maintenance service # to be built so the maintenance service can be used for things other than # updating applications. @@ -38,11 +35,6 @@ if CONFIG['MOZ_MAINTENANCE_SERVICE'] or CONFIG['MOZ_UPDATER'] and CONFIG['MOZ_WI 'mozapps/update/common-standalone', ] -if CONFIG['MOZ_MAINTENANCE_SERVICE']: - DIRS += [ - 'components/maintenanceservice' - ] - DIRS += ['xre'] if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3'): diff --git a/toolkit/mozapps/extensions/GMPInstallManager.jsm b/toolkit/mozapps/extensions/GMPInstallManager.jsm new file mode 100644 index 000000000..b9ebe5d7e --- /dev/null +++ b/toolkit/mozapps/extensions/GMPInstallManager.jsm @@ -0,0 +1,961 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = []; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = + Components; +// Chunk size for the incremental downloader +const DOWNLOAD_CHUNK_BYTES_SIZE = 300000; +// Incremental downloader interval +const DOWNLOAD_INTERVAL = 0; +// 1 day default +const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/ctypes.jsm"); +Cu.import("resource://gre/modules/GMPUtils.jsm"); + +this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader", + "GMPAddon"]; + +var gLocale = null; + +// Shared code for suppressing bad cert dialogs +XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() { + let temp = { }; + Cu.import("resource://gre/modules/CertUtils.jsm", temp); + return temp; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", + "resource://gre/modules/UpdateChannel.jsm"); + +/** + * Number of milliseconds after which we need to cancel `checkForAddons`. + * + * Bug 1087674 suggests that the XHR we use in `checkForAddons` may + * never terminate in presence of network nuisances (e.g. strange + * antivirus behavior). This timeout is a defensive measure to ensure + * that we fail cleanly in such case. + */ +const CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS = 20000; + +function getScopedLogger(prefix) { + // `PARENT_LOGGER_ID.` being passed here effectively links this logger + // to the parentLogger. + return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); +} + +// This is copied directly from nsUpdateService.js +// It is used for calculating the URL string w/ var replacement. +// TODO: refactor this out somewhere else +XPCOMUtils.defineLazyGetter(this, "gOSVersion", function aus_gOSVersion() { + let osVersion; + let sysInfo = Cc["@mozilla.org/system-info;1"]. + getService(Ci.nsIPropertyBag2); + try { + osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version"); + } + catch (e) { + LOG("gOSVersion - OS Version unknown: updates are not possible."); + } + + if (osVersion) { +#ifdef XP_WIN + const BYTE = ctypes.uint8_t; + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + const WCHAR = ctypes.char16_t; + const BOOL = ctypes.int; + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx + const SZCSDVERSIONLENGTH = 128; + const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', + [ + {dwOSVersionInfoSize: DWORD}, + {dwMajorVersion: DWORD}, + {dwMinorVersion: DWORD}, + {dwBuildNumber: DWORD}, + {dwPlatformId: DWORD}, + {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, + {wServicePackMajor: WORD}, + {wServicePackMinor: WORD}, + {wSuiteMask: WORD}, + {wProductType: BYTE}, + {wReserved: BYTE} + ]); + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx + const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO', + [ + {wProcessorArchitecture: WORD}, + {wReserved: WORD}, + {dwPageSize: DWORD}, + {lpMinimumApplicationAddress: ctypes.voidptr_t}, + {lpMaximumApplicationAddress: ctypes.voidptr_t}, + {dwActiveProcessorMask: DWORD.ptr}, + {dwNumberOfProcessors: DWORD}, + {dwProcessorType: DWORD}, + {dwAllocationGranularity: DWORD}, + {wProcessorLevel: WORD}, + {wProcessorRevision: WORD} + ]); + + let kernel32 = false; + try { + kernel32 = ctypes.open("Kernel32"); + } catch (e) { + LOG("gOSVersion - Unable to open kernel32! " + e); + osVersion += ".unknown (unknown)"; + } + + if(kernel32) { + try { + // Get Service pack info + try { + let GetVersionEx = kernel32.declare("GetVersionExW", + ctypes.default_abi, + BOOL, + OSVERSIONINFOEXW.ptr); + let winVer = OSVERSIONINFOEXW(); + winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; + + if(0 !== GetVersionEx(winVer.address())) { + osVersion += "." + winVer.wServicePackMajor + + "." + winVer.wServicePackMinor; + } else { + LOG("gOSVersion - Unknown failure in GetVersionEX (returned 0)"); + osVersion += ".unknown"; + } + } catch (e) { + LOG("gOSVersion - error getting service pack information. Exception: " + e); + osVersion += ".unknown"; + } + + // Get processor architecture + let arch = "unknown"; + try { + let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo", + ctypes.default_abi, + ctypes.void_t, + SYSTEM_INFO.ptr); + let sysInfo = SYSTEM_INFO(); + // Default to unknown + sysInfo.wProcessorArchitecture = 0xffff; + + GetNativeSystemInfo(sysInfo.address()); + switch(sysInfo.wProcessorArchitecture) { + case 9: + arch = "x64"; + break; + case 6: + arch = "IA64"; + break; + case 0: + arch = "x86"; + break; + } + } catch (e) { + LOG("gOSVersion - error getting processor architecture. Exception: " + e); + } finally { + osVersion += " (" + arch + ")"; + } + } finally { + kernel32.close(); + } + } +#endif + + try { + osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")"; + } + catch (e) { + // Not all platforms have a secondary widget library, so an error is nothing to worry about. + } + osVersion = encodeURIComponent(osVersion); + } + return osVersion; +}); + +// This is copied directly from nsUpdateService.js +// It is used for calculating the URL string w/ var replacement. +// TODO: refactor this out somewhere else +XPCOMUtils.defineLazyGetter(this, "gABI", function aus_gABI() { + let abi = null; + try { + abi = Services.appinfo.XPCOMABI; + } + catch (e) { + LOG("gABI - XPCOM ABI unknown: updates are not possible."); + } +#ifdef XP_MACOSX + // Mac universal build should report a different ABI than either macppc + // or mactel. + let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"]. + getService(Ci.nsIMacUtils); + + if (macutils.isUniversalBinary) + abi += "-u-" + macutils.architecturesInBinary; +#ifdef MOZ_SHARK + // Disambiguate optimised and shark nightlies + abi += "-shark" +#endif +#endif + return abi; +}); + +/** + * Provides an easy API for downloading and installing GMP Addons + */ +function GMPInstallManager() { +} +/** + * Temp file name used for downloading + */ +GMPInstallManager.prototype = { + /** + * Obtains a URL with replacement of vars + */ + _getURL: function() { + let log = getScopedLogger("GMPInstallManager._getURL"); + // Use the override URL if it is specified. The override URL is just like + // the normal URL but it does not check the cert. + let url = GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE); + if (url) { + log.info("Using override url: " + url); + } else { + url = GMPPrefs.get(GMPPrefs.KEY_URL); + log.info("Using url: " + url); + } + + url = + url.replace(/%PRODUCT%/g, Services.appinfo.name) + .replace(/%VERSION%/g, Services.appinfo.version) + .replace(/%BUILD_ID%/g, Services.appinfo.appBuildID) + .replace(/%BUILD_TARGET%/g, Services.appinfo.OS + "_" + gABI) + .replace(/%OS_VERSION%/g, gOSVersion); + if (/%LOCALE%/.test(url)) { + // TODO: Get the real local, does it actually matter for GMP plugins? + url = url.replace(/%LOCALE%/g, "en-US"); + } + url = + url.replace(/%CHANNEL%/g, UpdateChannel.get()) + .replace(/%PLATFORM_VERSION%/g, Services.appinfo.platformVersion) + .replace(/%DISTRIBUTION%/g, + GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION)) + .replace(/%DISTRIBUTION_VERSION%/g, + GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION_VERSION)) + .replace(/\+/g, "%2B"); + log.info("Using url (with replacement): " + url); + return url; + }, + /** + * Performs an addon check. + * @return a promise which will be resolved or rejected. + * The promise is resolved with an array of GMPAddons + * The promise is rejected with an object with properties: + * target: The XHR request object + * status: The HTTP status code + * type: Sometimes specifies type of rejection + */ + checkForAddons: function() { + let log = getScopedLogger("GMPInstallManager.checkForAddons"); + if (this._deferred) { + log.error("checkForAddons already called"); + return Promise.reject({type: "alreadycalled"}); + } + this._deferred = Promise.defer(); + let url = this._getURL(); + + this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsISupports); + // This is here to let unit test code override XHR + if (this._request.wrappedJSObject) { + this._request = this._request.wrappedJSObject; + } + this._request.open("GET", url, true); + let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true); + this._request.channel.notificationCallbacks = + new gCertUtils.BadCertHandler(allowNonBuiltIn); + // Prevent the request from reading from the cache. + this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + // Prevent the request from writing to the cache. + this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + + this._request.overrideMimeType("text/xml"); + // The Cache-Control header is only interpreted by proxies and the + // final destination. It does not help if a resource is already + // cached locally. + this._request.setRequestHeader("Cache-Control", "no-cache"); + // HTTP/1.0 servers might not implement Cache-Control and + // might only implement Pragma: no-cache + this._request.setRequestHeader("Pragma", "no-cache"); + + this._request.timeout = CHECK_FOR_ADDONS_TIMEOUT_DELAY_MS; + this._request.addEventListener("error", event => this.onFailXML("onErrorXML", event), false); + this._request.addEventListener("abort", event => this.onFailXML("onAbortXML", event), false); + this._request.addEventListener("timeout", event => this.onFailXML("onTimeoutXML", event), false); + this._request.addEventListener("load", event => this.onLoadXML(event), false); + + log.info("sending request to: " + url); + this._request.send(null); + + return this._deferred.promise; + }, + /** + * Installs the specified addon and calls a callback when done. + * @param gmpAddon The GMPAddon object to install + * @return a promise which will be resolved or rejected + * The promise will resolve with an array of paths that were extracted + * The promise will reject with an error object: + * target: The XHR request object + * status: The HTTP status code + * type: A string to represent the type of error + * downloaderr, verifyerr or previouserrorencountered + */ + installAddon: function(gmpAddon) { + if (this._deferred) { + log.error("previous error encountered"); + return Promise.reject({type: "previouserrorencountered"}); + } + this.gmpDownloader = new GMPDownloader(gmpAddon); + return this.gmpDownloader.start(); + }, + _getTimeSinceLastCheck: function() { + let now = Math.round(Date.now() / 1000); + // Default to 0 here because `now - 0` will be returned later if that case + // is hit. We want a large value so a check will occur. + let lastCheck = GMPPrefs.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0); + // Handle clock jumps, return now since we want it to represent + // a lot of time has passed since the last check. + if (now < lastCheck) { + return now; + } + return now - lastCheck; + }, + get _isEMEEnabled() { + return GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true); + }, + _isAddonUpdateEnabled: function(aAddon) { + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon) && + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon); + }, + _updateLastCheck: function() { + let now = Math.round(Date.now() / 1000); + GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now); + }, + _versionchangeOccurred: function() { + let savedBuildID = GMPPrefs.get(GMPPrefs.KEY_BUILDID, null); + let buildID = Services.appinfo.platformBuildID; + if (savedBuildID == buildID) { + return false; + } + GMPPrefs.set(GMPPrefs.KEY_BUILDID, buildID); + return true; + }, + /** + * Wrapper for checkForAddons and installAddon. + * Will only install if not already installed and will log the results. + * This will only install/update the OpenH264 and EME plugins + * @return a promise which will be resolved if all addons could be installed + * successfully, rejected otherwise. + */ + simpleCheckAndInstall: Task.async(function*() { + let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall"); + + if (this._versionchangeOccurred()) { + log.info("A version change occurred. Ignoring " + + "media.gmp-manager.lastCheck to check immediately for " + + "new or updated GMPs."); + } else { + let secondsBetweenChecks = + GMPPrefs.get(GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS, + DEFAULT_SECONDS_BETWEEN_CHECKS) + let secondsSinceLast = this._getTimeSinceLastCheck(); + log.info("Last check was: " + secondsSinceLast + + " seconds ago, minimum seconds: " + secondsBetweenChecks); + if (secondsBetweenChecks > secondsSinceLast) { + log.info("Will not check for updates."); + return {status: "too-frequent-no-check"}; + } + } + + try { + let gmpAddons = yield this.checkForAddons(); + this._updateLastCheck(); + log.info("Found " + gmpAddons.length + " addons advertised."); + let addonsToInstall = gmpAddons.filter(function(gmpAddon) { + log.info("Found addon: " + gmpAddon.toString()); + + if (!gmpAddon.isValid || GMPUtils.isPluginHidden(gmpAddon) || + gmpAddon.isInstalled) { + log.info("Addon invalid, hidden or already installed."); + return false; + } + + let addonUpdateEnabled = false; + if (GMP_PLUGIN_IDS.indexOf(gmpAddon.id) >= 0) { + addonUpdateEnabled = this._isAddonUpdateEnabled(gmpAddon.id); + if (!addonUpdateEnabled) { + log.info("Auto-update is off for " + gmpAddon.id + + ", skipping check."); + } + } else { + // Currently, we only support installs of OpenH264 and EME plugins. + log.info("Auto-update is off for unknown plugin '" + gmpAddon.id + + "', skipping check."); + } + + return addonUpdateEnabled; + }, this); + + if (!addonsToInstall.length) { + log.info("No new addons to install, returning"); + return {status: "nothing-new-to-install"}; + } + + let installResults = []; + let failureEncountered = false; + for (let addon of addonsToInstall) { + try { + yield this.installAddon(addon); + installResults.push({ + id: addon.id, + result: "succeeded", + }); + } catch (e) { + failureEncountered = true; + installResults.push({ + id: addon.id, + result: "failed", + }); + } + } + if (failureEncountered) { + throw {status: "failed", + results: installResults}; + } + return {status: "succeeded", + results: installResults}; + } catch(e) { + log.error("Could not check for addons", e); + throw e; + } + }), + + /** + * Makes sure everything is cleaned up + */ + uninit: function() { + let log = getScopedLogger("GMPInstallManager.uninit"); + if (this._request) { + log.info("Aborting request"); + this._request.abort(); + } + if (this._deferred) { + log.info("Rejecting deferred"); + this._deferred.reject({type: "uninitialized"}); + } + log.info("Done cleanup"); + }, + + /** + * If set to true, specifies to leave the temporary downloaded zip file. + * This is useful for tests. + */ + overrideLeaveDownloadedZip: false, + + /** + * The XMLHttpRequest succeeded and the document was loaded. + * @param event The nsIDOMEvent for the load + */ + onLoadXML: function(event) { + let log = getScopedLogger("GMPInstallManager.onLoadXML"); + try { + log.info("request completed downloading document"); + let certs = null; + if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) && + GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { + certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); + } + + let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, + true); + log.info("allowNonBuiltIn: " + allowNonBuiltIn); + + gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs); + + this.parseResponseXML(); + } catch (ex) { + log.error("could not load xml: " + ex); + this._deferred.reject({ + target: event.target, + status: this._getChannelStatus(event.target), + message: "" + ex, + }); + delete this._deferred; + } + }, + + /** + * Returns the status code for the XMLHttpRequest + */ + _getChannelStatus: function(request) { + let log = getScopedLogger("GMPInstallManager._getChannelStatus"); + let status = null; + try { + status = request.status; + log.info("request.status is: " + request.status); + } + catch (e) { + } + + if (status == null) { + status = request.channel.QueryInterface(Ci.nsIRequest).status; + } + return status; + }, + + /** + * There was an error of some kind during the XMLHttpRequest. This + * error may have been caused by external factors (e.g. network + * issues) or internally (by a timeout). + * + * @param event The nsIDOMEvent for the error + */ + onFailXML: function(failure, event) { + let log = getScopedLogger("GMPInstallManager.onFailXML " + failure); + let request = event.target; + let status = this._getChannelStatus(request); + let message = "request.status: " + status + " (" + event.type + ")"; + log.warn(message); + this._deferred.reject({ + target: request, + status: status, + message: message + }); + delete this._deferred; + }, + + /** + * Returns an array of GMPAddon objects discovered by the update check. + * Or returns an empty array if there were any problems with parsing. + * If there's an error, it will be logged if logging is enabled. + */ + parseResponseXML: function() { + try { + let log = getScopedLogger("GMPInstallManager.parseResponseXML"); + let updatesElement = this._request.responseXML.documentElement; + if (!updatesElement) { + let message = "empty updates document"; + log.warn(message); + this._deferred.reject({ + target: this._request, + message: message + }); + delete this._deferred; + return; + } + + if (updatesElement.nodeName != "updates") { + let message = "got node name: " + updatesElement.nodeName + + ", expected: updates"; + log.warn(message); + this._deferred.reject({ + target: this._request, + message: message + }); + delete this._deferred; + return; + } + + const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE; + let gmpResults = []; + for (let i = 0; i < updatesElement.childNodes.length; ++i) { + let updatesChildElement = updatesElement.childNodes.item(i); + if (updatesChildElement.nodeType != ELEMENT_NODE) { + continue; + } + if (updatesChildElement.localName == "addons") { + gmpResults = GMPAddon.parseGMPAddonsNode(updatesChildElement); + } + } + this._deferred.resolve(gmpResults); + delete this._deferred; + } catch (e) { + this._deferred.reject({ + target: this._request, + message: e + }); + delete this._deferred; + } + }, +}; + +/** + * Used to construct a single GMP addon + * GMPAddon objects are returns from GMPInstallManager.checkForAddons + * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon + * + * @param gmpAddon The AUS response XML's DOM element `addon` + */ +function GMPAddon(gmpAddon) { + let log = getScopedLogger("GMPAddon.constructor"); + gmpAddon.QueryInterface(Ci.nsIDOMElement); + ["id", "URL", "hashFunction", + "hashValue", "version", "size"].forEach(name => { + if (gmpAddon.hasAttribute(name)) { + this[name] = gmpAddon.getAttribute(name); + } + }); + this.size = Number(this.size) || undefined; + log.info ("Created new addon: " + this.toString()); +} +/** + * Parses an XML GMP addons node from AUS into an array + * @param addonsElement An nsIDOMElement compatible node with XML from AUS + * @return An array of GMPAddon results + */ +GMPAddon.parseGMPAddonsNode = function(addonsElement) { + let log = getScopedLogger("GMPAddon.parseGMPAddonsNode"); + let gmpResults = []; + if (addonsElement.localName !== "addons") { + return; + } + + addonsElement.QueryInterface(Ci.nsIDOMElement); + let addonCount = addonsElement.childNodes.length; + for (let i = 0; i < addonCount; ++i) { + let addonElement = addonsElement.childNodes.item(i); + if (addonElement.localName !== "addon") { + continue; + } + addonElement.QueryInterface(Ci.nsIDOMElement); + try { + gmpResults.push(new GMPAddon(addonElement)); + } catch (e) { + log.warn("invalid addon: " + e); + continue; + } + } + return gmpResults; +}; +GMPAddon.prototype = { + /** + * Returns a string representation of the addon + */ + toString: function() { + return this.id + " (" + + "isValid: " + this.isValid + + ", isInstalled: " + this.isInstalled + + ", hashFunction: " + this.hashFunction+ + ", hashValue: " + this.hashValue + + (this.size !== undefined ? ", size: " + this.size : "" ) + + ")"; + }, + /** + * If all the fields aren't specified don't consider this addon valid + * @return true if the addon is parsed and valid + */ + get isValid() { + return this.id && this.URL && this.version && + this.hashFunction && !!this.hashValue; + }, + get isInstalled() { + return this.version && + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === this.version; + }, + get isEME() { + return this.id == "gmp-widevinecdm" || this.id.indexOf("gmp-eme-") == 0; + }, +}; +/** + * Constructs a GMPExtractor object which is used to extract a GMP zip + * into the specified location. (Which typically leties per platform) + * @param zipPath The path on disk of the zip file to extract + */ +function GMPExtractor(zipPath, installToDirPath) { + this.zipPath = zipPath; + this.installToDirPath = installToDirPath; +} +GMPExtractor.prototype = { + /** + * Obtains a list of all the entries in a zipfile in the format of *.*. + * This also includes files inside directories. + * + * @param zipReader the nsIZipReader to check + * @return An array of string name entries which can be used + * in nsIZipReader.extract + */ + _getZipEntries: function(zipReader) { + let entries = []; + let enumerator = zipReader.findEntries("*.*"); + while (enumerator.hasMore()) { + entries.push(enumerator.getNext()); + } + return entries; + }, + /** + * Installs the this.zipPath contents into the directory used to store GMP + * addons for the current platform. + * + * @return a promise which will be resolved or rejected + * See GMPInstallManager.installAddon for resolve/rejected info + */ + install: function() { + try { + let log = getScopedLogger("GMPExtractor.install"); + this._deferred = Promise.defer(); + log.info("Installing " + this.zipPath + "..."); + // Get the input zip file + let zipFile = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsIFile); + zipFile.initWithPath(this.zipPath); + + // Initialize a zipReader and obtain the entries + var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. + createInstance(Ci.nsIZipReader); + zipReader.open(zipFile) + let entries = this._getZipEntries(zipReader); + let extractedPaths = []; + + // Extract each of the entries + entries.forEach(entry => { + // We don't need these types of files + if (entry.includes("__MACOSX")) { + return; + } + let outFile = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsILocalFile); + outFile.initWithPath(this.installToDirPath); + outFile.appendRelativePath(entry); + + // Make sure the directory hierarchy exists + if(!outFile.parent.exists()) { + outFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + zipReader.extract(entry, outFile); + extractedPaths.push(outFile.path); + log.info(entry + " was successfully extracted to: " + + outFile.path); + }); + zipReader.close(); + if (!GMPInstallManager.overrideLeaveDownloadedZip) { + zipFile.remove(false); + } + + log.info(this.zipPath + " was installed successfully"); + this._deferred.resolve(extractedPaths); + } catch (e) { + if (zipReader) { + zipReader.close(); + } + this._deferred.reject({ + target: this, + status: e, + type: "exception" + }); + } + return this._deferred.promise; + } +}; + + +/** + * Constructs an object which downloads and initiates an install of + * the specified GMPAddon object. + * @param gmpAddon The addon to install. + */ +function GMPDownloader(gmpAddon) +{ + this._gmpAddon = gmpAddon; +} +/** + * Computes the file hash of fileToHash with the specified hash function + * @param hashFunctionName A hash function name such as sha512 + * @param fileToHash An nsIFile to hash + * @return a promise which resolve to a digest in binary hex format + */ +GMPDownloader.computeHash = function(hashFunctionName, fileToHash) { + let log = getScopedLogger("GMPDownloader.computeHash"); + let digest; + let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fileStream.init(fileToHash, FileUtils.MODE_RDONLY, + FileUtils.PERMS_FILE, 0); + try { + let hash = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + let hashFunction = + Ci.nsICryptoHash[hashFunctionName.toUpperCase()]; + if (!hashFunction) { + log.error("could not get hash function"); + return Promise.reject(); + } + hash.init(hashFunction); + hash.updateFromStream(fileStream, -1); + digest = binaryToHex(hash.finish(false)); + } catch (e) { + log.warn("failed to compute hash: " + e); + digest = ""; + } + fileStream.close(); + return Promise.resolve(digest); +}, +GMPDownloader.prototype = { + /** + * Starts the download process for an addon. + * @return a promise which will be resolved or rejected + * See GMPInstallManager.installAddon for resolve/rejected info + */ + start: function() { + let log = getScopedLogger("GMPDownloader.start"); + this._deferred = Promise.defer(); + if (!this._gmpAddon.isValid) { + log.info("gmpAddon is not valid, will not continue"); + return Promise.reject({ + target: this, + status: status, + type: "downloaderr" + }); + } + + let uri = Services.io.newURI(this._gmpAddon.URL, null, null); + this._request = Cc["@mozilla.org/network/incremental-download;1"]. + createInstance(Ci.nsIIncrementalDownload); + let gmpFile = FileUtils.getFile("TmpD", [this._gmpAddon.id + ".zip"]); + if (gmpFile.exists()) { + gmpFile.remove(false); + } + + log.info("downloading from " + uri.spec + " to " + gmpFile.path); + this._request.init(uri, gmpFile, DOWNLOAD_CHUNK_BYTES_SIZE, + DOWNLOAD_INTERVAL); + this._request.start(this, null); + return this._deferred.promise; + }, + // For nsIRequestObserver + onStartRequest: function(request, context) { + }, + // For nsIRequestObserver + // Called when the GMP addon zip file is downloaded + onStopRequest: function(request, context, status) { + let log = getScopedLogger("GMPDownloader.onStopRequest"); + log.info("onStopRequest called"); + if (!Components.isSuccessCode(status)) { + log.info("status failed: " + status); + this._deferred.reject({ + target: this, + status: status, + type: "downloaderr" + }); + return; + } + + let promise = this._verifyDownload(); + promise.then(() => { + log.info("GMP file is ready to unzip"); + let destination = this._request.destination; + + let zipPath = destination.path; + let gmpAddon = this._gmpAddon; + let installToDirPath = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsIFile); + let path = OS.Path.join(OS.Constants.Path.profileDir, + gmpAddon.id, + gmpAddon.version); + installToDirPath.initWithPath(path); + log.info("install to directory path: " + installToDirPath.path); + let gmpInstaller = new GMPExtractor(zipPath, installToDirPath.path); + let installPromise = gmpInstaller.install(); + installPromise.then(extractedPaths => { + // Success, set the prefs + let now = Math.round(Date.now() / 1000); + GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id); + // Setting the version pref signals installation completion to consumers, + // if you need to set other prefs etc. do it before this. + GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version, + gmpAddon.id); + this._deferred.resolve(extractedPaths); + }, err => { + this._deferred.reject(err); + }); + }, err => { + log.warn("verifyDownload check failed"); + this._deferred.reject({ + target: this, + status: 200, + type: "verifyerr" + }); + }); + }, + /** + * Verifies that the downloaded zip file's hash matches the GMPAddon hash. + * @return a promise which resolves if the download verifies + */ + _verifyDownload: function() { + let verifyDownloadDeferred = Promise.defer(); + let log = getScopedLogger("GMPDownloader._verifyDownload"); + log.info("_verifyDownload called"); + if (!this._request) { + return Promise.reject(); + } + + let destination = this._request.destination; + log.info("for path: " + destination.path); + + // Ensure that the file size matches the expected file size. + if (this._gmpAddon.size !== undefined && + destination.fileSize != this._gmpAddon.size) { + log.warn("Downloader:_verifyDownload downloaded size " + + destination.fileSize + " != expected size " + + this._gmpAddon.size + "."); + return Promise.reject(); + } + + let promise = GMPDownloader.computeHash(this._gmpAddon.hashFunction, destination); + promise.then(digest => { + let expectedDigest = this._gmpAddon.hashValue.toLowerCase(); + if (digest !== expectedDigest) { + log.warn("hashes do not match! Got: `" + + digest + "`, expected: `" + expectedDigest + "`"); + this._deferred.reject(); + return; + } + + log.info("hashes match!"); + verifyDownloadDeferred.resolve(); + }, err => { + verifyDownloadDeferred.reject(); + }); + return verifyDownloadDeferred.promise; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver]) +}; + +/** + * Convert a string containing binary values to hex. + */ +function binaryToHex(input) { + let result = ""; + for (let i = 0; i < input.length; ++i) { + let hex = input.charCodeAt(i).toString(16); + if (hex.length == 1) + hex = "0" + hex; + result += hex; + } + return result; +} diff --git a/toolkit/modules/GMPUtils.jsm b/toolkit/mozapps/extensions/GMPUtils.jsm index 9e41a7a61..9e41a7a61 100644 --- a/toolkit/modules/GMPUtils.jsm +++ b/toolkit/mozapps/extensions/GMPUtils.jsm diff --git a/toolkit/mozapps/extensions/content/extensions.xml b/toolkit/mozapps/extensions/content/extensions.xml index cbd05bfa9..9c8fda8ed 100644 --- a/toolkit/mozapps/extensions/content/extensions.xml +++ b/toolkit/mozapps/extensions/content/extensions.xml @@ -941,10 +941,6 @@ #endif oncommand="document.getBindingParent(this).showPreferences();"/> <!-- label="&cmd.debugAddon.label;" --> - <xul:button anonid="debug-btn" class="addon-control debug" - label="&cmd.debugAddon.label;" - oncommand="document.getBindingParent(this).debug();"/> - <xul:button anonid="enable-btn" class="addon-control enable" label="&cmd.enableAddon.label;" oncommand="document.getBindingParent(this).userDisabled = false;"/> @@ -1087,10 +1083,6 @@ document.getAnonymousElementByAttribute(this, "anonid", "enable-btn"); </field> - <field name="_debugBtn"> - document.getAnonymousElementByAttribute(this, "anonid", - "debug-btn"); - </field> <field name="_disableBtn"> document.getAnonymousElementByAttribute(this, "anonid", "disable-btn"); @@ -1430,12 +1422,6 @@ this.mAddon.install.state != AddonManager.STATE_INSTALLED); this._showStatus(showProgress ? "progress" : "none"); - let debuggable = this.mAddon.isDebuggable && - Services.prefs.getBoolPref('devtools.chrome.enabled') && - Services.prefs.getBoolPref('devtools.debugger.remote-enabled'); - - this._debugBtn.disabled = this._debugBtn.hidden = !debuggable - if (this.mAddon.type == "experiment") { this.removeAttribute("notification"); let prefix = "experiment."; diff --git a/toolkit/mozapps/extensions/content/newaddon.js b/toolkit/mozapps/extensions/content/newaddon.js index 79978a698..aab556a62 100644 --- a/toolkit/mozapps/extensions/content/newaddon.js +++ b/toolkit/mozapps/extensions/content/newaddon.js @@ -43,9 +43,6 @@ function initialize() { // been seen or it cannot be enabled then this UI is useless, just close it. // This shouldn't normally happen unless session restore restores the tab. if (!aAddon || !aAddon.userDisabled || -#ifdef MOZ_WEBEXTENSIONS - aAddon.seen || -#endif !(aAddon.permissions & AddonManager.PERM_CAN_ENABLE)) { window.close(); return; @@ -82,16 +79,6 @@ function initialize() { document.getElementById("location").hidden = true; } -#ifdef MOZ_WEBEXTENSIONS - // Only mark the add-on as seen if the page actually gets focus - if (document.hasFocus()) { - aAddon.markAsSeen(); - } - else { - document.addEventListener("focus", () => aAddon.markAsSeen(), false); - } -#endif - var event = document.createEvent("Events"); event.initEvent("AddonDisplayed", true, true); document.dispatchEvent(event); diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm index 25651f1b8..131db7249 100644 --- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm +++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm @@ -49,6 +49,7 @@ const GMP_PLUGINS = [ homepageURL: "http://www.openh264.org/", optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul" }, +/* { id: EME_ADOBE_ID, name: "eme-adobe_name", @@ -57,6 +58,17 @@ const GMP_PLUGINS = [ homepageURL: "http://help.adobe.com/en_US/primetime/drm/HTML5_CDM", optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul", isEME: true + }, +*/ + { + id: WIDEVINE_ID, + name: "widevine_description", + // Describe the purpose of both CDMs in the same way. + description: "eme-adobe_description", + licenseURL: "https://www.google.com/policies/privacy/", + homepageURL: "https://www.widevine.com/", + optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul", + isEME: true }]; XPCOMUtils.defineConstant(this, "GMP_PLUGINS", GMP_PLUGINS); diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index 9ea876f6c..99a121da4 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -2584,7 +2584,7 @@ this.XPIProvider = { } } catch (e) { - logger.warn("Failed to call uninstall for " + id, e); + // If called on startup this may fail due to staged folder still existing. } try { diff --git a/toolkit/mozapps/extensions/jar.mn b/toolkit/mozapps/extensions/jar.mn index c4d8874c9..e95d93ca0 100644 --- a/toolkit/mozapps/extensions/jar.mn +++ b/toolkit/mozapps/extensions/jar.mn @@ -26,7 +26,7 @@ toolkit.jar: content/mozapps/extensions/eula.xul (content/eula.xul) content/mozapps/extensions/eula.js (content/eula.js) content/mozapps/extensions/newaddon.xul (content/newaddon.xul) -* content/mozapps/extensions/newaddon.js (content/newaddon.js) + content/mozapps/extensions/newaddon.js (content/newaddon.js) content/mozapps/extensions/setting.xml (content/setting.xml) content/mozapps/extensions/pluginPrefs.xul (content/pluginPrefs.xul) content/mozapps/extensions/gmpPrefs.xul (content/gmpPrefs.xul) diff --git a/toolkit/mozapps/extensions/moz.build b/toolkit/mozapps/extensions/moz.build index 3988cc27a..104e8d734 100644 --- a/toolkit/mozapps/extensions/moz.build +++ b/toolkit/mozapps/extensions/moz.build @@ -29,11 +29,13 @@ EXTRA_PP_COMPONENTS += [ EXTRA_JS_MODULES += [ 'ChromeManifestParser.jsm', 'DeferredSave.jsm', + 'GMPUtils.jsm', 'LightweightThemeManager.jsm', ] EXTRA_PP_JS_MODULES += [ - 'AddonManager.jsm' + 'AddonManager.jsm', + 'GMPInstallManager.jsm', ] # Additional debugging info is exposed in debug builds diff --git a/toolkit/mozapps/installer/windows/nsis/makensis.mk b/toolkit/mozapps/installer/windows/nsis/makensis.mk index aff6d29e9..121819f6a 100644 --- a/toolkit/mozapps/installer/windows/nsis/makensis.mk +++ b/toolkit/mozapps/installer/windows/nsis/makensis.mk @@ -75,10 +75,3 @@ uninstaller:: cd $(CONFIG_DIR) && $(MAKENSISU) uninstaller.nsi $(NSINSTALL) -D $(DIST)/bin/uninstall cp $(CONFIG_DIR)/helper.exe $(DIST)/bin/uninstall - -ifdef MOZ_MAINTENANCE_SERVICE -maintenanceservice_installer:: - cd $(CONFIG_DIR) && $(MAKENSISU) maintenanceservice_installer.nsi - $(NSINSTALL) -D $(DIST)/bin/ - cp $(CONFIG_DIR)/maintenanceservice_installer.exe $(DIST)/bin -endif diff --git a/toolkit/mozapps/update/common/errors.h b/toolkit/mozapps/update/common/errors.h index aac029175..846118d55 100644 --- a/toolkit/mozapps/update/common/errors.h +++ b/toolkit/mozapps/update/common/errors.h @@ -42,18 +42,6 @@ #define MAR_CHANNEL_MISMATCH_ERROR 22 #define VERSION_DOWNGRADE_ERROR 23 -// Error codes 24-33 and 49-57 are for the Windows maintenance service. -#define SERVICE_UPDATER_COULD_NOT_BE_STARTED 24 -#define SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS 25 -#define SERVICE_UPDATER_SIGN_ERROR 26 -#define SERVICE_UPDATER_COMPARE_ERROR 27 -#define SERVICE_UPDATER_IDENTITY_ERROR 28 -#define SERVICE_STILL_APPLYING_ON_SUCCESS 29 -#define SERVICE_STILL_APPLYING_ON_FAILURE 30 -#define SERVICE_UPDATER_NOT_FIXED_DRIVE 31 -#define SERVICE_COULD_NOT_LOCK_UPDATER 32 -#define SERVICE_INSTALLDIR_ERROR 33 - #define NO_INSTALLDIR_ERROR 34 #define WRITE_ERROR_ACCESS_DENIED 35 // #define WRITE_ERROR_SHARING_VIOLATION 36 // Replaced with errors 46-48 @@ -67,17 +55,6 @@ #define DELETE_ERROR_EXPECTED_FILE 47 #define RENAME_ERROR_EXPECTED_FILE 48 -// Error codes 24-33 and 49-57 are for the Windows maintenance service. -#define SERVICE_COULD_NOT_COPY_UPDATER 49 -#define SERVICE_STILL_APPLYING_TERMINATED 50 -#define SERVICE_STILL_APPLYING_NO_EXIT_CODE 51 -#define SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR 52 -#define SERVICE_CALC_REG_PATH_ERROR 53 -#define SERVICE_INVALID_APPLYTO_DIR_ERROR 54 -#define SERVICE_INVALID_INSTALL_DIR_PATH_ERROR 55 -#define SERVICE_INVALID_WORKING_DIR_PATH_ERROR 56 -#define SERVICE_INSTALL_DIR_REG_ERROR 57 - #define WRITE_ERROR_FILE_COPY 61 #define WRITE_ERROR_DELETE_FILE 62 #define WRITE_ERROR_OPEN_PATCH_FILE 63 diff --git a/toolkit/mozapps/update/common/moz.build b/toolkit/mozapps/update/common/moz.build index cacb0bad2..ca7c4c307 100644 --- a/toolkit/mozapps/update/common/moz.build +++ b/toolkit/mozapps/update/common/moz.build @@ -17,11 +17,6 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': 'updatehelper.cpp', 'updatehelper.h', ] - if CONFIG['MOZ_MAINTENANCE_SERVICE']: - EXPORTS += [ - 'certificatecheck.h', - 'registrycertificates.h', - ] Library('updatecommon') diff --git a/toolkit/mozapps/update/common/sources.mozbuild b/toolkit/mozapps/update/common/sources.mozbuild index cde51e09b..339944640 100644 --- a/toolkit/mozapps/update/common/sources.mozbuild +++ b/toolkit/mozapps/update/common/sources.mozbuild @@ -10,15 +10,6 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': 'uachelper.cpp', 'updatehelper.cpp', ] - if CONFIG['MOZ_MAINTENANCE_SERVICE']: - sources += [ - 'certificatecheck.cpp', - 'registrycertificates.cpp', - ] - OS_LIBS += [ - 'crypt32', - 'wintrust', - ] sources += [ 'readstrings.cpp', diff --git a/toolkit/mozapps/update/nsUpdateService.js b/toolkit/mozapps/update/nsUpdateService.js index 0cf7b8938..4e2d66a5c 100644 --- a/toolkit/mozapps/update/nsUpdateService.js +++ b/toolkit/mozapps/update/nsUpdateService.js @@ -35,15 +35,13 @@ const PREF_APP_UPDATE_LOG = "app.update.log"; const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported"; const PREF_APP_UPDATE_POSTUPDATE = "app.update.postupdate"; const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime"; -const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled"; -const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors"; -const PREF_APP_UPDATE_SERVICE_MAXERRORS = "app.update.service.maxErrors"; const PREF_APP_UPDATE_SILENT = "app.update.silent"; const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors"; const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT = "app.update.socket.retryTimeout"; const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled"; const PREF_APP_UPDATE_URL = "app.update.url"; const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details"; +const PREF_APP_UPDATE_URL_OVERRIDE = "app.update.url.override"; const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never."; @@ -72,12 +70,10 @@ const FILE_UPDATE_VERSION = "update.version"; const STATE_NONE = "null"; const STATE_DOWNLOADING = "downloading"; const STATE_PENDING = "pending"; -const STATE_PENDING_SERVICE = "pending-service"; const STATE_PENDING_ELEVATE = "pending-elevate"; const STATE_APPLYING = "applying"; const STATE_APPLIED = "applied"; const STATE_APPLIED_OS = "applied-os"; -const STATE_APPLIED_SERVICE = "applied-service"; const STATE_SUCCEEDED = "succeeded"; const STATE_DOWNLOAD_FAILED = "download-failed"; const STATE_FAILED = "failed"; @@ -85,22 +81,9 @@ const STATE_FAILED = "failed"; // The values below used by this code are from common/errors.h const WRITE_ERROR = 7; const ELEVATION_CANCELED = 9; -const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24; -const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25; -const SERVICE_UPDATER_SIGN_ERROR = 26; -const SERVICE_UPDATER_COMPARE_ERROR = 27; -const SERVICE_UPDATER_IDENTITY_ERROR = 28; -const SERVICE_STILL_APPLYING_ON_SUCCESS = 29; -const SERVICE_STILL_APPLYING_ON_FAILURE = 30; -const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31; -const SERVICE_COULD_NOT_LOCK_UPDATER = 32; -const SERVICE_INSTALLDIR_ERROR = 33; const WRITE_ERROR_ACCESS_DENIED = 35; const WRITE_ERROR_CALLBACK_APP = 37; const FILESYSTEM_MOUNT_READWRITE_ERROR = 43; -const SERVICE_COULD_NOT_COPY_UPDATER = 49; -const SERVICE_STILL_APPLYING_TERMINATED = 50; -const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51; const WRITE_ERROR_FILE_COPY = 61; const WRITE_ERROR_DELETE_FILE = 62; const WRITE_ERROR_OPEN_PATCH_FILE = 63; @@ -127,21 +110,6 @@ const WRITE_ERRORS = [WRITE_ERROR, WRITE_ERROR_DELETE_BACKUP, WRITE_ERROR_EXTRACT]; -// Array of write errors to simplify checks for service errors -const SERVICE_ERRORS = [SERVICE_UPDATER_COULD_NOT_BE_STARTED, - SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS, - SERVICE_UPDATER_SIGN_ERROR, - SERVICE_UPDATER_COMPARE_ERROR, - SERVICE_UPDATER_IDENTITY_ERROR, - SERVICE_STILL_APPLYING_ON_SUCCESS, - SERVICE_STILL_APPLYING_ON_FAILURE, - SERVICE_UPDATER_NOT_FIXED_DRIVE, - SERVICE_COULD_NOT_LOCK_UPDATER, - SERVICE_INSTALLDIR_ERROR, - SERVICE_COULD_NOT_COPY_UPDATER, - SERVICE_STILL_APPLYING_TERMINATED, - SERVICE_STILL_APPLYING_NO_EXIT_CODE]; - // Error codes 80 through 99 are reserved for nsUpdateService.js and are not // defined in common/errors.h const FOTA_GENERAL_ERROR = 80; @@ -164,10 +132,6 @@ const DOWNLOAD_FOREGROUND_INTERVAL = 0; const UPDATE_WINDOW_NAME = "Update:Wizard"; -// The number of consecutive failures when updating using the service before -// setting the app.update.service.enabled preference to false. -const DEFAULT_SERVICE_MAX_ERRORS = 10; - // The number of consecutive socket errors to allow before falling back to // downloading a different MAR file or failing if already downloading the full. const DEFAULT_SOCKET_MAX_ERRORS = 10; @@ -392,15 +356,7 @@ function getElevationRequired() { * @return true if an update can be applied, false otherwise */ function getCanApplyUpdates() { - let useService = false; - if (shouldUseService()) { - // No need to perform directory write checks, the maintenance service will - // be able to write to all directories. - LOG("getCanApplyUpdates - bypass the write checks because we'll use the service"); - useService = true; - } - - if (!useService && AppConstants.platform != "macosx") { + if (AppConstants.platform != "macosx") { try { let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); LOG("getCanApplyUpdates - testing write access " + updateTestFile.path); @@ -478,7 +434,7 @@ function getCanApplyUpdates() { // No write privileges to install directory return false; } - } // if (!useService) + } LOG("getCanApplyUpdates - able to apply updates"); return true; @@ -544,13 +500,6 @@ function getCanStageUpdates() { return false; } - if (AppConstants.platform == "win" && shouldUseService()) { - // No need to perform directory write checks, the maintenance service will - // be able to write to all directories. - LOG("getCanStageUpdates - able to stage updates using the service"); - return true; - } - if (!hasUpdateMutex()) { LOG("getCanStageUpdates - unable to apply updates because another " + "instance of the application is already handling updates for this " + @@ -778,120 +727,6 @@ function writeVersionFile(dir, version) { } /** - * Determines if the service should be used to attempt an update - * or not. - * - * @return true if the service should be used for updates. - */ -function shouldUseService() { - // This function will return true if the mantenance service should be used if - // all of the following conditions are met: - // 1) This build was done with the maintenance service enabled - // 2) The maintenance service is installed - // 3) The pref for using the service is enabled - // 4) The Windows version is XP Service Pack 3 or above (for SHA-2 support) - // The maintenance service requires SHA-2 support because we sign our binaries - // with a SHA-2 certificate and the certificate is verified before the binary - // is launched. - if (!AppConstants.MOZ_MAINTENANCE_SERVICE || !isServiceInstalled() || - !getPref("getBoolPref", PREF_APP_UPDATE_SERVICE_ENABLED, false) || - !AppConstants.isPlatformAndVersionAtLeast("win", "5.1") /* WinXP */) { - return false; - } - - // If it's newer than XP, then the service pack doesn't matter. - if (Services.sysinfo.getProperty("version") != "5.1") { - return true; - } - - // If the Windows version is XP, we also need to check the service pack. - // We'll return false if only < SP3 is installed, or if we can't tell. - // Check the service pack level by calling GetVersionEx via ctypes. - const BYTE = ctypes.uint8_t; - const WORD = ctypes.uint16_t; - const DWORD = ctypes.uint32_t; - const WCHAR = ctypes.char16_t; - const BOOL = ctypes.int; - // This structure is described at: - // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx - const SZCSDVERSIONLENGTH = 128; - const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', - [ - {dwOSVersionInfoSize: DWORD}, - {dwMajorVersion: DWORD}, - {dwMinorVersion: DWORD}, - {dwBuildNumber: DWORD}, - {dwPlatformId: DWORD}, - {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, - {wServicePackMajor: WORD}, - {wServicePackMinor: WORD}, - {wSuiteMask: WORD}, - {wProductType: BYTE}, - {wReserved: BYTE} - ]); - - let kernel32 = false; - try { - kernel32 = ctypes.open("Kernel32"); - } catch (e) { - Cu.reportError("Unable to open kernel32! " + e); - return false; - } - - if (kernel32) { - try { - try { - let GetVersionEx = kernel32.declare("GetVersionExW", - ctypes.default_abi, - BOOL, - OSVERSIONINFOEXW.ptr); - let winVer = OSVERSIONINFOEXW(); - winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; - - if (0 !== GetVersionEx(winVer.address())) { - return winVer.wServicePackMajor >= 3; - } - Cu.reportError("Unknown failure in GetVersionEX (returned 0)"); - return false; - } catch (e) { - Cu.reportError("Error getting service pack information. Exception: " + e); - return false; - } - } finally { - kernel32.close(); - } - } - - // If the service pack check couldn't be done, assume we can't use the service. - return false; -} - -/** - * Determines if the service is is installed. - * - * @return true if the service is installed. - */ -function isServiceInstalled() { - if (AppConstants.MOZ_MAINTENANCE_SERVICE && AppConstants.platform == "win") { - let installed = 0; - try { - let wrk = Cc["@mozilla.org/windows-registry-key;1"]. - createInstance(Ci.nsIWindowsRegKey); - wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE, - "SOFTWARE\\Mozilla\\MaintenanceService", - wrk.ACCESS_READ | wrk.WOW64_64); - installed = wrk.readIntValue("Installed"); - wrk.close(); - } catch (e) { - } - installed = installed == 1; // convert to bool - LOG("isServiceInstalled = " + installed); - return installed; - } - return false; -} - -/** * Removes the contents of the updates patch directory and rotates the update * logs when present. If the update.log exists in the patch directory this will * move the last-update.log if it exists to backup-update.log in the parent @@ -1102,35 +937,6 @@ function handleUpdateFailure(update, errorCode) { Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); } - // Replace with Array.prototype.includes when it has stabilized. - if (SERVICE_ERRORS.indexOf(update.errorCode) != -1) { - var failCount = getPref("getIntPref", - PREF_APP_UPDATE_SERVICE_ERRORS, 0); - var maxFail = getPref("getIntPref", - PREF_APP_UPDATE_SERVICE_MAXERRORS, - DEFAULT_SERVICE_MAX_ERRORS); - // Prevent the preference from setting a value greater than 10. - maxFail = Math.min(maxFail, 10); - // As a safety, when the service reaches maximum failures, it will - // disable itself and fallback to using the normal update mechanism - // without the service. - if (failCount >= maxFail) { - Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); - Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); - } else { - failCount++; - Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, - failCount); - } - - writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); - return true; - } - - if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ERRORS)) { - Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); - } - return false; } @@ -1189,9 +995,6 @@ function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) { case STATE_PENDING: stateCode = 4; break; - case STATE_PENDING_SERVICE: - stateCode = 5; - break; case STATE_APPLYING: stateCode = 6; break; @@ -1201,9 +1004,6 @@ function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) { case STATE_APPLIED_OS: stateCode = 8; break; - case STATE_APPLIED_SERVICE: - stateCode = 9; - break; case STATE_SUCCEEDED: stateCode = 10; break; @@ -1831,8 +1631,8 @@ UpdateService.prototype = { // version and nsUpdateDriver.cpp skipped updating due to the version being // older than the current version. if (update && update.appVersion && - (status == STATE_PENDING || status == STATE_PENDING_SERVICE || - status == STATE_APPLIED || status == STATE_APPLIED_SERVICE || + (status == STATE_PENDING || + status == STATE_APPLIED || status == STATE_PENDING_ELEVATE)) { if (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 || Services.vc.compare(update.appVersion, Services.appinfo.version) == 0 && @@ -1870,9 +1670,7 @@ UpdateService.prototype = { // that state to "applying" and we just wait and hope for the best. // If it's "applying", we know that we've already been here once, so // we really want to start from a clean state. - if (update && - (update.state == STATE_PENDING || - update.state == STATE_PENDING_SERVICE)) { + if (update && (update.state == STATE_PENDING)) { LOG("UpdateService:_postUpdateProcessing - patch found in applying " + "state for the first time"); update.state = STATE_APPLYING; @@ -2121,27 +1919,13 @@ UpdateService.prototype = { this._pingSuffix, PREF_APP_UPDATE_CANCELATIONS_OSX, 0, 0); } - if (AppConstants.MOZ_MAINTENANCE_SERVICE) { - // Histogram IDs: - // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL - // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY - AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_" + - this._pingSuffix, - PREF_APP_UPDATE_SERVICE_ENABLED, true); - // Histogram IDs: - // UPDATE_PREF_SERVICE_ERRORS_EXTERNAL - // UPDATE_PREF_SERVICE_ERRORS_NOTIFY - AUSTLMY.pingIntPref("UPDATE_PREF_SERVICE_ERRORS_" + this._pingSuffix, - PREF_APP_UPDATE_SERVICE_ERRORS, 0, 0); - if (AppConstants.platform == "win") { - // Histogram IDs: - // UPDATE_SERVICE_INSTALLED_EXTERNAL - // UPDATE_SERVICE_INSTALLED_NOTIFY - // UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL - // UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY - AUSTLMY.pingServiceInstallStatus(this._pingSuffix, isServiceInstalled()); - } - } + let prefType = Services.prefs.getPrefType(PREF_APP_UPDATE_URL_OVERRIDE); + let overridePrefHasValue = prefType != Ci.nsIPrefBranch.PREF_INVALID; + // Histogram IDs: + // UPDATE_HAS_PREF_URL_OVERRIDE_EXTERNAL + // UPDATE_HAS_PREF_URL_OVERRIDE_NOTIFY + AUSTLMY.pingGeneric("UPDATE_HAS_PREF_URL_OVERRIDE_" + this._pingSuffix, + overridePrefHasValue, false); // If a download is in progress or the patch has been staged do nothing. if (this.isDownloading) { @@ -2151,7 +1935,7 @@ UpdateService.prototype = { if (this._downloader && this._downloader.patchIsStaged) { let readState = readStatusFile(getUpdatesDir()); - if (readState == STATE_PENDING || readState == STATE_PENDING_SERVICE || + if (readState == STATE_PENDING || readState == STATE_PENDING_ELEVATE) { AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED); } else { @@ -2173,8 +1957,18 @@ UpdateService.prototype = { } else if (!UpdateUtils.ABI) { AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI); } else if (!validUpdateURL) { - AUSTLMY.pingCheckCode(this._pingSuffix, - AUSTLMY.CHK_INVALID_DEFAULT_URL); + if (overridePrefHasValue) { + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_URL_OVERRIDE)) { + AUSTLMY.pingCheckCode(this._pingSuffix, + AUSTLMY.CHK_INVALID_USER_OVERRIDE_URL); + } else { + AUSTLMY.pingCheckCode(this._pingSuffix, + AUSTLMY.CHK_INVALID_DEFAULT_OVERRIDE_URL); + } + } else { + AUSTLMY.pingCheckCode(this._pingSuffix, + AUSTLMY.CHK_INVALID_DEFAULT_URL); + } } else if (!getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) { AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED); } else if (!hasUpdateMutex()) { @@ -2885,8 +2679,8 @@ UpdateManager.prototype = { for (let i = updates.length - 1; i >= 0; --i) { let state = updates[i].state; if (state == STATE_NONE || state == STATE_DOWNLOADING || - state == STATE_APPLIED || state == STATE_APPLIED_SERVICE || - state == STATE_PENDING || state == STATE_PENDING_SERVICE || + state == STATE_APPLIED || + state == STATE_PENDING || state == STATE_PENDING_ELEVATE) { updates.splice(i, 1); } @@ -2931,9 +2725,6 @@ UpdateManager.prototype = { update.QueryInterface(Ci.nsIWritablePropertyBag); update.setProperty("stagingFailed", "true"); } - if (update.state == STATE_APPLIED && shouldUseService()) { - writeStatusFile(getUpdatesDir(), update.state = STATE_APPLIED_SERVICE); - } // Send an observer notification which the update wizard uses in // order to update its UI. @@ -2949,9 +2740,7 @@ UpdateManager.prototype = { } if (update.state == STATE_APPLIED || - update.state == STATE_APPLIED_SERVICE || update.state == STATE_PENDING || - update.state == STATE_PENDING_SERVICE || update.state == STATE_PENDING_ELEVATE) { // Notify the user that an update has been staged and is ready for // installation (i.e. that they should restart the application). @@ -3023,11 +2812,16 @@ Checker.prototype = { getUpdateURL: function UC_getUpdateURL(force) { this._forced = force; - let url; - try { - url = Services.prefs.getDefaultBranch(null). - getCharPref(PREF_APP_UPDATE_URL); - } catch (e) { + // Use the override URL if specified. + let url = getPref("getCharPref", PREF_APP_UPDATE_URL_OVERRIDE, null); + + // Otherwise, construct the update URL from component parts. + if (!url) { + try { + url = Services.prefs.getDefaultBranch(null). + getCharPref(PREF_APP_UPDATE_URL); + } catch (e) { + } } if (!url || url == "") { @@ -3309,9 +3103,9 @@ Downloader.prototype = { // Note that if we decide to download and apply new updates after another // update has been successfully applied in the background, we need to stop // checking for the APPLIED state here. - return readState == STATE_PENDING || readState == STATE_PENDING_SERVICE || + return readState == STATE_PENDING || readState == STATE_PENDING_ELEVATE || - readState == STATE_APPLIED || readState == STATE_APPLIED_SERVICE; + readState == STATE_APPLIED; }, /** @@ -3428,7 +3222,7 @@ Downloader.prototype = { return selectedPatch; } - if (state == STATE_PENDING || state == STATE_PENDING_SERVICE || + if (state == STATE_PENDING || state == STATE_PENDING_ELEVATE) { LOG("Downloader:_selectPatch - already downloaded and staged"); return null; @@ -3721,9 +3515,7 @@ Downloader.prototype = { "max fail: " + maxFail + ", " + "retryTimeout: " + retryTimeout); if (Components.isSuccessCode(status)) { if (this._verifyDownload()) { - if (shouldUseService()) { - state = STATE_PENDING_SERVICE; - } else if (getElevationRequired()) { + if (getElevationRequired()) { state = STATE_PENDING_ELEVATE; } else { state = STATE_PENDING; @@ -3873,7 +3665,7 @@ Downloader.prototype = { return; } - if (state == STATE_PENDING || state == STATE_PENDING_SERVICE || + if (state == STATE_PENDING || state == STATE_PENDING_ELEVATE) { if (getCanStageUpdates()) { LOG("Downloader:onStopRequest - attempting to stage update: " + diff --git a/toolkit/mozapps/update/tests/TestAUSHelper.cpp b/toolkit/mozapps/update/tests/TestAUSHelper.cpp index f71103b7a..a20c4a79a 100644 --- a/toolkit/mozapps/update/tests/TestAUSHelper.cpp +++ b/toolkit/mozapps/update/tests/TestAUSHelper.cpp @@ -217,16 +217,8 @@ int NS_main(int argc, NS_tchar **argv) } if (!NS_tstrcmp(argv[1], NS_T("check-signature"))) { -#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE) - if (ERROR_SUCCESS == VerifyCertificateTrustForFile(argv[2])) { - return 0; - } else { - return 1; - } -#else - // Not implemented on non-Windows platforms + // Not implemented return 1; -#endif } if (!NS_tstrcmp(argv[1], NS_T("setup-symlink"))) { diff --git a/toolkit/mozapps/update/tests/moz.build b/toolkit/mozapps/update/tests/moz.build index 842ec7f90..94949b17f 100644 --- a/toolkit/mozapps/update/tests/moz.build +++ b/toolkit/mozapps/update/tests/moz.build @@ -15,9 +15,6 @@ XPCSHELL_TESTS_MANIFESTS += [ 'unit_base_updater/xpcshell.ini' ] -if CONFIG['MOZ_MAINTENANCE_SERVICE']: - XPCSHELL_TESTS_MANIFESTS += ['unit_service_updater/xpcshell.ini'] - SimplePrograms([ 'TestAUSHelper', 'TestAUSReadStrings', @@ -47,9 +44,6 @@ for var in ('MOZ_APP_NAME', 'MOZ_APP_BASENAME', 'MOZ_APP_DISPLAYNAME', DEFINES['NS_NO_XPCOM'] = True -if CONFIG['MOZ_MAINTENANCE_SERVICE']: - DEFINES['MOZ_MAINTENANCE_SERVICE'] = CONFIG['MOZ_MAINTENANCE_SERVICE'] - # For debugging purposes only #DEFINES['DISABLE_UPDATER_AUTHENTICODE_CHECK'] = True diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp index dbf50329c..c0b01bfbc 100644 --- a/toolkit/mozapps/update/updater/updater.cpp +++ b/toolkit/mozapps/update/updater/updater.cpp @@ -119,9 +119,6 @@ struct UpdateServerThreadArgs #endif #ifdef XP_WIN -#ifdef MOZ_MAINTENANCE_SERVICE -#include "registrycertificates.h" -#endif BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, @@ -290,7 +287,6 @@ static ArchiveReader gArchiveReader; static bool gSucceeded = false; static bool sStagedUpdate = false; static bool sReplaceRequest = false; -static bool sUsingService = false; static bool sIsOSUpdate = false; #ifdef XP_WIN @@ -1954,8 +1950,7 @@ PatchIfFile::Finish(int status) /** * Launch the post update application (helper.exe). It takes in the path of the - * callback application to calculate the path of helper.exe. For service updates - * this is called from both the system account and the current user account. + * callback application to calculate the path of helper.exe. * * @param installationDir The path to the callback application binary. * @param updateInfoDir The directory where update info is stored. @@ -2024,13 +2019,6 @@ LaunchWinPostProcess(const WCHAR *installationDir, return false; } -#if !defined(TEST_UPDATER) && defined(MOZ_MAINTENANCE_SERVICE) - if (sUsingService && - !DoesBinaryMatchAllowedCertificates(installationDir, exefullpath)) { - return false; - } -#endif - WCHAR dlogFile[MAX_PATH + 1]; if (!PathGetSiblingFilePath(dlogFile, exefullpath, L"uninstall.update")) { return false; @@ -2054,12 +2042,6 @@ LaunchWinPostProcess(const WCHAR *installationDir, wcsncpy(cmdline, dummyArg, len); wcscat(cmdline, exearg); - if (sUsingService || - !_wcsnicmp(exeasync, L"false", 6) || - !_wcsnicmp(exeasync, L"0", 2)) { - async = false; - } - // We want to launch the post update helper app to update the Windows // registry even if there is a failure with removing the uninstall.update // file or copying the update.log file. @@ -2095,8 +2077,7 @@ LaunchWinPostProcess(const WCHAR *installationDir, static void LaunchCallbackApp(const NS_tchar *workingDir, int argc, - NS_tchar **argv, - bool usingService) + NS_tchar **argv) { putenv(const_cast<char*>("NO_EM_RESTART=")); putenv(const_cast<char*>("MOZ_LAUNCHED_CHILD=1")); @@ -2113,11 +2094,7 @@ LaunchCallbackApp(const NS_tchar *workingDir, #elif defined(XP_MACOSX) LaunchChild(argc, (const char**)argv); #elif defined(XP_WIN) - // Do not allow the callback to run when running an update through the - // service as session 0. The unelevated updater.exe will do the launching. - if (!usingService) { - WinLaunchChild(argv[0], argc, argv, nullptr); - } + WinLaunchChild(argv[0], argc, argv, nullptr); #else # warning "Need implementaton of LaunchCallbackApp" #endif @@ -2188,40 +2165,6 @@ WriteStatusFile(int status) WriteStatusFile(text); } -#ifdef MOZ_MAINTENANCE_SERVICE -/* - * Read the update.status file and sets isPendingService to true if - * the status is set to pending-service. - * - * @param isPendingService Out parameter for specifying if the status - * is set to pending-service or not. - * @return true if the information was retrieved and it is pending - * or pending-service. -*/ -static bool -IsUpdateStatusPendingService() -{ - NS_tchar filename[MAXPATHLEN]; - NS_tsnprintf(filename, sizeof(filename)/sizeof(filename[0]), - NS_T("%s/update.status"), gPatchDirPath); - - AutoFile file(NS_tfopen(filename, NS_T("rb"))); - if (file == nullptr) - return false; - - char buf[32] = { 0 }; - fread(buf, sizeof(buf), 1, file); - - const char kPendingService[] = "pending-service"; - const char kAppliedService[] = "applied-service"; - - return (strncmp(buf, kPendingService, - sizeof(kPendingService) - 1) == 0) || - (strncmp(buf, kAppliedService, - sizeof(kAppliedService) - 1) == 0); -} -#endif - #ifdef XP_WIN /* * Read the update.status file and sets isSuccess to true if @@ -2601,7 +2544,7 @@ UpdateThreadFunc(void *param) // updater application again in order to apply the update without // staging. if (sReplaceRequest) { - WriteStatusFile(sUsingService ? "pending-service" : "pending"); + WriteStatusFile("pending"); } else { WriteStatusFile(rv); } @@ -2668,17 +2611,6 @@ int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv, if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { fprintf(stderr, "The post update process was not launched"); } - - // The service update will only be executed if it is already installed. - // For first time installs of the service, the install will happen from - // the PostUpdate process. We do the service update process here - // because it's possible we are updating with updater.exe without the - // service if the service failed to apply the update. We want to update - // the service to a newer version in that case. If we are not running - // through the service, then MOZ_USING_SERVICE will not exist. - if (!sUsingService) { - StartServiceUpdate(gInstallDirPath); - } } EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0); #elif XP_MACOSX @@ -2690,8 +2622,7 @@ int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv, LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex, - sUsingService); + argv + callbackIndex); #ifdef XP_MACOSX } // if (!isElevated) #endif /* XP_MACOSX */ @@ -2765,8 +2696,6 @@ int NS_main(int argc, NS_tchar **argv) return 1; } - // This check is also performed in workmonitor.cpp since the maintenance - // service can be called directly. if (!IsValidFullPath(argv[1])) { // Since the status file is written to the patch directory and the patch // directory is invalid don't write the status file. @@ -2783,8 +2712,6 @@ int NS_main(int argc, NS_tchar **argv) // The directory containing the update information. NS_tstrncpy(gPatchDirPath, argv[1], MAXPATHLEN); - // This check is also performed in workmonitor.cpp since the maintenance - // service can be called directly. if (!IsValidFullPath(argv[2])) { WriteStatusFile(INVALID_INSTALL_DIR_PATH_ERROR); fprintf(stderr, "The install directory path is not valid for this " \ @@ -2809,23 +2736,7 @@ int NS_main(int argc, NS_tchar **argv) } #ifdef XP_WIN - bool useService = false; bool testOnlyFallbackKeyExists = false; - bool noServiceFallback = false; - - // We never want the service to be used unless we build with - // the maintenance service. -#ifdef MOZ_MAINTENANCE_SERVICE - useService = IsUpdateStatusPendingService(); -#ifdef TEST_UPDATER - noServiceFallback = EnvHasValue("MOZ_NO_SERVICE_FALLBACK"); - putenv(const_cast<char*>("MOZ_NO_SERVICE_FALLBACK=")); - // Our tests run with a different apply directory for each test. - // We use this registry key on our test slaves to store the - // allowed name/issuers. - testOnlyFallbackKeyExists = DoesFallbackKeyExist(); -#endif -#endif // Remove everything except close window from the context menu { @@ -2869,8 +2780,6 @@ int NS_main(int argc, NS_tchar **argv) } } - // This check is also performed in workmonitor.cpp since the maintenance - // service can be called directly. if (!IsValidFullPath(argv[3])) { WriteStatusFile(INVALID_WORKING_DIR_PATH_ERROR); fprintf(stderr, "The working directory path is not valid for this " \ @@ -2894,8 +2803,6 @@ int NS_main(int argc, NS_tchar **argv) *slash = NS_T('\0'); } - // These checks are also performed in workmonitor.cpp since the maintenance - // service can be called directly. if (argc > callbackIndex) { if (!IsValidFullPath(argv[callbackIndex])) { WriteStatusFile(INVALID_CALLBACK_PATH_ERROR); @@ -2977,8 +2884,6 @@ int NS_main(int argc, NS_tchar **argv) LOG(("WORKING DIRECTORY " LOG_S, gWorkingDirPath)); #if defined(XP_WIN) - // These checks are also performed in workmonitor.cpp since the maintenance - // service can be called directly. if (_wcsnicmp(gWorkingDirPath, gInstallDirPath, MAX_PATH) != 0) { if (!sStagedUpdate && !sReplaceRequest) { WriteStatusFile(INVALID_APPLYTO_DIR_ERROR); @@ -3030,23 +2935,11 @@ int NS_main(int argc, NS_tchar **argv) #endif #ifdef XP_WIN -#ifdef MOZ_MAINTENANCE_SERVICE - sUsingService = EnvHasValue("MOZ_USING_SERVICE"); - putenv(const_cast<char*>("MOZ_USING_SERVICE=")); -#endif - // lastFallbackError keeps track of the last error for the service not being - // used, in case of an error when fallback is not enabled we write the - // error to the update.status file. - // When fallback is disabled (MOZ_NO_SERVICE_FALLBACK does not exist) then - // we will instead fallback to not using the service and display a UAC prompt. - int lastFallbackError = FALLBACKKEY_UNKNOWN_ERROR; - // Launch a second instance of the updater with the runas verb on Windows // when write access is denied to the installation directory. HANDLE updateLockFileHandle = INVALID_HANDLE_VALUE; NS_tchar elevatedLockFilePath[MAXPATHLEN] = {NS_T('\0')}; - if (!sUsingService && - (argc > callbackIndex || sStagedUpdate || sReplaceRequest)) { + if (argc > callbackIndex || sStagedUpdate || sReplaceRequest) { NS_tchar updateLockFilePath[MAXPATHLEN]; if (sStagedUpdate) { // When staging an update, the lock file is: @@ -3104,8 +2997,7 @@ int NS_main(int argc, NS_tchar **argv) bool startedFromUnelevatedUpdater = GetFileAttributesW(elevatedLockFilePath) != INVALID_FILE_ATTRIBUTES; - // If we're running from the service, then we were started with the same - // token as the service so the permissions are already dropped. If we're + // If we're // running from an elevated updater that was started from an unelevated // updater, then we drop the permissions here. We do not drop the // permissions on the originally called updater because we use its token @@ -3116,8 +3008,7 @@ int NS_main(int argc, NS_tchar **argv) UACHelper::DisablePrivileges(nullptr); } - if (updateLockFileHandle == INVALID_HANDLE_VALUE || - (useService && testOnlyFallbackKeyExists && noServiceFallback)) { + if (updateLockFileHandle == INVALID_HANDLE_VALUE) { if (!_waccess(elevatedLockFilePath, F_OK) && NS_tremove(elevatedLockFilePath) != 0) { fprintf(stderr, "Unable to create elevated lock file! Exiting\n"); @@ -3144,105 +3035,11 @@ int NS_main(int argc, NS_tchar **argv) return 1; } - // Make sure the path to the updater to use for the update is on local. - // We do this check to make sure that file locking is available for - // race condition security checks. - if (useService) { - BOOL isLocal = FALSE; - useService = IsLocalFile(argv[0], isLocal) && isLocal; - } - - // If we have unprompted elevation we should NOT use the service - // for the update. Service updates happen with the SYSTEM account - // which has more privs than we need to update with. - // Windows 8 provides a user interface so users can configure this - // behavior and it can be configured in the registry in all Windows - // versions that support UAC. - if (useService) { - BOOL unpromptedElevation; - if (IsUnpromptedElevation(unpromptedElevation)) { - useService = !unpromptedElevation; - } - } - - // Make sure the service registry entries for the instsallation path - // are available. If not don't use the service. - if (useService) { - WCHAR maintenanceServiceKey[MAX_PATH + 1]; - if (CalculateRegistryPathFromFilePath(gInstallDirPath, - maintenanceServiceKey)) { - HKEY baseKey = nullptr; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, - maintenanceServiceKey, 0, - KEY_READ | KEY_WOW64_64KEY, - &baseKey) == ERROR_SUCCESS) { - RegCloseKey(baseKey); - } else { -#ifdef TEST_UPDATER - useService = testOnlyFallbackKeyExists; -#endif - if (!useService) { - lastFallbackError = FALLBACKKEY_NOKEY_ERROR; - } - } - } else { - useService = false; - lastFallbackError = FALLBACKKEY_REGPATH_ERROR; - } - } - - // Originally we used to write "pending" to update.status before - // launching the service command. This is no longer needed now - // since the service command is launched from updater.exe. If anything - // fails in between, we can fall back to using the normal update process - // on our own. - - // If we still want to use the service try to launch the service - // comamnd for the update. - if (useService) { - // If the update couldn't be started, then set useService to false so - // we do the update the old way. - DWORD ret = LaunchServiceSoftwareUpdateCommand(argc, (LPCWSTR *)argv); - useService = (ret == ERROR_SUCCESS); - // If the command was launched then wait for the service to be done. - if (useService) { - bool showProgressUI = false; - // Never show the progress UI when staging updates. - if (!sStagedUpdate) { - // We need to call this separately instead of allowing ShowProgressUI - // to initialize the strings because the service will move the - // ini file out of the way when running updater. - showProgressUI = !InitProgressUIStrings(); - } - // Wait for the service to stop for 5 seconds. If the service - // has still not stopped then show an indeterminate progress bar. - DWORD lastState = WaitForServiceStop(SVC_NAME, 5); - if (lastState != SERVICE_STOPPED) { - Thread t1; - if (t1.Run(WaitForServiceFinishThread, nullptr) == 0 && - showProgressUI) { - ShowProgressUI(true, false); - } - t1.Join(); - } - - lastState = WaitForServiceStop(SVC_NAME, 1); - if (lastState != SERVICE_STOPPED) { - // If the service doesn't stop after 10 minutes there is - // something seriously wrong. - lastFallbackError = FALLBACKKEY_SERVICE_NO_STOP_ERROR; - useService = false; - } - } else { - lastFallbackError = FALLBACKKEY_LAUNCH_ERROR; - } - } - - // If the service can't be used when staging an update, make sure that + // When staging an update, make sure that // the UAC prompt is not shown! In this case, just set the status to // pending and the update will be applied during the next startup. - if (!useService && sStagedUpdate) { + if (sStagedUpdate) { if (updateLockFileHandle != INVALID_HANDLE_VALUE) { CloseHandle(updateLockFileHandle); } @@ -3250,32 +3047,10 @@ int NS_main(int argc, NS_tchar **argv) return 0; } - // If we started the service command, and it finished, check the - // update.status file to make sure it succeeded, and if it did - // we need to manually start the PostUpdate process from the - // current user's session of this unelevated updater.exe the - // current process is running as. - // Note that we don't need to do this if we're just staging the update, - // as the PostUpdate step runs when performing the replacing in that case. - if (useService && !sStagedUpdate) { - bool updateStatusSucceeded = false; - if (IsUpdateStatusSucceeded(updateStatusSucceeded) && - updateStatusSucceeded) { - if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { - fprintf(stderr, "The post update process which runs as the user" - " for service update could not be launched."); - } - } - } - - // If we didn't want to use the service at all, or if an update was - // already happening, or launching the service command failed, then - // launch the elevated updater.exe as we do without the service. + // If an update was already happening, launch the elevated updater.exe. // We don't launch the elevated updater in the case that we did have - // write access all along because in that case the only reason we're - // using the service is because we are testing. - if (!useService && !noServiceFallback && - updateLockFileHandle == INVALID_HANDLE_VALUE) { + // write access all along because in that case we are testing. + if (updateLockFileHandle == INVALID_HANDLE_VALUE) { SHELLEXECUTEINFO sinfo; memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO)); sinfo.cbSize = sizeof(SHELLEXECUTEINFO); @@ -3301,32 +3076,21 @@ int NS_main(int argc, NS_tchar **argv) if (argc > callbackIndex) { LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex, sUsingService); + argv + callbackIndex); } CloseHandle(elevatedFileHandle); - if (!useService && !noServiceFallback && - INVALID_HANDLE_VALUE == updateLockFileHandle) { - // We didn't use the service and we did run the elevated updater.exe. + if (INVALID_HANDLE_VALUE == updateLockFileHandle) { + // We ran the elevated updater.exe. // The elevated updater.exe is responsible for writing out the // update.status file. return 0; - } else if (useService) { - // The service command was launched. The service is responsible for - // writing out the update.status file. - if (updateLockFileHandle != INVALID_HANDLE_VALUE) { - CloseHandle(updateLockFileHandle); - } - return 0; } else { - // Otherwise the service command was not launched at all. // We are only reaching this code path because we had write access // all along to the directory and a fallback key existed, and we - // have fallback disabled (MOZ_NO_SERVICE_FALLBACK env var exists). - // We only currently use this env var from XPCShell tests. + // have fallback disabled. CloseHandle(updateLockFileHandle); - WriteStatusFile(lastFallbackError); return 0; } } @@ -3386,7 +3150,7 @@ int NS_main(int argc, NS_tchar **argv) EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); if (argc > callbackIndex) { LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex, sUsingService); + argv + callbackIndex); } return 1; } @@ -3440,8 +3204,7 @@ int NS_main(int argc, NS_tchar **argv) if (argc > callbackIndex) { LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex, - sUsingService); + argv + callbackIndex); } return 1; } @@ -3504,8 +3267,7 @@ int NS_main(int argc, NS_tchar **argv) EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); LaunchCallbackApp(argv[callbackIndex], argc - callbackIndex, - argv + callbackIndex, - sUsingService); + argv + callbackIndex); return 1; } @@ -3553,8 +3315,7 @@ int NS_main(int argc, NS_tchar **argv) EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex, - sUsingService); + argv + callbackIndex); return 1; } LOG(("NS_main: callback app file in use, continuing without " \ diff --git a/toolkit/mozapps/webextensions/AddonContentPolicy.cpp b/toolkit/mozapps/webextensions/AddonContentPolicy.cpp deleted file mode 100644 index 90e53b2ea..000000000 --- a/toolkit/mozapps/webextensions/AddonContentPolicy.cpp +++ /dev/null @@ -1,478 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* 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 "AddonContentPolicy.h" - -#include "mozilla/dom/nsCSPUtils.h" -#include "nsCOMPtr.h" -#include "nsContentPolicyUtils.h" -#include "nsContentTypeParser.h" -#include "nsContentUtils.h" -#include "nsIConsoleService.h" -#include "nsIContentSecurityPolicy.h" -#include "nsIContent.h" -#include "nsIDocument.h" -#include "nsIEffectiveTLDService.h" -#include "nsIScriptError.h" -#include "nsIStringBundle.h" -#include "nsIUUIDGenerator.h" -#include "nsIURI.h" -#include "nsNetCID.h" -#include "nsNetUtil.h" - -using namespace mozilla; - -/* Enforces content policies for WebExtension scopes. Currently: - * - * - Prevents loading scripts with a non-default JavaScript version. - * - Checks custom content security policies for sufficiently stringent - * script-src and object-src directives. - */ - -#define VERSIONED_JS_BLOCKED_MESSAGE \ - u"Versioned JavaScript is a non-standard, deprecated extension, and is " \ - u"not supported in WebExtension code. For alternatives, please see: " \ - u"https://developer.mozilla.org/Add-ons/WebExtensions/Tips" - -AddonContentPolicy::AddonContentPolicy() -{ -} - -AddonContentPolicy::~AddonContentPolicy() -{ -} - -NS_IMPL_ISUPPORTS(AddonContentPolicy, nsIContentPolicy, nsIAddonContentPolicy) - -static nsresult -GetWindowIDFromContext(nsISupports* aContext, uint64_t *aResult) -{ - NS_ENSURE_TRUE(aContext, NS_ERROR_FAILURE); - - nsCOMPtr<nsIContent> content = do_QueryInterface(aContext); - NS_ENSURE_TRUE(content, NS_ERROR_FAILURE); - - nsCOMPtr<nsIDocument> document = content->OwnerDoc(); - NS_ENSURE_TRUE(document, NS_ERROR_FAILURE); - - nsCOMPtr<nsPIDOMWindowInner> window = document->GetInnerWindow(); - NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); - - *aResult = window->WindowID(); - return NS_OK; -} - -static nsresult -LogMessage(const nsAString &aMessage, nsIURI* aSourceURI, const nsAString &aSourceSample, - nsISupports* aContext) -{ - nsCOMPtr<nsIScriptError> error = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); - NS_ENSURE_TRUE(error, NS_ERROR_OUT_OF_MEMORY); - - nsCString sourceName = aSourceURI->GetSpecOrDefault(); - - uint64_t windowID = 0; - GetWindowIDFromContext(aContext, &windowID); - - nsresult rv = - error->InitWithWindowID(aMessage, NS_ConvertUTF8toUTF16(sourceName), - aSourceSample, 0, 0, nsIScriptError::errorFlag, - "JavaScript", windowID); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr<nsIConsoleService> console = do_GetService(NS_CONSOLESERVICE_CONTRACTID); - NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY); - - console->LogMessage(error); - return NS_OK; -} - - -// Content policy enforcement: - -NS_IMETHODIMP -AddonContentPolicy::ShouldLoad(uint32_t aContentType, - nsIURI* aContentLocation, - nsIURI* aRequestOrigin, - nsISupports* aContext, - const nsACString& aMimeTypeGuess, - nsISupports* aExtra, - nsIPrincipal* aRequestPrincipal, - int16_t* aShouldLoad) -{ - MOZ_ASSERT(aContentType == nsContentUtils::InternalContentPolicyTypeToExternal(aContentType), - "We should only see external content policy types here."); - - *aShouldLoad = nsIContentPolicy::ACCEPT; - - if (!aRequestOrigin) { - return NS_OK; - } - - // Only apply this policy to requests from documents loaded from - // moz-extension URLs, or to resources being loaded from moz-extension URLs. - bool equals; - if (!((NS_SUCCEEDED(aContentLocation->SchemeIs("moz-extension", &equals)) && equals) || - (NS_SUCCEEDED(aRequestOrigin->SchemeIs("moz-extension", &equals)) && equals))) { - return NS_OK; - } - - if (aContentType == nsIContentPolicy::TYPE_SCRIPT) { - NS_ConvertUTF8toUTF16 typeString(aMimeTypeGuess); - nsContentTypeParser mimeParser(typeString); - - // Reject attempts to load JavaScript scripts with a non-default version. - nsAutoString mimeType, version; - if (NS_SUCCEEDED(mimeParser.GetType(mimeType)) && - nsContentUtils::IsJavascriptMIMEType(mimeType) && - NS_SUCCEEDED(mimeParser.GetParameter("version", version))) { - *aShouldLoad = nsIContentPolicy::REJECT_REQUEST; - - LogMessage(NS_MULTILINE_LITERAL_STRING(VERSIONED_JS_BLOCKED_MESSAGE), - aRequestOrigin, typeString, aContext); - return NS_OK; - } - } - - return NS_OK; -} - -NS_IMETHODIMP -AddonContentPolicy::ShouldProcess(uint32_t aContentType, - nsIURI* aContentLocation, - nsIURI* aRequestOrigin, - nsISupports* aRequestingContext, - const nsACString& aMimeTypeGuess, - nsISupports* aExtra, - nsIPrincipal* aRequestPrincipal, - int16_t* aShouldProcess) -{ - MOZ_ASSERT(aContentType == nsContentUtils::InternalContentPolicyTypeToExternal(aContentType), - "We should only see external content policy types here."); - - *aShouldProcess = nsIContentPolicy::ACCEPT; - return NS_OK; -} - - -// CSP Validation: - -static const char* allowedSchemes[] = { - "blob", - "filesystem", - nullptr -}; - -static const char* allowedHostSchemes[] = { - "https", - "moz-extension", - nullptr -}; - -/** - * Validates a CSP directive to ensure that it is sufficiently stringent. - * In particular, ensures that: - * - * - No remote sources are allowed other than from https: schemes - * - * - No remote sources specify host wildcards for generic domains - * (*.blogspot.com, *.com, *) - * - * - All remote sources and local extension sources specify a host - * - * - No scheme sources are allowed other than blob:, filesystem:, - * moz-extension:, and https: - * - * - No keyword sources are allowed other than 'none', 'self', 'unsafe-eval', - * and hash sources. - */ -class CSPValidator final : public nsCSPSrcVisitor { - public: - CSPValidator(nsAString& aURL, CSPDirective aDirective, bool aDirectiveRequired = true) : - mURL(aURL), - mDirective(CSP_CSPDirectiveToString(aDirective)), - mFoundSelf(false) - { - // Start with the default error message for a missing directive, since no - // visitors will be called if the directive isn't present. - if (aDirectiveRequired) { - FormatError("csp.error.missing-directive"); - } - } - - // Visitors - - bool visitSchemeSrc(const nsCSPSchemeSrc& src) override - { - nsAutoString scheme; - src.getScheme(scheme); - - if (SchemeInList(scheme, allowedHostSchemes)) { - FormatError("csp.error.missing-host", scheme); - return false; - } - if (!SchemeInList(scheme, allowedSchemes)) { - FormatError("csp.error.illegal-protocol", scheme); - return false; - } - return true; - }; - - bool visitHostSrc(const nsCSPHostSrc& src) override - { - nsAutoString scheme, host; - - src.getScheme(scheme); - src.getHost(host); - - if (scheme.LowerCaseEqualsLiteral("https")) { - if (!HostIsAllowed(host)) { - FormatError("csp.error.illegal-host-wildcard", scheme); - return false; - } - } else if (scheme.LowerCaseEqualsLiteral("moz-extension")) { - // The CSP parser silently converts 'self' keywords to the origin - // URL, so we need to reconstruct the URL to see if it was present. - if (!mFoundSelf) { - nsAutoString url(u"moz-extension://"); - url.Append(host); - - mFoundSelf = url.Equals(mURL); - } - - if (host.IsEmpty() || host.EqualsLiteral("*")) { - FormatError("csp.error.missing-host", scheme); - return false; - } - } else if (!SchemeInList(scheme, allowedSchemes)) { - FormatError("csp.error.illegal-protocol", scheme); - return false; - } - - return true; - }; - - bool visitKeywordSrc(const nsCSPKeywordSrc& src) override - { - switch (src.getKeyword()) { - case CSP_NONE: - case CSP_SELF: - case CSP_UNSAFE_EVAL: - return true; - - default: - NS_ConvertASCIItoUTF16 keyword(CSP_EnumToKeyword(src.getKeyword())); - - FormatError("csp.error.illegal-keyword", keyword); - return false; - } - }; - - bool visitNonceSrc(const nsCSPNonceSrc& src) override - { - FormatError("csp.error.illegal-keyword", NS_LITERAL_STRING("'nonce-*'")); - return false; - }; - - bool visitHashSrc(const nsCSPHashSrc& src) override - { - return true; - }; - - // Accessors - - inline nsAString& GetError() - { - return mError; - }; - - inline bool FoundSelf() - { - return mFoundSelf; - }; - - - // Formatters - - template <typename... T> - inline void FormatError(const char* aName, const T ...aParams) - { - const char16_t* params[] = { mDirective.get(), aParams.get()... }; - FormatErrorParams(aName, params, MOZ_ARRAY_LENGTH(params)); - }; - - private: - // Validators - - bool HostIsAllowed(nsAString& host) - { - if (host.First() == '*') { - if (host.EqualsLiteral("*") || host[1] != '.') { - return false; - } - - host.Cut(0, 2); - - nsCOMPtr<nsIEffectiveTLDService> tldService = - do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); - - if (!tldService) { - return false; - } - - NS_ConvertUTF16toUTF8 cHost(host); - nsAutoCString publicSuffix; - - nsresult rv = tldService->GetPublicSuffixFromHost(cHost, publicSuffix); - - return NS_SUCCEEDED(rv) && !cHost.Equals(publicSuffix); - } - - return true; - }; - - bool SchemeInList(nsAString& scheme, const char** schemes) - { - for (; *schemes; schemes++) { - if (scheme.LowerCaseEqualsASCII(*schemes)) { - return true; - } - } - return false; - }; - - - // Formatters - - already_AddRefed<nsIStringBundle> - GetStringBundle() - { - nsCOMPtr<nsIStringBundleService> sbs = - mozilla::services::GetStringBundleService(); - NS_ENSURE_TRUE(sbs, nullptr); - - nsCOMPtr<nsIStringBundle> stringBundle; - sbs->CreateBundle("chrome://global/locale/extensions.properties", - getter_AddRefs(stringBundle)); - - return stringBundle.forget(); - }; - - void FormatErrorParams(const char* aName, const char16_t** aParams, int32_t aLength) - { - nsresult rv = NS_ERROR_FAILURE; - - nsCOMPtr<nsIStringBundle> stringBundle = GetStringBundle(); - - if (stringBundle) { - NS_ConvertASCIItoUTF16 name(aName); - - rv = stringBundle->FormatStringFromName(name.get(), aParams, aLength, - getter_Copies(mError)); - } - - if (NS_WARN_IF(NS_FAILED(rv))) { - mError.AssignLiteral("An unexpected error occurred"); - } - }; - - - // Data members - - nsAutoString mURL; - NS_ConvertASCIItoUTF16 mDirective; - nsXPIDLString mError; - - bool mFoundSelf; -}; - -/** - * Validates a custom content security policy string for use by an add-on. - * In particular, ensures that: - * - * - Both object-src and script-src directives are present, and meet - * the policies required by the CSPValidator class - * - * - The script-src directive includes the source 'self' - */ -NS_IMETHODIMP -AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString, - nsAString& aResult) -{ - nsresult rv; - - // Validate against a randomly-generated extension origin. - // There is no add-on-specific behavior in the CSP code, beyond the ability - // for add-ons to specify a custom policy, but the parser requires a valid - // origin in order to operate correctly. - nsAutoString url(u"moz-extension://"); - { - nsCOMPtr<nsIUUIDGenerator> uuidgen = services::GetUUIDGenerator(); - NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE); - - nsID id; - rv = uuidgen->GenerateUUIDInPlace(&id); - NS_ENSURE_SUCCESS(rv, rv); - - char idString[NSID_LENGTH]; - id.ToProvidedString(idString); - - MOZ_RELEASE_ASSERT(idString[0] == '{' && idString[NSID_LENGTH - 2] == '}', - "UUID generator did not return a valid UUID"); - - url.AppendASCII(idString + 1, NSID_LENGTH - 3); - } - - - RefPtr<BasePrincipal> principal = - BasePrincipal::CreateCodebasePrincipal(NS_ConvertUTF16toUTF8(url)); - - nsCOMPtr<nsIContentSecurityPolicy> csp; - rv = principal->EnsureCSP(nullptr, getter_AddRefs(csp)); - NS_ENSURE_SUCCESS(rv, rv); - - - csp->AppendPolicy(aPolicyString, false, false); - - const nsCSPPolicy* policy = csp->GetPolicy(0); - if (!policy) { - CSPValidator validator(url, nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE); - aResult.Assign(validator.GetError()); - return NS_OK; - } - - bool haveValidDefaultSrc = false; - { - CSPDirective directive = nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; - CSPValidator validator(url, directive); - - haveValidDefaultSrc = policy->visitDirectiveSrcs(directive, &validator); - } - - aResult.SetIsVoid(true); - { - CSPDirective directive = nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE; - CSPValidator validator(url, directive, !haveValidDefaultSrc); - - if (!policy->visitDirectiveSrcs(directive, &validator)) { - aResult.Assign(validator.GetError()); - } else if (!validator.FoundSelf()) { - validator.FormatError("csp.error.missing-source", NS_LITERAL_STRING("'self'")); - aResult.Assign(validator.GetError()); - } - } - - if (aResult.IsVoid()) { - CSPDirective directive = nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE; - CSPValidator validator(url, directive, !haveValidDefaultSrc); - - if (!policy->visitDirectiveSrcs(directive, &validator)) { - aResult.Assign(validator.GetError()); - } - } - - return NS_OK; -} diff --git a/toolkit/mozapps/webextensions/AddonContentPolicy.h b/toolkit/mozapps/webextensions/AddonContentPolicy.h deleted file mode 100644 index 4c8af4828..000000000 --- a/toolkit/mozapps/webextensions/AddonContentPolicy.h +++ /dev/null @@ -1,22 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* 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 "nsIContentPolicy.h" -#include "nsIAddonPolicyService.h" - -class AddonContentPolicy : public nsIContentPolicy, - public nsIAddonContentPolicy -{ -protected: - virtual ~AddonContentPolicy(); - -public: - AddonContentPolicy(); - - NS_DECL_ISUPPORTS - NS_DECL_NSICONTENTPOLICY - NS_DECL_NSIADDONCONTENTPOLICY -}; diff --git a/toolkit/mozapps/webextensions/AddonManager.jsm b/toolkit/mozapps/webextensions/AddonManager.jsm deleted file mode 100644 index a3bcbb504..000000000 --- a/toolkit/mozapps/webextensions/AddonManager.jsm +++ /dev/null @@ -1,3666 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -// Cannot use Services.appinfo here, or else xpcshell-tests will blow up, as -// most tests later register different nsIAppInfo implementations, which -// wouldn't be reflected in Services.appinfo anymore, as the lazy getter -// underlying it would have been initialized if we used it here. -if ("@mozilla.org/xre/app-info;1" in Cc) { - let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); - if (runtime.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { - // Refuse to run in child processes. - throw new Error("You cannot use the AddonManager in child processes!"); - } -} - -Cu.import("resource://gre/modules/AppConstants.jsm"); - -const MOZ_COMPATIBILITY_NIGHTLY = !['aurora', 'beta', 'release', 'esr'].includes(AppConstants.MOZ_UPDATE_CHANNEL); - -const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion"; -const PREF_DEFAULT_PROVIDERS_ENABLED = "extensions.defaultProviders.enabled"; -const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled"; -const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion"; -const PREF_EM_LAST_PLATFORM_VERSION = "extensions.lastPlatformVersion"; -const PREF_EM_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault"; -const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility"; -const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; -const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url"; -const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; -const PREF_APP_UPDATE_AUTO = "app.update.auto"; -const PREF_EM_HOTFIX_ID = "extensions.hotfix.id"; -const PREF_EM_HOTFIX_LASTVERSION = "extensions.hotfix.lastVersion"; -const PREF_EM_HOTFIX_URL = "extensions.hotfix.url"; -const PREF_EM_CERT_CHECKATTRIBUTES = "extensions.hotfix.cert.checkAttributes"; -const PREF_EM_HOTFIX_CERTS = "extensions.hotfix.certs."; -const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; -const PREF_SELECTED_LOCALE = "general.useragent.locale"; -const UNKNOWN_XPCOM_ABI = "unknownABI"; - -const PREF_MIN_WEBEXT_PLATFORM_VERSION = "extensions.webExtensionsMinPlatformVersion"; -const PREF_WEBAPI_TESTING = "extensions.webapi.testing"; - -const UPDATE_REQUEST_VERSION = 2; -const CATEGORY_UPDATE_PARAMS = "extension-update-params"; - -const XMLURI_BLOCKLIST = "http://www.mozilla.org/2006/addons-blocklist"; - -const KEY_PROFILEDIR = "ProfD"; -const KEY_APPDIR = "XCurProcD"; -const FILE_BLOCKLIST = "blocklist.xml"; - -const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi; -const PREF_EM_CHECK_COMPATIBILITY = "extensions.enableCompatibilityChecking"; - -const TOOLKIT_ID = "toolkit@mozilla.org"; - -const VALID_TYPES_REGEXP = /^[\w\-]+$/; - -const WEBAPI_INSTALL_HOSTS = ["addons.mozilla.org", "testpilot.firefox.com"]; -const WEBAPI_TEST_INSTALL_HOSTS = [ - "addons.allizom.org", "addons-dev.allizom.org", - "testpilot.stage.mozaws.net", "testpilot.dev.mozaws.net", - "example.com", -]; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/AsyncShutdown.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", - "resource://gre/modules/addons/AddonRepository.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Extension", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); - -XPCOMUtils.defineLazyGetter(this, "CertUtils", function() { - let certUtils = {}; - Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); - return certUtils; -}); - -const INTEGER = /^[1-9]\d*$/; - -this.EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ]; - -const CATEGORY_PROVIDER_MODULE = "addon-provider-module"; - -// A list of providers to load by default -const DEFAULT_PROVIDERS = [ - "resource://gre/modules/addons/XPIProvider.jsm", - "resource://gre/modules/LightweightThemeManager.jsm" -]; - -Cu.import("resource://gre/modules/Log.jsm"); -// Configure a logger at the parent 'addons' level to format -// messages for all the modules under addons.* -const PARENT_LOGGER_ID = "addons"; -var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID); -parentLogger.level = Log.Level.Warn; -var formatter = new Log.BasicFormatter(); -// Set parent logger (and its children) to append to -// the Javascript section of the Browser Console -parentLogger.addAppender(new Log.ConsoleAppender(formatter)); -// Set parent logger (and its children) to -// also append to standard out -parentLogger.addAppender(new Log.DumpAppender(formatter)); - -// Create a new logger (child of 'addons' logger) -// for use by the Addons Manager -const LOGGER_ID = "addons.manager"; -var logger = Log.repository.getLogger(LOGGER_ID); - -// Provide the ability to enable/disable logging -// messages at runtime. -// If the "extensions.logging.enabled" preference is -// missing or 'false', messages at the WARNING and higher -// severity should be logged to the JS console and standard error. -// If "extensions.logging.enabled" is set to 'true', messages -// at DEBUG and higher should go to JS console and standard error. -const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; -const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; - -const UNNAMED_PROVIDER = "<unnamed-provider>"; -function providerName(aProvider) { - return aProvider.name || UNNAMED_PROVIDER; -} - -/** - * Preference listener which listens for a change in the - * "extensions.logging.enabled" preference and changes the logging level of the - * parent 'addons' level logger accordingly. - */ -var PrefObserver = { - init: function() { - Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false); - Services.obs.addObserver(this, "xpcom-shutdown", false); - this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED); - }, - - observe: function(aSubject, aTopic, aData) { - if (aTopic == "xpcom-shutdown") { - Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this); - Services.obs.removeObserver(this, "xpcom-shutdown"); - } - else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) { - let debugLogEnabled = false; - try { - debugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED); - } - catch (e) { - } - if (debugLogEnabled) { - parentLogger.level = Log.Level.Debug; - } - else { - parentLogger.level = Log.Level.Warn; - } - } - } -}; - -PrefObserver.init(); - -/** - * Calls a callback method consuming any thrown exception. Any parameters after - * the callback parameter will be passed to the callback. - * - * @param aCallback - * The callback method to call - */ -function safeCall(aCallback, ...aArgs) { - try { - aCallback.apply(null, aArgs); - } - catch (e) { - logger.warn("Exception calling callback", e); - } -} - -/** - * Creates a function that will call the passed callback catching and logging - * any exceptions. - * - * @param aCallback - * The callback method to call - */ -function makeSafe(aCallback) { - return function(...aArgs) { - safeCall(aCallback, ...aArgs); - } -} - -/** - * Report an exception thrown by a provider API method. - */ -function reportProviderError(aProvider, aMethod, aError) { - let method = `provider ${providerName(aProvider)}.${aMethod}`; - AddonManagerPrivate.recordException("AMI", method, aError); - logger.error("Exception calling " + method, aError); -} - -/** - * Calls a method on a provider if it exists and consumes any thrown exception. - * Any parameters after the aDefault parameter are passed to the provider's method. - * - * @param aProvider - * The provider to call - * @param aMethod - * The method name to call - * @param aDefault - * A default return value if the provider does not implement the named - * method or throws an error. - * @return the return value from the provider, or aDefault if the provider does not - * implement method or throws an error - */ -function callProvider(aProvider, aMethod, aDefault, ...aArgs) { - if (!(aMethod in aProvider)) - return aDefault; - - try { - return aProvider[aMethod].apply(aProvider, aArgs); - } - catch (e) { - reportProviderError(aProvider, aMethod, e); - return aDefault; - } -} - -/** - * Calls a method on a provider if it exists and consumes any thrown exception. - * Parameters after aMethod are passed to aProvider.aMethod(). - * The last parameter must be a callback function. - * If the provider does not implement the method, or the method throws, calls - * the callback with 'undefined'. - * - * @param aProvider - * The provider to call - * @param aMethod - * The method name to call - */ -function callProviderAsync(aProvider, aMethod, ...aArgs) { - let callback = aArgs[aArgs.length - 1]; - if (!(aMethod in aProvider)) { - callback(undefined); - return undefined; - } - try { - return aProvider[aMethod].apply(aProvider, aArgs); - } - catch (e) { - reportProviderError(aProvider, aMethod, e); - callback(undefined); - return undefined; - } -} - -/** - * Calls a method on a provider if it exists and consumes any thrown exception. - * Parameters after aMethod are passed to aProvider.aMethod() and an additional - * callback is added for the provider to return a result to. - * - * @param aProvider - * The provider to call - * @param aMethod - * The method name to call - * @return {Promise} - * @resolves The result the provider returns, or |undefined| if the provider - * does not implement the method or the method throws. - * @rejects Never - */ -function promiseCallProvider(aProvider, aMethod, ...aArgs) { - return new Promise(resolve => { - callProviderAsync(aProvider, aMethod, ...aArgs, resolve); - }); -} - -/** - * Gets the currently selected locale for display. - * @return the selected locale or "en-US" if none is selected - */ -function getLocale() { - try { - if (Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE)) - return Services.locale.getLocaleComponentForUserAgent(); - } - catch (e) { } - - try { - let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE, - Ci.nsIPrefLocalizedString); - if (locale) - return locale; - } - catch (e) { } - - try { - return Services.prefs.getCharPref(PREF_SELECTED_LOCALE); - } - catch (e) { } - - return "en-US"; -} - -function webAPIForAddon(addon) { - if (!addon) { - return null; - } - - let result = {}; - - // By default just pass through any plain property, the webidl will - // control access. Also filter out private properties, regular Addon - // objects are okay but MockAddon used in tests has non-serializable - // private properties. - for (let prop in addon) { - if (prop[0] != "_" && typeof(addon[prop]) != "function") { - result[prop] = addon[prop]; - } - } - - // A few properties are computed for a nicer API - result.isEnabled = !addon.userDisabled; - result.canUninstall = Boolean(addon.permissions & AddonManager.PERM_CAN_UNINSTALL); - - return result; -} - -/** - * A helper class to repeatedly call a listener with each object in an array - * optionally checking whether the object has a method in it. - * - * @param aObjects - * The array of objects to iterate through - * @param aMethod - * An optional method name, if not null any objects without this method - * will not be passed to the listener - * @param aListener - * A listener implementing nextObject and noMoreObjects methods. The - * former will be called with the AsyncObjectCaller as the first - * parameter and the object as the second. noMoreObjects will be passed - * just the AsyncObjectCaller - */ -function AsyncObjectCaller(aObjects, aMethod, aListener) { - this.objects = [...aObjects]; - this.method = aMethod; - this.listener = aListener; - - this.callNext(); -} - -AsyncObjectCaller.prototype = { - objects: null, - method: null, - listener: null, - - /** - * Passes the next object to the listener or calls noMoreObjects if there - * are none left. - */ - callNext: function() { - if (this.objects.length == 0) { - this.listener.noMoreObjects(this); - return; - } - - let object = this.objects.shift(); - if (!this.method || this.method in object) - this.listener.nextObject(this, object); - else - this.callNext(); - } -}; - -/** - * Listens for a browser changing origin and cancels the installs that were - * started by it. - */ -function BrowserListener(aBrowser, aInstallingPrincipal, aInstalls) { - this.browser = aBrowser; - this.principal = aInstallingPrincipal; - this.installs = aInstalls; - this.installCount = aInstalls.length; - - aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); - Services.obs.addObserver(this, "message-manager-close", true); - - for (let install of this.installs) - install.addListener(this); - - this.registered = true; -} - -BrowserListener.prototype = { - browser: null, - installs: null, - installCount: null, - registered: false, - - unregister: function() { - if (!this.registered) - return; - this.registered = false; - - Services.obs.removeObserver(this, "message-manager-close"); - // The browser may have already been detached - if (this.browser.removeProgressListener) - this.browser.removeProgressListener(this); - - for (let install of this.installs) - install.removeListener(this); - this.installs = null; - }, - - cancelInstalls: function() { - for (let install of this.installs) { - try { - install.cancel(); - } - catch (e) { - // Some installs may have already failed or been cancelled, ignore these - } - } - }, - - observe: function(subject, topic, data) { - if (subject != this.browser.messageManager) - return; - - // The browser's message manager has closed and so the browser is - // going away, cancel all installs - this.cancelInstalls(); - }, - - onLocationChange: function(webProgress, request, location) { - if (this.browser.contentPrincipal && this.principal.subsumes(this.browser.contentPrincipal)) - return; - - // The browser has navigated to a new origin so cancel all installs - this.cancelInstalls(); - }, - - onDownloadCancelled: function(install) { - // Don't need to hear more events from this install - install.removeListener(this); - - // Once all installs have ended unregister everything - if (--this.installCount == 0) - this.unregister(); - }, - - onDownloadFailed: function(install) { - this.onDownloadCancelled(install); - }, - - onInstallFailed: function(install) { - this.onDownloadCancelled(install); - }, - - onInstallEnded: function(install) { - this.onDownloadCancelled(install); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, - Ci.nsIWebProgressListener, - Ci.nsIObserver]) -}; - -/** - * This represents an author of an add-on (e.g. creator or developer) - * - * @param aName - * The name of the author - * @param aURL - * The URL of the author's profile page - */ -function AddonAuthor(aName, aURL) { - this.name = aName; - this.url = aURL; -} - -AddonAuthor.prototype = { - name: null, - url: null, - - // Returns the author's name, defaulting to the empty string - toString: function() { - return this.name || ""; - } -} - -/** - * This represents an screenshot for an add-on - * - * @param aURL - * The URL to the full version of the screenshot - * @param aWidth - * The width in pixels of the screenshot - * @param aHeight - * The height in pixels of the screenshot - * @param aThumbnailURL - * The URL to the thumbnail version of the screenshot - * @param aThumbnailWidth - * The width in pixels of the thumbnail version of the screenshot - * @param aThumbnailHeight - * The height in pixels of the thumbnail version of the screenshot - * @param aCaption - * The caption of the screenshot - */ -function AddonScreenshot(aURL, aWidth, aHeight, aThumbnailURL, - aThumbnailWidth, aThumbnailHeight, aCaption) { - this.url = aURL; - if (aWidth) this.width = aWidth; - if (aHeight) this.height = aHeight; - if (aThumbnailURL) this.thumbnailURL = aThumbnailURL; - if (aThumbnailWidth) this.thumbnailWidth = aThumbnailWidth; - if (aThumbnailHeight) this.thumbnailHeight = aThumbnailHeight; - if (aCaption) this.caption = aCaption; -} - -AddonScreenshot.prototype = { - url: null, - width: null, - height: null, - thumbnailURL: null, - thumbnailWidth: null, - thumbnailHeight: null, - caption: null, - - // Returns the screenshot URL, defaulting to the empty string - toString: function() { - return this.url || ""; - } -} - - -/** - * This represents a compatibility override for an addon. - * - * @param aType - * Overrride type - "compatible" or "incompatible" - * @param aMinVersion - * Minimum version of the addon to match - * @param aMaxVersion - * Maximum version of the addon to match - * @param aAppID - * Application ID used to match appMinVersion and appMaxVersion - * @param aAppMinVersion - * Minimum version of the application to match - * @param aAppMaxVersion - * Maximum version of the application to match - */ -function AddonCompatibilityOverride(aType, aMinVersion, aMaxVersion, aAppID, - aAppMinVersion, aAppMaxVersion) { - this.type = aType; - this.minVersion = aMinVersion; - this.maxVersion = aMaxVersion; - this.appID = aAppID; - this.appMinVersion = aAppMinVersion; - this.appMaxVersion = aAppMaxVersion; -} - -AddonCompatibilityOverride.prototype = { - /** - * Type of override - "incompatible" or "compatible". - * Only "incompatible" is supported for now. - */ - type: null, - - /** - * Min version of the addon to match. - */ - minVersion: null, - - /** - * Max version of the addon to match. - */ - maxVersion: null, - - /** - * Application ID to match. - */ - appID: null, - - /** - * Min version of the application to match. - */ - appMinVersion: null, - - /** - * Max version of the application to match. - */ - appMaxVersion: null -}; - - -/** - * A type of add-on, used by the UI to determine how to display different types - * of add-ons. - * - * @param aID - * The add-on type ID - * @param aLocaleURI - * The URI of a localized properties file to get the displayable name - * for the type from - * @param aLocaleKey - * The key for the string in the properties file or the actual display - * name if aLocaleURI is null. Include %ID% to include the type ID in - * the key - * @param aViewType - * The optional type of view to use in the UI - * @param aUIPriority - * The priority is used by the UI to list the types in order. Lower - * values push the type higher in the list. - * @param aFlags - * An option set of flags that customize the display of the add-on in - * the UI. - */ -function AddonType(aID, aLocaleURI, aLocaleKey, aViewType, aUIPriority, aFlags) { - if (!aID) - throw Components.Exception("An AddonType must have an ID", Cr.NS_ERROR_INVALID_ARG); - - if (aViewType && aUIPriority === undefined) - throw Components.Exception("An AddonType with a defined view must have a set UI priority", - Cr.NS_ERROR_INVALID_ARG); - - if (!aLocaleKey) - throw Components.Exception("An AddonType must have a displayable name", - Cr.NS_ERROR_INVALID_ARG); - - this.id = aID; - this.uiPriority = aUIPriority; - this.viewType = aViewType; - this.flags = aFlags; - - if (aLocaleURI) { - XPCOMUtils.defineLazyGetter(this, "name", () => { - let bundle = Services.strings.createBundle(aLocaleURI); - return bundle.GetStringFromName(aLocaleKey.replace("%ID%", aID)); - }); - } - else { - this.name = aLocaleKey; - } -} - -var gStarted = false; -var gStartupComplete = false; -var gCheckCompatibility = true; -var gStrictCompatibility = true; -var gCheckUpdateSecurityDefault = true; -var gCheckUpdateSecurity = gCheckUpdateSecurityDefault; -var gUpdateEnabled = true; -var gAutoUpdateDefault = true; -var gHotfixID = null; -var gWebExtensionsMinPlatformVersion = null; -var gShutdownBarrier = null; -var gRepoShutdownState = ""; -var gShutdownInProgress = false; -var gPluginPageListener = null; - -/** - * This is the real manager, kept here rather than in AddonManager to keep its - * contents hidden from API users. - */ -var AddonManagerInternal = { - managerListeners: [], - installListeners: [], - addonListeners: [], - typeListeners: [], - pendingProviders: new Set(), - providers: new Set(), - providerShutdowns: new Map(), - types: {}, - startupChanges: {}, - // Store telemetry details per addon provider - telemetryDetails: {}, - upgradeListeners: new Map(), - - recordTimestamp: function(name, value) { - this.TelemetryTimestamps.add(name, value); - }, - - validateBlocklist: function() { - let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]); - - // If there is no application shipped blocklist then there is nothing to do - if (!appBlocklist.exists()) - return; - - let profileBlocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]); - - // If there is no blocklist in the profile then copy the application shipped - // one there - if (!profileBlocklist.exists()) { - try { - appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST); - } - catch (e) { - logger.warn("Failed to copy the application shipped blocklist to the profile", e); - } - return; - } - - let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - try { - let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]. - createInstance(Ci.nsIConverterInputStream); - fileStream.init(appBlocklist, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); - cstream.init(fileStream, "UTF-8", 0, 0); - - let data = ""; - let str = {}; - let read = 0; - do { - read = cstream.readString(0xffffffff, str); - data += str.value; - } while (read != 0); - - let parser = Cc["@mozilla.org/xmlextras/domparser;1"]. - createInstance(Ci.nsIDOMParser); - var doc = parser.parseFromString(data, "text/xml"); - } - catch (e) { - logger.warn("Application shipped blocklist could not be loaded", e); - return; - } - finally { - try { - fileStream.close(); - } - catch (e) { - logger.warn("Unable to close blocklist file stream", e); - } - } - - // If the namespace is incorrect then ignore the application shipped - // blocklist - if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) { - logger.warn("Application shipped blocklist has an unexpected namespace (" + - doc.documentElement.namespaceURI + ")"); - return; - } - - // If there is no lastupdate information then ignore the application shipped - // blocklist - if (!doc.documentElement.hasAttribute("lastupdate")) - return; - - // If the application shipped blocklist is older than the profile blocklist - // then do nothing - if (doc.documentElement.getAttribute("lastupdate") <= - profileBlocklist.lastModifiedTime) - return; - - // Otherwise copy the application shipped blocklist to the profile - try { - appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST); - } - catch (e) { - logger.warn("Failed to copy the application shipped blocklist to the profile", e); - } - }, - - /** - * Start up a provider, and register its shutdown hook if it has one - */ - _startProvider(aProvider, aAppChanged, aOldAppVersion, aOldPlatformVersion) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - logger.debug(`Starting provider: ${providerName(aProvider)}`); - callProvider(aProvider, "startup", null, aAppChanged, aOldAppVersion, aOldPlatformVersion); - if ('shutdown' in aProvider) { - let name = providerName(aProvider); - let AMProviderShutdown = () => { - // If the provider has been unregistered, it will have been removed from - // this.providers. If it hasn't been unregistered, then this is a normal - // shutdown - and we move it to this.pendingProviders incase we're - // running in a test that will start AddonManager again. - if (this.providers.has(aProvider)) { - this.providers.delete(aProvider); - this.pendingProviders.add(aProvider); - } - - return new Promise((resolve, reject) => { - logger.debug("Calling shutdown blocker for " + name); - resolve(aProvider.shutdown()); - }) - .catch(err => { - logger.warn("Failure during shutdown of " + name, err); - AddonManagerPrivate.recordException("AMI", "Async shutdown of " + name, err); - }); - }; - logger.debug("Registering shutdown blocker for " + name); - this.providerShutdowns.set(aProvider, AMProviderShutdown); - AddonManager.shutdown.addBlocker(name, AMProviderShutdown); - } - - this.pendingProviders.delete(aProvider); - this.providers.add(aProvider); - logger.debug(`Provider finished startup: ${providerName(aProvider)}`); - }, - - _getProviderByName(aName) { - for (let provider of this.providers) { - if (providerName(provider) == aName) - return provider; - } - return undefined; - }, - - /** - * Initializes the AddonManager, loading any known providers and initializing - * them. - */ - startup: function() { - try { - if (gStarted) - return; - - this.recordTimestamp("AMI_startup_begin"); - - // clear this for xpcshell test restarts - for (let provider in this.telemetryDetails) - delete this.telemetryDetails[provider]; - - let appChanged = undefined; - - let oldAppVersion = null; - try { - oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION); - appChanged = Services.appinfo.version != oldAppVersion; - } - catch (e) { } - - Extension.browserUpdated = appChanged; - - let oldPlatformVersion = null; - try { - oldPlatformVersion = Services.prefs.getCharPref(PREF_EM_LAST_PLATFORM_VERSION); - } - catch (e) { } - - if (appChanged !== false) { - logger.debug("Application has been upgraded"); - Services.prefs.setCharPref(PREF_EM_LAST_APP_VERSION, - Services.appinfo.version); - Services.prefs.setCharPref(PREF_EM_LAST_PLATFORM_VERSION, - Services.appinfo.platformVersion); - Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION, - (appChanged === undefined ? 0 : -1)); - this.validateBlocklist(); - } - - try { - gCheckCompatibility = Services.prefs.getBoolPref(PREF_EM_CHECK_COMPATIBILITY); - } catch (e) {} - Services.prefs.addObserver(PREF_EM_CHECK_COMPATIBILITY, this, false); - - try { - gStrictCompatibility = Services.prefs.getBoolPref(PREF_EM_STRICT_COMPATIBILITY); - } catch (e) {} - Services.prefs.addObserver(PREF_EM_STRICT_COMPATIBILITY, this, false); - - try { - let defaultBranch = Services.prefs.getDefaultBranch(""); - gCheckUpdateSecurityDefault = defaultBranch.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY); - } catch (e) {} - - try { - gCheckUpdateSecurity = Services.prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY); - } catch (e) {} - Services.prefs.addObserver(PREF_EM_CHECK_UPDATE_SECURITY, this, false); - - try { - gUpdateEnabled = Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED); - } catch (e) {} - Services.prefs.addObserver(PREF_EM_UPDATE_ENABLED, this, false); - - try { - gAutoUpdateDefault = Services.prefs.getBoolPref(PREF_EM_AUTOUPDATE_DEFAULT); - } catch (e) {} - Services.prefs.addObserver(PREF_EM_AUTOUPDATE_DEFAULT, this, false); - - try { - gHotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID); - } catch (e) {} - Services.prefs.addObserver(PREF_EM_HOTFIX_ID, this, false); - - try { - gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(PREF_MIN_WEBEXT_PLATFORM_VERSION); - } catch (e) {} - Services.prefs.addObserver(PREF_MIN_WEBEXT_PLATFORM_VERSION, this, false); - - let defaultProvidersEnabled = true; - try { - defaultProvidersEnabled = Services.prefs.getBoolPref(PREF_DEFAULT_PROVIDERS_ENABLED); - } catch (e) {} - AddonManagerPrivate.recordSimpleMeasure("default_providers", defaultProvidersEnabled); - - // Ensure all default providers have had a chance to register themselves - if (defaultProvidersEnabled) { - for (let url of DEFAULT_PROVIDERS) { - try { - let scope = {}; - Components.utils.import(url, scope); - // Sanity check - make sure the provider exports a symbol that - // has a 'startup' method - let syms = Object.keys(scope); - if ((syms.length < 1) || - (typeof scope[syms[0]].startup != "function")) { - logger.warn("Provider " + url + " has no startup()"); - AddonManagerPrivate.recordException("AMI", "provider " + url, "no startup()"); - } - logger.debug("Loaded provider scope for " + url + ": " + Object.keys(scope).toSource()); - } - catch (e) { - AddonManagerPrivate.recordException("AMI", "provider " + url + " load failed", e); - logger.error("Exception loading default provider \"" + url + "\"", e); - } - } - } - - // Load any providers registered in the category manager - let catman = Cc["@mozilla.org/categorymanager;1"]. - getService(Ci.nsICategoryManager); - let entries = catman.enumerateCategory(CATEGORY_PROVIDER_MODULE); - while (entries.hasMoreElements()) { - let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; - let url = catman.getCategoryEntry(CATEGORY_PROVIDER_MODULE, entry); - - try { - Components.utils.import(url, {}); - logger.debug(`Loaded provider scope for ${url}`); - } - catch (e) { - AddonManagerPrivate.recordException("AMI", "provider " + url + " load failed", e); - logger.error("Exception loading provider " + entry + " from category \"" + - url + "\"", e); - } - } - - // Register our shutdown handler with the AsyncShutdown manager - gShutdownBarrier = new AsyncShutdown.Barrier("AddonManager: Waiting for providers to shut down."); - AsyncShutdown.profileBeforeChange.addBlocker("AddonManager: shutting down.", - this.shutdownManager.bind(this), - {fetchState: this.shutdownState.bind(this)}); - - // Once we start calling providers we must allow all normal methods to work. - gStarted = true; - - for (let provider of this.pendingProviders) { - this._startProvider(provider, appChanged, oldAppVersion, oldPlatformVersion); - } - - // If this is a new profile just pretend that there were no changes - if (appChanged === undefined) { - for (let type in this.startupChanges) - delete this.startupChanges[type]; - } - - // Support for remote about:plugins. Note that this module isn't loaded - // at the top because Services.appinfo is defined late in tests. - let { RemotePages } = Cu.import("resource://gre/modules/RemotePageManager.jsm", {}); - - gPluginPageListener = new RemotePages("about:plugins"); - gPluginPageListener.addMessageListener("RequestPlugins", this.requestPlugins); - - gStartupComplete = true; - this.recordTimestamp("AMI_startup_end"); - } - catch (e) { - logger.error("startup failed", e); - AddonManagerPrivate.recordException("AMI", "startup failed", e); - } - - logger.debug("Completed startup sequence"); - this.callManagerListeners("onStartup"); - }, - - /** - * Registers a new AddonProvider. - * - * @param aProvider - * The provider to register - * @param aTypes - * An optional array of add-on types - */ - registerProvider: function(aProvider, aTypes) { - if (!aProvider || typeof aProvider != "object") - throw Components.Exception("aProvider must be specified", - Cr.NS_ERROR_INVALID_ARG); - - if (aTypes && !Array.isArray(aTypes)) - throw Components.Exception("aTypes must be an array or null", - Cr.NS_ERROR_INVALID_ARG); - - this.pendingProviders.add(aProvider); - - if (aTypes) { - for (let type of aTypes) { - if (!(type.id in this.types)) { - if (!VALID_TYPES_REGEXP.test(type.id)) { - logger.warn("Ignoring invalid type " + type.id); - return; - } - - this.types[type.id] = { - type: type, - providers: [aProvider] - }; - - let typeListeners = this.typeListeners.slice(0); - for (let listener of typeListeners) - safeCall(() => listener.onTypeAdded(type)); - } - else { - this.types[type.id].providers.push(aProvider); - } - } - } - - // If we're registering after startup call this provider's startup. - if (gStarted) { - this._startProvider(aProvider); - } - }, - - /** - * Unregisters an AddonProvider. - * - * @param aProvider - * The provider to unregister - * @return Whatever the provider's 'shutdown' method returns (if anything). - * For providers that have async shutdown methods returning Promises, - * the caller should wait for that Promise to resolve. - */ - unregisterProvider: function(aProvider) { - if (!aProvider || typeof aProvider != "object") - throw Components.Exception("aProvider must be specified", - Cr.NS_ERROR_INVALID_ARG); - - this.providers.delete(aProvider); - // The test harness will unregister XPIProvider *after* shutdown, which is - // after the provider will have been moved from providers to - // pendingProviders. - this.pendingProviders.delete(aProvider); - - for (let type in this.types) { - this.types[type].providers = this.types[type].providers.filter(p => p != aProvider); - if (this.types[type].providers.length == 0) { - let oldType = this.types[type].type; - delete this.types[type]; - - let typeListeners = this.typeListeners.slice(0); - for (let listener of typeListeners) - safeCall(() => listener.onTypeRemoved(oldType)); - } - } - - // If we're unregistering after startup but before shutting down, - // remove the blocker for this provider's shutdown and call it. - // If we're already shutting down, just let gShutdownBarrier call it to avoid races. - if (gStarted && !gShutdownInProgress) { - logger.debug("Unregistering shutdown blocker for " + providerName(aProvider)); - let shutter = this.providerShutdowns.get(aProvider); - if (shutter) { - this.providerShutdowns.delete(aProvider); - gShutdownBarrier.client.removeBlocker(shutter); - return shutter(); - } - } - return undefined; - }, - - /** - * Mark a provider as safe to access via AddonManager APIs, before its - * startup has completed. - * - * Normally a provider isn't marked as safe until after its (synchronous) - * startup() method has returned. Until a provider has been marked safe, - * it won't be used by any of the AddonManager APIs. markProviderSafe() - * allows a provider to mark itself as safe during its startup; this can be - * useful if the provider wants to perform tasks that block startup, which - * happen after its required initialization tasks and therefore when the - * provider is in a safe state. - * - * @param aProvider Provider object to mark safe - */ - markProviderSafe: function(aProvider) { - if (!gStarted) { - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - } - - if (!aProvider || typeof aProvider != "object") { - throw Components.Exception("aProvider must be specified", - Cr.NS_ERROR_INVALID_ARG); - } - - if (!this.pendingProviders.has(aProvider)) { - return; - } - - this.pendingProviders.delete(aProvider); - this.providers.add(aProvider); - }, - - /** - * Calls a method on all registered providers if it exists and consumes any - * thrown exception. Return values are ignored. Any parameters after the - * method parameter are passed to the provider's method. - * WARNING: Do not use for asynchronous calls; callProviders() does not - * invoke callbacks if provider methods throw synchronous exceptions. - * - * @param aMethod - * The method name to call - * @see callProvider - */ - callProviders: function(aMethod, ...aArgs) { - if (!aMethod || typeof aMethod != "string") - throw Components.Exception("aMethod must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - let providers = [...this.providers]; - for (let provider of providers) { - try { - if (aMethod in provider) - provider[aMethod].apply(provider, aArgs); - } - catch (e) { - reportProviderError(provider, aMethod, e); - } - } - }, - - /** - * Report the current state of asynchronous shutdown - */ - shutdownState() { - let state = []; - if (gShutdownBarrier) { - state.push({ - name: gShutdownBarrier.client.name, - state: gShutdownBarrier.state - }); - } - state.push({ - name: "AddonRepository: async shutdown", - state: gRepoShutdownState - }); - return state; - }, - - /** - * Shuts down the addon manager and all registered providers, this must clean - * up everything in order for automated tests to fake restarts. - * @return Promise{null} that resolves when all providers and dependent modules - * have finished shutting down - */ - shutdownManager: Task.async(function*() { - logger.debug("shutdown"); - this.callManagerListeners("onShutdown"); - - gRepoShutdownState = "pending"; - gShutdownInProgress = true; - // Clean up listeners - Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this); - Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this); - Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this); - Services.prefs.removeObserver(PREF_EM_UPDATE_ENABLED, this); - Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this); - Services.prefs.removeObserver(PREF_EM_HOTFIX_ID, this); - gPluginPageListener.destroy(); - gPluginPageListener = null; - - let savedError = null; - // Only shut down providers if they've been started. - if (gStarted) { - try { - yield gShutdownBarrier.wait(); - } - catch (err) { - savedError = err; - logger.error("Failure during wait for shutdown barrier", err); - AddonManagerPrivate.recordException("AMI", "Async shutdown of AddonManager providers", err); - } - } - - // Shut down AddonRepository after providers (if any). - try { - gRepoShutdownState = "in progress"; - yield AddonRepository.shutdown(); - gRepoShutdownState = "done"; - } - catch (err) { - savedError = err; - logger.error("Failure during AddonRepository shutdown", err); - AddonManagerPrivate.recordException("AMI", "Async shutdown of AddonRepository", err); - } - - logger.debug("Async provider shutdown done"); - this.managerListeners.splice(0, this.managerListeners.length); - this.installListeners.splice(0, this.installListeners.length); - this.addonListeners.splice(0, this.addonListeners.length); - this.typeListeners.splice(0, this.typeListeners.length); - this.providerShutdowns.clear(); - for (let type in this.startupChanges) - delete this.startupChanges[type]; - gStarted = false; - gStartupComplete = false; - gShutdownBarrier = null; - gShutdownInProgress = false; - if (savedError) { - throw savedError; - } - }), - - requestPlugins: function({ target: port }) { - // Lists all the properties that plugins.html needs - const NEEDED_PROPS = ["name", "pluginLibraries", "pluginFullpath", "version", - "isActive", "blocklistState", "description", - "pluginMimeTypes"]; - function filterProperties(plugin) { - let filtered = {}; - for (let prop of NEEDED_PROPS) { - filtered[prop] = plugin[prop]; - } - return filtered; - } - - AddonManager.getAddonsByTypes(["plugin"], function(aPlugins) { - port.sendAsyncMessage("PluginList", aPlugins.map(filterProperties)); - }); - }, - - /** - * Notified when a preference we're interested in has changed. - * - * @see nsIObserver - */ - observe: function(aSubject, aTopic, aData) { - switch (aData) { - case PREF_EM_CHECK_COMPATIBILITY: { - let oldValue = gCheckCompatibility; - try { - gCheckCompatibility = Services.prefs.getBoolPref(PREF_EM_CHECK_COMPATIBILITY); - } catch (e) { - gCheckCompatibility = true; - } - - this.callManagerListeners("onCompatibilityModeChanged"); - - if (gCheckCompatibility != oldValue) - this.updateAddonAppDisabledStates(); - - break; - } - case PREF_EM_STRICT_COMPATIBILITY: { - let oldValue = gStrictCompatibility; - try { - gStrictCompatibility = Services.prefs.getBoolPref(PREF_EM_STRICT_COMPATIBILITY); - } catch (e) { - gStrictCompatibility = true; - } - - this.callManagerListeners("onCompatibilityModeChanged"); - - if (gStrictCompatibility != oldValue) - this.updateAddonAppDisabledStates(); - - break; - } - case PREF_EM_CHECK_UPDATE_SECURITY: { - let oldValue = gCheckUpdateSecurity; - try { - gCheckUpdateSecurity = Services.prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY); - } catch (e) { - gCheckUpdateSecurity = true; - } - - this.callManagerListeners("onCheckUpdateSecurityChanged"); - - if (gCheckUpdateSecurity != oldValue) - this.updateAddonAppDisabledStates(); - - break; - } - case PREF_EM_UPDATE_ENABLED: { - let oldValue = gUpdateEnabled; - try { - gUpdateEnabled = Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED); - } catch (e) { - gUpdateEnabled = true; - } - - this.callManagerListeners("onUpdateModeChanged"); - break; - } - case PREF_EM_AUTOUPDATE_DEFAULT: { - let oldValue = gAutoUpdateDefault; - try { - gAutoUpdateDefault = Services.prefs.getBoolPref(PREF_EM_AUTOUPDATE_DEFAULT); - } catch (e) { - gAutoUpdateDefault = true; - } - - this.callManagerListeners("onUpdateModeChanged"); - break; - } - case PREF_EM_HOTFIX_ID: { - try { - gHotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID); - } catch (e) { - gHotfixID = null; - } - break; - } - case PREF_MIN_WEBEXT_PLATFORM_VERSION: { - gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(PREF_MIN_WEBEXT_PLATFORM_VERSION); - break; - } - } - }, - - /** - * Replaces %...% strings in an addon url (update and updateInfo) with - * appropriate values. - * - * @param aAddon - * The Addon representing the add-on - * @param aUri - * The string representation of the URI to escape - * @param aAppVersion - * The optional application version to use for %APP_VERSION% - * @return The appropriately escaped URI. - */ - escapeAddonURI: function(aAddon, aUri, aAppVersion) - { - if (!aAddon || typeof aAddon != "object") - throw Components.Exception("aAddon must be an Addon object", - Cr.NS_ERROR_INVALID_ARG); - - if (!aUri || typeof aUri != "string") - throw Components.Exception("aUri must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (aAppVersion && typeof aAppVersion != "string") - throw Components.Exception("aAppVersion must be a string or null", - Cr.NS_ERROR_INVALID_ARG); - - var addonStatus = aAddon.userDisabled || aAddon.softDisabled ? "userDisabled" - : "userEnabled"; - - if (!aAddon.isCompatible) - addonStatus += ",incompatible"; - if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) - addonStatus += ",blocklisted"; - if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) - addonStatus += ",softblocked"; - - try { - var xpcomABI = Services.appinfo.XPCOMABI; - } catch (ex) { - xpcomABI = UNKNOWN_XPCOM_ABI; - } - - let uri = aUri.replace(/%ITEM_ID%/g, aAddon.id); - uri = uri.replace(/%ITEM_VERSION%/g, aAddon.version); - uri = uri.replace(/%ITEM_STATUS%/g, addonStatus); - uri = uri.replace(/%APP_ID%/g, Services.appinfo.ID); - uri = uri.replace(/%APP_VERSION%/g, aAppVersion ? aAppVersion : - Services.appinfo.version); - uri = uri.replace(/%REQ_VERSION%/g, UPDATE_REQUEST_VERSION); - uri = uri.replace(/%APP_OS%/g, Services.appinfo.OS); - uri = uri.replace(/%APP_ABI%/g, xpcomABI); - uri = uri.replace(/%APP_LOCALE%/g, getLocale()); - uri = uri.replace(/%CURRENT_APP_VERSION%/g, Services.appinfo.version); - - // Replace custom parameters (names of custom parameters must have at - // least 3 characters to prevent lookups for something like %D0%C8) - var catMan = null; - uri = uri.replace(/%(\w{3,})%/g, function(aMatch, aParam) { - if (!catMan) { - catMan = Cc["@mozilla.org/categorymanager;1"]. - getService(Ci.nsICategoryManager); - } - - try { - var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, aParam); - var paramHandler = Cc[contractID].getService(Ci.nsIPropertyBag2); - return paramHandler.getPropertyAsAString(aParam); - } - catch (e) { - return aMatch; - } - }); - - // escape() does not properly encode + symbols in any embedded FVF strings. - return uri.replace(/\+/g, "%2B"); - }, - - /** - * Performs a background update check by starting an update for all add-ons - * that can be updated. - * @return Promise{null} Resolves when the background update check is complete - * (the resulting addon installations may still be in progress). - */ - backgroundUpdateCheck: function() { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - let buPromise = Task.spawn(function*() { - let hotfixID = this.hotfixID; - - let appUpdateEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_ENABLED) && - Services.prefs.getBoolPref(PREF_APP_UPDATE_AUTO); - let checkHotfix = hotfixID && appUpdateEnabled; - - logger.debug("Background update check beginning"); - - Services.obs.notifyObservers(null, "addons-background-update-start", null); - - if (this.updateEnabled) { - let scope = {}; - Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", scope); - scope.LightweightThemeManager.updateCurrentTheme(); - - let allAddons = yield new Promise((resolve, reject) => this.getAllAddons(resolve)); - - // Repopulate repository cache first, to ensure compatibility overrides - // are up to date before checking for addon updates. - yield AddonRepository.backgroundUpdateCheck(); - - // Keep track of all the async add-on updates happening in parallel - let updates = []; - - for (let addon of allAddons) { - if (addon.id == hotfixID) { - continue; - } - - // Check all add-ons for updates so that any compatibility updates will - // be applied - updates.push(new Promise((resolve, reject) => { - addon.findUpdates({ - onUpdateAvailable: function(aAddon, aInstall) { - // Start installing updates when the add-on can be updated and - // background updates should be applied. - logger.debug("Found update for add-on ${id}", aAddon); - if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE && - AddonManager.shouldAutoUpdate(aAddon)) { - // XXX we really should resolve when this install is done, - // not when update-available check completes, no? - logger.debug(`Starting upgrade install of ${aAddon.id}`); - aInstall.install(); - } - }, - - onUpdateFinished: aAddon => { logger.debug("onUpdateFinished for ${id}", aAddon); resolve(); } - }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE); - })); - } - yield Promise.all(updates); - } - - if (checkHotfix) { - var hotfixVersion = ""; - try { - hotfixVersion = Services.prefs.getCharPref(PREF_EM_HOTFIX_LASTVERSION); - } - catch (e) { } - - let url = null; - if (Services.prefs.getPrefType(PREF_EM_HOTFIX_URL) == Ci.nsIPrefBranch.PREF_STRING) - url = Services.prefs.getCharPref(PREF_EM_HOTFIX_URL); - else - url = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL); - - // Build the URI from a fake add-on data. - url = AddonManager.escapeAddonURI({ - id: hotfixID, - version: hotfixVersion, - userDisabled: false, - appDisabled: false - }, url); - - Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); - let update = null; - try { - let foundUpdates = yield new Promise((resolve, reject) => { - AddonUpdateChecker.checkForUpdates(hotfixID, null, url, { - onUpdateCheckComplete: resolve, - onUpdateCheckError: reject - }); - }); - update = AddonUpdateChecker.getNewestCompatibleUpdate(foundUpdates); - } catch (e) { - // AUC.checkForUpdates already logged the error - } - - // Check that we have a hotfix update, and it's newer than the one we already - // have installed (if any) - if (update) { - if (Services.vc.compare(hotfixVersion, update.version) < 0) { - logger.debug("Downloading hotfix version " + update.version); - let aInstall = yield new Promise((resolve, reject) => - AddonManager.getInstallForURL(update.updateURL, resolve, - "application/x-xpinstall", update.updateHash, null, - null, update.version)); - - aInstall.addListener({ - onDownloadEnded: function(aInstall) { - if (aInstall.addon.id != hotfixID) { - logger.warn("The downloaded hotfix add-on did not have the " + - "expected ID and so will not be installed."); - aInstall.cancel(); - return; - } - - // If XPIProvider has reported the hotfix as properly signed then - // there is nothing more to do here - if (aInstall.addon.signedState == AddonManager.SIGNEDSTATE_SIGNED) - return; - - try { - if (!Services.prefs.getBoolPref(PREF_EM_CERT_CHECKATTRIBUTES)) - return; - } - catch (e) { - // By default don't do certificate checks. - return; - } - - try { - CertUtils.validateCert(aInstall.certificate, - CertUtils.readCertPrefs(PREF_EM_HOTFIX_CERTS)); - } - catch (e) { - logger.warn("The hotfix add-on was not signed by the expected " + - "certificate and so will not be installed.", e); - aInstall.cancel(); - } - }, - - onInstallEnded: function(aInstall) { - // Remember the last successfully installed version. - Services.prefs.setCharPref(PREF_EM_HOTFIX_LASTVERSION, - aInstall.version); - }, - - onInstallCancelled: function(aInstall) { - // Revert to the previous version if the installation was - // cancelled. - Services.prefs.setCharPref(PREF_EM_HOTFIX_LASTVERSION, - hotfixVersion); - } - }); - - aInstall.install(); - } - } - } - - if (appUpdateEnabled) { - try { - yield AddonManagerInternal._getProviderByName("XPIProvider").updateSystemAddons(); - } - catch (e) { - logger.warn("Failed to update system addons", e); - } - } - - logger.debug("Background update check complete"); - Services.obs.notifyObservers(null, - "addons-background-update-complete", - null); - }.bind(this)); - // Fork the promise chain so we can log the error and let our caller see it too. - buPromise.then(null, e => logger.warn("Error in background update", e)); - return buPromise; - }, - - /** - * Adds a add-on to the list of detected changes for this startup. If - * addStartupChange is called multiple times for the same add-on in the same - * startup then only the most recent change will be remembered. - * - * @param aType - * The type of change as a string. Providers can define their own - * types of changes or use the existing defined STARTUP_CHANGE_* - * constants - * @param aID - * The ID of the add-on - */ - addStartupChange: function(aType, aID) { - if (!aType || typeof aType != "string") - throw Components.Exception("aType must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (!aID || typeof aID != "string") - throw Components.Exception("aID must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (gStartupComplete) - return; - logger.debug("Registering startup change '" + aType + "' for " + aID); - - // Ensure that an ID is only listed in one type of change - for (let type in this.startupChanges) - this.removeStartupChange(type, aID); - - if (!(aType in this.startupChanges)) - this.startupChanges[aType] = []; - this.startupChanges[aType].push(aID); - }, - - /** - * Removes a startup change for an add-on. - * - * @param aType - * The type of change - * @param aID - * The ID of the add-on - */ - removeStartupChange: function(aType, aID) { - if (!aType || typeof aType != "string") - throw Components.Exception("aType must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (!aID || typeof aID != "string") - throw Components.Exception("aID must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (gStartupComplete) - return; - - if (!(aType in this.startupChanges)) - return; - - this.startupChanges[aType] = this.startupChanges[aType].filter(aItem => aItem != aID); - }, - - /** - * Calls all registered AddonManagerListeners with an event. Any parameters - * after the method parameter are passed to the listener. - * - * @param aMethod - * The method on the listeners to call - */ - callManagerListeners: function(aMethod, ...aArgs) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aMethod || typeof aMethod != "string") - throw Components.Exception("aMethod must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - let managerListeners = this.managerListeners.slice(0); - for (let listener of managerListeners) { - try { - if (aMethod in listener) - listener[aMethod].apply(listener, aArgs); - } - catch (e) { - logger.warn("AddonManagerListener threw exception when calling " + aMethod, e); - } - } - }, - - /** - * Calls all registered InstallListeners with an event. Any parameters after - * the extraListeners parameter are passed to the listener. - * - * @param aMethod - * The method on the listeners to call - * @param aExtraListeners - * An optional array of extra InstallListeners to also call - * @return false if any of the listeners returned false, true otherwise - */ - callInstallListeners: function(aMethod, - aExtraListeners, ...aArgs) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aMethod || typeof aMethod != "string") - throw Components.Exception("aMethod must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (aExtraListeners && !Array.isArray(aExtraListeners)) - throw Components.Exception("aExtraListeners must be an array or null", - Cr.NS_ERROR_INVALID_ARG); - - let result = true; - let listeners; - if (aExtraListeners) - listeners = aExtraListeners.concat(this.installListeners); - else - listeners = this.installListeners.slice(0); - - for (let listener of listeners) { - try { - if (aMethod in listener) { - if (listener[aMethod].apply(listener, aArgs) === false) - result = false; - } - } - catch (e) { - logger.warn("InstallListener threw exception when calling " + aMethod, e); - } - } - return result; - }, - - /** - * Calls all registered AddonListeners with an event. Any parameters after - * the method parameter are passed to the listener. - * - * @param aMethod - * The method on the listeners to call - */ - callAddonListeners: function(aMethod, ...aArgs) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aMethod || typeof aMethod != "string") - throw Components.Exception("aMethod must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - let addonListeners = this.addonListeners.slice(0); - for (let listener of addonListeners) { - try { - if (aMethod in listener) - listener[aMethod].apply(listener, aArgs); - } - catch (e) { - logger.warn("AddonListener threw exception when calling " + aMethod, e); - } - } - }, - - /** - * Notifies all providers that an add-on has been enabled when that type of - * add-on only supports a single add-on being enabled at a time. This allows - * the providers to disable theirs if necessary. - * - * @param aID - * The ID of the enabled add-on - * @param aType - * The type of the enabled add-on - * @param aPendingRestart - * A boolean indicating if the change will only take place the next - * time the application is restarted - */ - notifyAddonChanged: function(aID, aType, aPendingRestart) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (aID && typeof aID != "string") - throw Components.Exception("aID must be a string or null", - Cr.NS_ERROR_INVALID_ARG); - - if (!aType || typeof aType != "string") - throw Components.Exception("aType must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - // Temporary hack until bug 520124 lands. - // We can get here during synchronous startup, at which point it's - // considered unsafe (and therefore disallowed by AddonManager.jsm) to - // access providers that haven't been initialized yet. Since this is when - // XPIProvider is starting up, XPIProvider can't access itself via APIs - // going through AddonManager.jsm. Furthermore, LightweightThemeManager may - // not be initialized until after XPIProvider is, and therefore would also - // be unaccessible during XPIProvider startup. Thankfully, these are the - // only two uses of this API, and we know it's safe to use this API with - // both providers; so we have this hack to allow bypassing the normal - // safetey guard. - // The notifyAddonChanged/addonChanged API will be unneeded and therefore - // removed by bug 520124, so this is a temporary quick'n'dirty hack. - let providers = [...this.providers, ...this.pendingProviders]; - for (let provider of providers) { - callProvider(provider, "addonChanged", null, aID, aType, aPendingRestart); - } - }, - - /** - * Notifies all providers they need to update the appDisabled property for - * their add-ons in response to an application change such as a blocklist - * update. - */ - updateAddonAppDisabledStates: function() { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - this.callProviders("updateAddonAppDisabledStates"); - }, - - /** - * Notifies all providers that the repository has updated its data for - * installed add-ons. - * - * @param aCallback - * Function to call when operation is complete. - */ - updateAddonRepositoryData: function(aCallback) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - new AsyncObjectCaller(this.providers, "updateAddonRepositoryData", { - nextObject: function(aCaller, aProvider) { - callProviderAsync(aProvider, "updateAddonRepositoryData", - aCaller.callNext.bind(aCaller)); - }, - noMoreObjects: function(aCaller) { - safeCall(aCallback); - // only tests should care about this - Services.obs.notifyObservers(null, "TEST:addon-repository-data-updated", null); - } - }); - }, - - /** - * Asynchronously gets an AddonInstall for a URL. - * - * @param aUrl - * The string represenation of the URL the add-on is located at - * @param aCallback - * A callback to pass the AddonInstall to - * @param aMimetype - * The mimetype of the add-on - * @param aHash - * An optional hash of the add-on - * @param aName - * An optional placeholder name while the add-on is being downloaded - * @param aIcons - * Optional placeholder icons while the add-on is being downloaded - * @param aVersion - * An optional placeholder version while the add-on is being downloaded - * @param aLoadGroup - * An optional nsILoadGroup to associate any network requests with - * @throws if the aUrl, aCallback or aMimetype arguments are not specified - */ - getInstallForURL: function(aUrl, aCallback, aMimetype, - aHash, aName, aIcons, - aVersion, aBrowser) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aUrl || typeof aUrl != "string") - throw Components.Exception("aURL must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - if (!aMimetype || typeof aMimetype != "string") - throw Components.Exception("aMimetype must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (aHash && typeof aHash != "string") - throw Components.Exception("aHash must be a string or null", - Cr.NS_ERROR_INVALID_ARG); - - if (aName && typeof aName != "string") - throw Components.Exception("aName must be a string or null", - Cr.NS_ERROR_INVALID_ARG); - - if (aIcons) { - if (typeof aIcons == "string") - aIcons = { "32": aIcons }; - else if (typeof aIcons != "object") - throw Components.Exception("aIcons must be a string, an object or null", - Cr.NS_ERROR_INVALID_ARG); - } else { - aIcons = {}; - } - - if (aVersion && typeof aVersion != "string") - throw Components.Exception("aVersion must be a string or null", - Cr.NS_ERROR_INVALID_ARG); - - if (aBrowser && (!(aBrowser instanceof Ci.nsIDOMElement))) - throw Components.Exception("aBrowser must be a nsIDOMElement or null", - Cr.NS_ERROR_INVALID_ARG); - - let providers = [...this.providers]; - for (let provider of providers) { - if (callProvider(provider, "supportsMimetype", false, aMimetype)) { - callProviderAsync(provider, "getInstallForURL", - aUrl, aHash, aName, aIcons, aVersion, aBrowser, - function getInstallForURL_safeCall(aInstall) { - safeCall(aCallback, aInstall); - }); - return; - } - } - safeCall(aCallback, null); - }, - - /** - * Asynchronously gets an AddonInstall for an nsIFile. - * - * @param aFile - * The nsIFile where the add-on is located - * @param aCallback - * A callback to pass the AddonInstall to - * @param aMimetype - * An optional mimetype hint for the add-on - * @throws if the aFile or aCallback arguments are not specified - */ - getInstallForFile: function(aFile, aCallback, aMimetype) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!(aFile instanceof Ci.nsIFile)) - throw Components.Exception("aFile must be a nsIFile", - Cr.NS_ERROR_INVALID_ARG); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - if (aMimetype && typeof aMimetype != "string") - throw Components.Exception("aMimetype must be a string or null", - Cr.NS_ERROR_INVALID_ARG); - - new AsyncObjectCaller(this.providers, "getInstallForFile", { - nextObject: function(aCaller, aProvider) { - callProviderAsync(aProvider, "getInstallForFile", aFile, - function(aInstall) { - if (aInstall) - safeCall(aCallback, aInstall); - else - aCaller.callNext(); - }); - }, - - noMoreObjects: function(aCaller) { - safeCall(aCallback, null); - } - }); - }, - - /** - * Asynchronously gets all current AddonInstalls optionally limiting to a list - * of types. - * - * @param aTypes - * An optional array of types to retrieve. Each type is a string name - * @param aCallback - * A callback which will be passed an array of AddonInstalls - * @throws If the aCallback argument is not specified - */ - getInstallsByTypes: function(aTypes, aCallback) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (aTypes && !Array.isArray(aTypes)) - throw Components.Exception("aTypes must be an array or null", - Cr.NS_ERROR_INVALID_ARG); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - let installs = []; - - new AsyncObjectCaller(this.providers, "getInstallsByTypes", { - nextObject: function(aCaller, aProvider) { - callProviderAsync(aProvider, "getInstallsByTypes", aTypes, - function(aProviderInstalls) { - if (aProviderInstalls) { - installs = installs.concat(aProviderInstalls); - } - aCaller.callNext(); - }); - }, - - noMoreObjects: function(aCaller) { - safeCall(aCallback, installs); - } - }); - }, - - /** - * Asynchronously gets all current AddonInstalls. - * - * @param aCallback - * A callback which will be passed an array of AddonInstalls - */ - getAllInstalls: function(aCallback) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - this.getInstallsByTypes(null, aCallback); - }, - - /** - * Synchronously map a URI to the corresponding Addon ID. - * - * Mappable URIs are limited to in-application resources belonging to the - * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc. - * but do not include URIs from meta data, such as the add-on homepage. - * - * @param aURI - * nsIURI to map to an addon id - * @return string containing the Addon ID or null - * @see amIAddonManager.mapURIToAddonID - */ - mapURIToAddonID: function(aURI) { - if (!(aURI instanceof Ci.nsIURI)) { - throw Components.Exception("aURI is not a nsIURI", - Cr.NS_ERROR_INVALID_ARG); - } - - // Try all providers - let providers = [...this.providers]; - for (let provider of providers) { - var id = callProvider(provider, "mapURIToAddonID", null, aURI); - if (id !== null) { - return id; - } - } - - return null; - }, - - /** - * Checks whether installation is enabled for a particular mimetype. - * - * @param aMimetype - * The mimetype to check - * @return true if installation is enabled for the mimetype - */ - isInstallEnabled: function(aMimetype) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aMimetype || typeof aMimetype != "string") - throw Components.Exception("aMimetype must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - let providers = [...this.providers]; - for (let provider of providers) { - if (callProvider(provider, "supportsMimetype", false, aMimetype) && - callProvider(provider, "isInstallEnabled")) - return true; - } - return false; - }, - - /** - * Checks whether a particular source is allowed to install add-ons of a - * given mimetype. - * - * @param aMimetype - * The mimetype of the add-on - * @param aInstallingPrincipal - * The nsIPrincipal that initiated the install - * @return true if the source is allowed to install this mimetype - */ - isInstallAllowed: function(aMimetype, aInstallingPrincipal) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aMimetype || typeof aMimetype != "string") - throw Components.Exception("aMimetype must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal)) - throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal", - Cr.NS_ERROR_INVALID_ARG); - - let providers = [...this.providers]; - for (let provider of providers) { - if (callProvider(provider, "supportsMimetype", false, aMimetype) && - callProvider(provider, "isInstallAllowed", null, aInstallingPrincipal)) - return true; - } - return false; - }, - - /** - * Starts installation of an array of AddonInstalls notifying the registered - * web install listener of blocked or started installs. - * - * @param aMimetype - * The mimetype of add-ons being installed - * @param aBrowser - * The optional browser element that started the installs - * @param aInstallingPrincipal - * The nsIPrincipal that initiated the install - * @param aInstalls - * The array of AddonInstalls to be installed - */ - installAddonsFromWebpage: function(aMimetype, aBrowser, - aInstallingPrincipal, aInstalls) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aMimetype || typeof aMimetype != "string") - throw Components.Exception("aMimetype must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (aBrowser && !(aBrowser instanceof Ci.nsIDOMElement)) - throw Components.Exception("aSource must be a nsIDOMElement, or null", - Cr.NS_ERROR_INVALID_ARG); - - if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal)) - throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal", - Cr.NS_ERROR_INVALID_ARG); - - if (!Array.isArray(aInstalls)) - throw Components.Exception("aInstalls must be an array", - Cr.NS_ERROR_INVALID_ARG); - - if (!("@mozilla.org/addons/web-install-listener;1" in Cc)) { - logger.warn("No web installer available, cancelling all installs"); - for (let install of aInstalls) - install.cancel(); - return; - } - - // When a chrome in-content UI has loaded a <browser> inside to host a - // website we want to do our security checks on the inner-browser but - // notify front-end that install events came from the outer-browser (the - // main tab's browser). Check this by seeing if the browser we've been - // passed is in a content type docshell and if so get the outer-browser. - let topBrowser = aBrowser; - let docShell = aBrowser.ownerDocument.defaultView - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIDocShellTreeItem); - if (docShell.itemType == Ci.nsIDocShellTreeItem.typeContent) - topBrowser = docShell.chromeEventHandler; - - try { - let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"]. - getService(Ci.amIWebInstallListener); - - if (!this.isInstallEnabled(aMimetype)) { - for (let install of aInstalls) - install.cancel(); - - weblistener.onWebInstallDisabled(topBrowser, aInstallingPrincipal.URI, - aInstalls, aInstalls.length); - return; - } - else if (!aBrowser.contentPrincipal || !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)) { - for (let install of aInstalls) - install.cancel(); - - if (weblistener instanceof Ci.amIWebInstallListener2) { - weblistener.onWebInstallOriginBlocked(topBrowser, aInstallingPrincipal.URI, - aInstalls, aInstalls.length); - } - return; - } - - // The installs may start now depending on the web install listener, - // listen for the browser navigating to a new origin and cancel the - // installs in that case. - new BrowserListener(aBrowser, aInstallingPrincipal, aInstalls); - - if (!this.isInstallAllowed(aMimetype, aInstallingPrincipal)) { - if (weblistener.onWebInstallBlocked(topBrowser, aInstallingPrincipal.URI, - aInstalls, aInstalls.length)) { - for (let install of aInstalls) - install.install(); - } - } - else if (weblistener.onWebInstallRequested(topBrowser, aInstallingPrincipal.URI, - aInstalls, aInstalls.length)) { - for (let install of aInstalls) - install.install(); - } - } - catch (e) { - // In the event that the weblistener throws during instantiation or when - // calling onWebInstallBlocked or onWebInstallRequested all of the - // installs should get cancelled. - logger.warn("Failure calling web installer", e); - for (let install of aInstalls) - install.cancel(); - } - }, - - /** - * Adds a new InstallListener if the listener is not already registered. - * - * @param aListener - * The InstallListener to add - */ - addInstallListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be a InstallListener object", - Cr.NS_ERROR_INVALID_ARG); - - if (!this.installListeners.some(function(i) { - return i == aListener; })) - this.installListeners.push(aListener); - }, - - /** - * Removes an InstallListener if the listener is registered. - * - * @param aListener - * The InstallListener to remove - */ - removeInstallListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be a InstallListener object", - Cr.NS_ERROR_INVALID_ARG); - - let pos = 0; - while (pos < this.installListeners.length) { - if (this.installListeners[pos] == aListener) - this.installListeners.splice(pos, 1); - else - pos++; - } - }, - /* - * Adds new or overrides existing UpgradeListener. - * - * @param aInstanceID - * The instance ID of an addon to register a listener for. - * @param aCallback - * The callback to invoke when updates are available for this addon. - * @throws if there is no addon matching the instanceID - */ - addUpgradeListener: function(aInstanceID, aCallback) { - if (!aInstanceID || typeof aInstanceID != "symbol") - throw Components.Exception("aInstanceID must be a symbol", - Cr.NS_ERROR_INVALID_ARG); - - if (!aCallback || typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - this.getAddonByInstanceID(aInstanceID).then(wrapper => { - if (!wrapper) { - throw Error("No addon matching instanceID:", aInstanceID.toString()); - } - let addonId = wrapper.addonId(); - logger.debug(`Registering upgrade listener for ${addonId}`); - this.upgradeListeners.set(addonId, aCallback); - }); - }, - - /** - * Removes an UpgradeListener if the listener is registered. - * - * @param aInstanceID - * The instance ID of the addon to remove - */ - removeUpgradeListener: function(aInstanceID) { - if (!aInstanceID || typeof aInstanceID != "symbol") - throw Components.Exception("aInstanceID must be a symbol", - Cr.NS_ERROR_INVALID_ARG); - - this.getAddonByInstanceID(aInstanceID).then(addon => { - if (!addon) { - throw Error("No addon for instanceID:", aInstanceID.toString()); - } - if (this.upgradeListeners.has(addon.id)) { - this.upgradeListeners.delete(addon.id); - } else { - throw Error("No upgrade listener registered for addon ID:", addon.id); - } - }); - }, - - /** - * Installs a temporary add-on from a local file or directory. - * @param aFile - * An nsIFile for the file or directory of the add-on to be - * temporarily installed. - * @return a Promise that rejects if the add-on is not a valid restartless - * add-on or if the same ID is already temporarily installed. - */ - installTemporaryAddon: function(aFile) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!(aFile instanceof Ci.nsIFile)) - throw Components.Exception("aFile must be a nsIFile", - Cr.NS_ERROR_INVALID_ARG); - - return AddonManagerInternal._getProviderByName("XPIProvider") - .installTemporaryAddon(aFile); - }, - - installAddonFromSources: function(aFile) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!(aFile instanceof Ci.nsIFile)) - throw Components.Exception("aFile must be a nsIFile", - Cr.NS_ERROR_INVALID_ARG); - - return AddonManagerInternal._getProviderByName("XPIProvider") - .installAddonFromSources(aFile); - }, - - /** - * Returns an Addon corresponding to an instance ID. - * @param aInstanceID - * An Addon Instance ID symbol - * @return {Promise} - * @resolves The found Addon or null if no such add-on exists. - * @rejects Never - * @throws if the aInstanceID argument is not specified - * or the AddonManager is not initialized - */ - getAddonByInstanceID: function(aInstanceID) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aInstanceID || typeof aInstanceID != "symbol") - throw Components.Exception("aInstanceID must be a Symbol()", - Cr.NS_ERROR_INVALID_ARG); - - return AddonManagerInternal._getProviderByName("XPIProvider") - .getAddonByInstanceID(aInstanceID); - }, - - /** - * Gets an icon from the icon set provided by the add-on - * that is closest to the specified size. - * - * The optional window parameter will be used to determine - * the screen resolution and select a more appropriate icon. - * Calling this method with 48px on retina screens will try to - * match an icon of size 96px. - * - * @param aAddon - * An addon object, meaning: - * An object with either an icons property that is a key-value - * list of icon size and icon URL, or an object having an iconURL - * and icon64URL property. - * @param aSize - * Ideal icon size in pixels - * @param aWindow - * Optional window object for determining the correct scale. - * @return {String} The absolute URL of the icon or null if the addon doesn't have icons - */ - getPreferredIconURL: function(aAddon, aSize, aWindow = undefined) { - if (aWindow && aWindow.devicePixelRatio) { - aSize *= aWindow.devicePixelRatio; - } - - let icons = aAddon.icons; - - // certain addon-types only have iconURLs - if (!icons) { - icons = {}; - if (aAddon.iconURL) { - icons[32] = aAddon.iconURL; - icons[48] = aAddon.iconURL; - } - if (aAddon.icon64URL) { - icons[64] = aAddon.icon64URL; - } - } - - // quick return if the exact size was found - if (icons[aSize]) { - return icons[aSize]; - } - - let bestSize = null; - - for (let size of Object.keys(icons)) { - if (!INTEGER.test(size)) { - throw Components.Exception("Invalid icon size, must be an integer", - Cr.NS_ERROR_ILLEGAL_VALUE); - } - - size = parseInt(size, 10); - - if (!bestSize) { - bestSize = size; - continue; - } - - if (size > aSize && bestSize > aSize) { - // If both best size and current size are larger than the wanted size then choose - // the one closest to the wanted size - bestSize = Math.min(bestSize, size); - } - else { - // Otherwise choose the largest of the two so we'll prefer sizes as close to below aSize - // or above aSize - bestSize = Math.max(bestSize, size); - } - } - - return icons[bestSize] || null; - }, - - /** - * Asynchronously gets an add-on with a specific ID. - * - * @param aID - * The ID of the add-on to retrieve - * @return {Promise} - * @resolves The found Addon or null if no such add-on exists. - * @rejects Never - * @throws if the aID argument is not specified - */ - getAddonByID: function(aID) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aID || typeof aID != "string") - throw Components.Exception("aID must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - let promises = Array.from(this.providers, - p => promiseCallProvider(p, "getAddonByID", aID)); - return Promise.all(promises).then(aAddons => { - return aAddons.find(a => !!a) || null; - }); - }, - - /** - * Asynchronously get an add-on with a specific Sync GUID. - * - * @param aGUID - * String GUID of add-on to retrieve - * @param aCallback - * The callback to pass the retrieved add-on to. - * @throws if the aGUID or aCallback arguments are not specified - */ - getAddonBySyncGUID: function(aGUID, aCallback) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!aGUID || typeof aGUID != "string") - throw Components.Exception("aGUID must be a non-empty string", - Cr.NS_ERROR_INVALID_ARG); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - new AsyncObjectCaller(this.providers, "getAddonBySyncGUID", { - nextObject: function(aCaller, aProvider) { - callProviderAsync(aProvider, "getAddonBySyncGUID", aGUID, - function(aAddon) { - if (aAddon) { - safeCall(aCallback, aAddon); - } else { - aCaller.callNext(); - } - }); - }, - - noMoreObjects: function(aCaller) { - safeCall(aCallback, null); - } - }); - }, - - /** - * Asynchronously gets an array of add-ons. - * - * @param aIDs - * The array of IDs to retrieve - * @return {Promise} - * @resolves The array of found add-ons. - * @rejects Never - * @throws if the aIDs argument is not specified - */ - getAddonsByIDs: function(aIDs) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (!Array.isArray(aIDs)) - throw Components.Exception("aIDs must be an array", - Cr.NS_ERROR_INVALID_ARG); - - let promises = aIDs.map(a => AddonManagerInternal.getAddonByID(a)); - return Promise.all(promises); - }, - - /** - * Asynchronously gets add-ons of specific types. - * - * @param aTypes - * An optional array of types to retrieve. Each type is a string name - * @param aCallback - * The callback to pass an array of Addons to. - * @throws if the aCallback argument is not specified - */ - getAddonsByTypes: function(aTypes, aCallback) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (aTypes && !Array.isArray(aTypes)) - throw Components.Exception("aTypes must be an array or null", - Cr.NS_ERROR_INVALID_ARG); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - let addons = []; - - new AsyncObjectCaller(this.providers, "getAddonsByTypes", { - nextObject: function(aCaller, aProvider) { - callProviderAsync(aProvider, "getAddonsByTypes", aTypes, - function(aProviderAddons) { - if (aProviderAddons) { - addons = addons.concat(aProviderAddons); - } - aCaller.callNext(); - }); - }, - - noMoreObjects: function(aCaller) { - safeCall(aCallback, addons); - } - }); - }, - - /** - * Asynchronously gets all installed add-ons. - * - * @param aCallback - * A callback which will be passed an array of Addons - */ - getAllAddons: function(aCallback) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - this.getAddonsByTypes(null, aCallback); - }, - - /** - * Asynchronously gets add-ons that have operations waiting for an application - * restart to complete. - * - * @param aTypes - * An optional array of types to retrieve. Each type is a string name - * @param aCallback - * The callback to pass the array of Addons to - * @throws if the aCallback argument is not specified - */ - getAddonsWithOperationsByTypes: function(aTypes, aCallback) { - if (!gStarted) - throw Components.Exception("AddonManager is not initialized", - Cr.NS_ERROR_NOT_INITIALIZED); - - if (aTypes && !Array.isArray(aTypes)) - throw Components.Exception("aTypes must be an array or null", - Cr.NS_ERROR_INVALID_ARG); - - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - let addons = []; - - new AsyncObjectCaller(this.providers, "getAddonsWithOperationsByTypes", { - nextObject: function getAddonsWithOperationsByTypes_nextObject - (aCaller, aProvider) { - callProviderAsync(aProvider, "getAddonsWithOperationsByTypes", aTypes, - function getAddonsWithOperationsByTypes_concatAddons - (aProviderAddons) { - if (aProviderAddons) { - addons = addons.concat(aProviderAddons); - } - aCaller.callNext(); - }); - }, - - noMoreObjects: function(caller) { - safeCall(aCallback, addons); - } - }); - }, - - /** - * Adds a new AddonManagerListener if the listener is not already registered. - * - * @param aListener - * The listener to add - */ - addManagerListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be an AddonManagerListener object", - Cr.NS_ERROR_INVALID_ARG); - - if (!this.managerListeners.some(i => i == aListener)) - this.managerListeners.push(aListener); - }, - - /** - * Removes an AddonManagerListener if the listener is registered. - * - * @param aListener - * The listener to remove - */ - removeManagerListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be an AddonManagerListener object", - Cr.NS_ERROR_INVALID_ARG); - - let pos = 0; - while (pos < this.managerListeners.length) { - if (this.managerListeners[pos] == aListener) - this.managerListeners.splice(pos, 1); - else - pos++; - } - }, - - /** - * Adds a new AddonListener if the listener is not already registered. - * - * @param aListener - * The AddonListener to add - */ - addAddonListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be an AddonListener object", - Cr.NS_ERROR_INVALID_ARG); - - if (!this.addonListeners.some(i => i == aListener)) - this.addonListeners.push(aListener); - }, - - /** - * Removes an AddonListener if the listener is registered. - * - * @param aListener - * The AddonListener to remove - */ - removeAddonListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be an AddonListener object", - Cr.NS_ERROR_INVALID_ARG); - - let pos = 0; - while (pos < this.addonListeners.length) { - if (this.addonListeners[pos] == aListener) - this.addonListeners.splice(pos, 1); - else - pos++; - } - }, - - /** - * Adds a new TypeListener if the listener is not already registered. - * - * @param aListener - * The TypeListener to add - */ - addTypeListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be a TypeListener object", - Cr.NS_ERROR_INVALID_ARG); - - if (!this.typeListeners.some(i => i == aListener)) - this.typeListeners.push(aListener); - }, - - /** - * Removes an TypeListener if the listener is registered. - * - * @param aListener - * The TypeListener to remove - */ - removeTypeListener: function(aListener) { - if (!aListener || typeof aListener != "object") - throw Components.Exception("aListener must be a TypeListener object", - Cr.NS_ERROR_INVALID_ARG); - - let pos = 0; - while (pos < this.typeListeners.length) { - if (this.typeListeners[pos] == aListener) - this.typeListeners.splice(pos, 1); - else - pos++; - } - }, - - get addonTypes() { - // A read-only wrapper around the types dictionary - return new Proxy(this.types, { - defineProperty(target, property, descriptor) { - // Not allowed to define properties - return false; - }, - - deleteProperty(target, property) { - // Not allowed to delete properties - return false; - }, - - get(target, property, receiver) { - if (!target.hasOwnProperty(property)) - return undefined; - - return target[property].type; - }, - - getOwnPropertyDescriptor(target, property) { - if (!target.hasOwnProperty(property)) - return undefined; - - return { - value: target[property].type, - writable: false, - // Claim configurability to maintain the proxy invariants. - configurable: true, - enumerable: true - } - }, - - preventExtensions(target) { - // Not allowed to prevent adding new properties - return false; - }, - - set(target, property, value, receiver) { - // Not allowed to set properties - return false; - }, - - setPrototypeOf(target, prototype) { - // Not allowed to change prototype - return false; - } - }); - }, - - get autoUpdateDefault() { - return gAutoUpdateDefault; - }, - - set autoUpdateDefault(aValue) { - aValue = !!aValue; - if (aValue != gAutoUpdateDefault) - Services.prefs.setBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, aValue); - return aValue; - }, - - get checkCompatibility() { - return gCheckCompatibility; - }, - - set checkCompatibility(aValue) { - aValue = !!aValue; - if (aValue != gCheckCompatibility) { - if (!aValue) - Services.prefs.setBoolPref(PREF_EM_CHECK_COMPATIBILITY, false); - else - Services.prefs.clearUserPref(PREF_EM_CHECK_COMPATIBILITY); - } - return aValue; - }, - - get strictCompatibility() { - return gStrictCompatibility; - }, - - set strictCompatibility(aValue) { - aValue = !!aValue; - if (aValue != gStrictCompatibility) - Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, aValue); - return aValue; - }, - - get checkUpdateSecurityDefault() { - return gCheckUpdateSecurityDefault; - }, - - get checkUpdateSecurity() { - return gCheckUpdateSecurity; - }, - - set checkUpdateSecurity(aValue) { - aValue = !!aValue; - if (aValue != gCheckUpdateSecurity) { - if (aValue != gCheckUpdateSecurityDefault) - Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, aValue); - else - Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY); - } - return aValue; - }, - - get updateEnabled() { - return gUpdateEnabled; - }, - - set updateEnabled(aValue) { - aValue = !!aValue; - if (aValue != gUpdateEnabled) - Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue); - return aValue; - }, - - get hotfixID() { - return gHotfixID; - }, - - webAPI: { - // installs maps integer ids to AddonInstall instances. - installs: new Map(), - nextInstall: 0, - - sendEvent: null, - setEventHandler(fn) { - this.sendEvent = fn; - }, - - getAddonByID(target, id) { - return new Promise(resolve => { - AddonManager.getAddonByID(id, (addon) => { - resolve(webAPIForAddon(addon)); - }); - }); - }, - - // helper to copy (and convert) the properties we care about - copyProps(install, obj) { - obj.state = AddonManager.stateToString(install.state); - obj.error = AddonManager.errorToString(install.error); - obj.progress = install.progress; - obj.maxProgress = install.maxProgress; - }, - - makeListener(id, mm) { - const events = [ - "onDownloadStarted", - "onDownloadProgress", - "onDownloadEnded", - "onDownloadCancelled", - "onDownloadFailed", - "onInstallStarted", - "onInstallEnded", - "onInstallCancelled", - "onInstallFailed", - ]; - - let listener = {}; - events.forEach(event => { - listener[event] = (install) => { - let data = {event, id}; - AddonManager.webAPI.copyProps(install, data); - this.sendEvent(mm, data); - } - }); - return listener; - }, - - forgetInstall(id) { - let info = this.installs.get(id); - if (!info) { - throw new Error(`forgetInstall cannot find ${id}`); - } - info.install.removeListener(info.listener); - this.installs.delete(id); - }, - - createInstall(target, options) { - // Throw an appropriate error if the given URL is not valid - // as an installation source. Return silently if it is okay. - function checkInstallUrl(url) { - let host = Services.io.newURI(options.url, null, null).host; - if (WEBAPI_INSTALL_HOSTS.includes(host)) { - return; - } - if (Services.prefs.getBoolPref(PREF_WEBAPI_TESTING) - && WEBAPI_TEST_INSTALL_HOSTS.includes(host)) { - return; - } - - throw new Error(`Install from ${host} not permitted`); - } - - return new Promise((resolve, reject) => { - try { - checkInstallUrl(options.url); - } catch (err) { - reject({message: err.message}); - return; - } - - let newInstall = install => { - let id = this.nextInstall++; - let listener = this.makeListener(id, target.messageManager); - install.addListener(listener); - - this.installs.set(id, {install, target, listener}); - - let result = {id}; - this.copyProps(install, result); - resolve(result); - }; - AddonManager.getInstallForURL(options.url, newInstall, "application/x-xpinstall", options.hash); - }); - }, - - addonUninstall(target, id) { - return new Promise(resolve => { - AddonManager.getAddonByID(id, addon => { - if (!addon) { - resolve(false); - } - - try { - addon.uninstall(); - resolve(true); - } catch (err) { - Cu.reportError(err); - resolve(false); - } - }); - }); - }, - - addonSetEnabled(target, id, value) { - return new Promise((resolve, reject) => { - AddonManager.getAddonByID(id, addon => { - if (!addon) { - reject({message: `No such addon ${id}`}); - } - addon.userDisabled = !value; - resolve(); - }); - }); - }, - - addonInstallDoInstall(target, id) { - let state = this.installs.get(id); - if (!state) { - return Promise.reject(`invalid id ${id}`); - } - return Promise.resolve(state.install.install()); - }, - - addonInstallCancel(target, id) { - let state = this.installs.get(id); - if (!state) { - return Promise.reject(`invalid id ${id}`); - } - return Promise.resolve(state.install.cancel()); - }, - - clearInstalls(ids) { - for (let id of ids) { - this.forgetInstall(id); - } - }, - - clearInstallsFrom(mm) { - for (let [id, info] of this.installs) { - if (info.target.messageManager == mm) { - this.forgetInstall(id); - } - } - }, - }, -}; - -/** - * Should not be used outside of core Mozilla code. This is a private API for - * the startup and platform integration code to use. Refer to the methods on - * AddonManagerInternal for documentation however note that these methods are - * subject to change at any time. - */ -this.AddonManagerPrivate = { - startup: function() { - AddonManagerInternal.startup(); - }, - - registerProvider: function(aProvider, aTypes) { - AddonManagerInternal.registerProvider(aProvider, aTypes); - }, - - unregisterProvider: function(aProvider) { - AddonManagerInternal.unregisterProvider(aProvider); - }, - - markProviderSafe: function(aProvider) { - AddonManagerInternal.markProviderSafe(aProvider); - }, - - backgroundUpdateCheck: function() { - return AddonManagerInternal.backgroundUpdateCheck(); - }, - - backgroundUpdateTimerHandler() { - // Don't call through to the real update check if no checks are enabled. - let checkHotfix = AddonManagerInternal.hotfixID && - Services.prefs.getBoolPref(PREF_APP_UPDATE_ENABLED) && - Services.prefs.getBoolPref(PREF_APP_UPDATE_AUTO); - - if (!AddonManagerInternal.updateEnabled && !checkHotfix) { - logger.info("Skipping background update check"); - return; - } - // Don't return the promise here, since the caller doesn't care. - AddonManagerInternal.backgroundUpdateCheck(); - }, - - addStartupChange: function(aType, aID) { - AddonManagerInternal.addStartupChange(aType, aID); - }, - - removeStartupChange: function(aType, aID) { - AddonManagerInternal.removeStartupChange(aType, aID); - }, - - notifyAddonChanged: function(aID, aType, aPendingRestart) { - AddonManagerInternal.notifyAddonChanged(aID, aType, aPendingRestart); - }, - - updateAddonAppDisabledStates: function() { - AddonManagerInternal.updateAddonAppDisabledStates(); - }, - - updateAddonRepositoryData: function(aCallback) { - AddonManagerInternal.updateAddonRepositoryData(aCallback); - }, - - callInstallListeners: function(...aArgs) { - return AddonManagerInternal.callInstallListeners.apply(AddonManagerInternal, - aArgs); - }, - - callAddonListeners: function(...aArgs) { - AddonManagerInternal.callAddonListeners.apply(AddonManagerInternal, aArgs); - }, - - AddonAuthor: AddonAuthor, - - AddonScreenshot: AddonScreenshot, - - AddonCompatibilityOverride: AddonCompatibilityOverride, - - AddonType: AddonType, - - recordTimestamp: function(name, value) { - AddonManagerInternal.recordTimestamp(name, value); - }, - - _simpleMeasures: {}, - recordSimpleMeasure: function(name, value) { - this._simpleMeasures[name] = value; - }, - - recordException: function(aModule, aContext, aException) { - let report = { - module: aModule, - context: aContext - }; - - if (typeof aException == "number") { - report.message = Components.Exception("", aException).name; - } - else { - report.message = aException.toString(); - if (aException.fileName) { - report.file = aException.fileName; - report.line = aException.lineNumber; - } - } - - this._simpleMeasures.exception = report; - }, - - getSimpleMeasures: function() { - return this._simpleMeasures; - }, - - getTelemetryDetails: function() { - return AddonManagerInternal.telemetryDetails; - }, - - setTelemetryDetails: function(aProvider, aDetails) { - AddonManagerInternal.telemetryDetails[aProvider] = aDetails; - }, - - // Start a timer, record a simple measure of the time interval when - // timer.done() is called - simpleTimer: function(aName) { - let startTime = Cu.now(); - return { - done: () => this.recordSimpleMeasure(aName, Math.round(Cu.now() - startTime)) - }; - }, - - /** - * Helper to call update listeners when no update is available. - * - * This can be used as an implementation for Addon.findUpdates() when - * no update mechanism is available. - */ - callNoUpdateListeners: function(addon, listener, reason, appVersion, platformVersion) { - if ("onNoCompatibilityUpdateAvailable" in listener) { - safeCall(listener.onNoCompatibilityUpdateAvailable.bind(listener), addon); - } - if ("onNoUpdateAvailable" in listener) { - safeCall(listener.onNoUpdateAvailable.bind(listener), addon); - } - if ("onUpdateFinished" in listener) { - safeCall(listener.onUpdateFinished.bind(listener), addon); - } - }, - - get webExtensionsMinPlatformVersion() { - return gWebExtensionsMinPlatformVersion; - }, - - hasUpgradeListener: function(aId) { - return AddonManagerInternal.upgradeListeners.has(aId); - }, - - getUpgradeListener: function(aId) { - return AddonManagerInternal.upgradeListeners.get(aId); - }, -}; - -/** - * This is the public API that UI and developers should be calling. All methods - * just forward to AddonManagerInternal. - */ -this.AddonManager = { - // Constants for the AddonInstall.state property - // These will show up as AddonManager.STATE_* (eg, STATE_AVAILABLE) - _states: new Map([ - // The install is available for download. - ["STATE_AVAILABLE", 0], - // The install is being downloaded. - ["STATE_DOWNLOADING", 1], - // The install is checking for compatibility information. - ["STATE_CHECKING", 2], - // The install is downloaded and ready to install. - ["STATE_DOWNLOADED", 3], - // The download failed. - ["STATE_DOWNLOAD_FAILED", 4], - // The install has been postponed. - ["STATE_POSTPONED", 5], - // The add-on is being installed. - ["STATE_INSTALLING", 6], - // The add-on has been installed. - ["STATE_INSTALLED", 7], - // The install failed. - ["STATE_INSTALL_FAILED", 8], - // The install has been cancelled. - ["STATE_CANCELLED", 9], - ]), - - // Constants representing different types of errors while downloading an - // add-on. - // These will show up as AddonManager.ERROR_* (eg, ERROR_NETWORK_FAILURE) - _errors: new Map([ - // The download failed due to network problems. - ["ERROR_NETWORK_FAILURE", -1], - // The downloaded file did not match the provided hash. - ["ERROR_INCORRECT_HASH", -2], - // The downloaded file seems to be corrupted in some way. - ["ERROR_CORRUPT_FILE", -3], - // An error occured trying to write to the filesystem. - ["ERROR_FILE_ACCESS", -4], - // The add-on must be signed and isn't. - ["ERROR_SIGNEDSTATE_REQUIRED", -5], - // The downloaded add-on had a different type than expected. - ["ERROR_UNEXPECTED_ADDON_TYPE", -6], - // The addon did not have the expected ID - ["ERROR_INCORRECT_ID", -7], - ]), - - // These must be kept in sync with AddonUpdateChecker. - // No error was encountered. - UPDATE_STATUS_NO_ERROR: 0, - // The update check timed out - UPDATE_STATUS_TIMEOUT: -1, - // There was an error while downloading the update information. - UPDATE_STATUS_DOWNLOAD_ERROR: -2, - // The update information was malformed in some way. - UPDATE_STATUS_PARSE_ERROR: -3, - // The update information was not in any known format. - UPDATE_STATUS_UNKNOWN_FORMAT: -4, - // The update information was not correctly signed or there was an SSL error. - UPDATE_STATUS_SECURITY_ERROR: -5, - // The update was cancelled. - UPDATE_STATUS_CANCELLED: -6, - - // Constants to indicate why an update check is being performed - // Update check has been requested by the user. - UPDATE_WHEN_USER_REQUESTED: 1, - // Update check is necessary to see if the Addon is compatibile with a new - // version of the application. - UPDATE_WHEN_NEW_APP_DETECTED: 2, - // Update check is necessary because a new application has been installed. - UPDATE_WHEN_NEW_APP_INSTALLED: 3, - // Update check is a regular background update check. - UPDATE_WHEN_PERIODIC_UPDATE: 16, - // Update check is needed to check an Addon that is being installed. - UPDATE_WHEN_ADDON_INSTALLED: 17, - - // Constants for operations in Addon.pendingOperations - // Indicates that the Addon has no pending operations. - PENDING_NONE: 0, - // Indicates that the Addon will be enabled after the application restarts. - PENDING_ENABLE: 1, - // Indicates that the Addon will be disabled after the application restarts. - PENDING_DISABLE: 2, - // Indicates that the Addon will be uninstalled after the application restarts. - PENDING_UNINSTALL: 4, - // Indicates that the Addon will be installed after the application restarts. - PENDING_INSTALL: 8, - PENDING_UPGRADE: 16, - - // Constants for operations in Addon.operationsRequiringRestart - // Indicates that restart isn't required for any operation. - OP_NEEDS_RESTART_NONE: 0, - // Indicates that restart is required for enabling the addon. - OP_NEEDS_RESTART_ENABLE: 1, - // Indicates that restart is required for disabling the addon. - OP_NEEDS_RESTART_DISABLE: 2, - // Indicates that restart is required for uninstalling the addon. - OP_NEEDS_RESTART_UNINSTALL: 4, - // Indicates that restart is required for installing the addon. - OP_NEEDS_RESTART_INSTALL: 8, - - // Constants for permissions in Addon.permissions. - // Indicates that the Addon can be uninstalled. - PERM_CAN_UNINSTALL: 1, - // Indicates that the Addon can be enabled by the user. - PERM_CAN_ENABLE: 2, - // Indicates that the Addon can be disabled by the user. - PERM_CAN_DISABLE: 4, - // Indicates that the Addon can be upgraded. - PERM_CAN_UPGRADE: 8, - // Indicates that the Addon can be set to be optionally enabled - // on a case-by-case basis. - PERM_CAN_ASK_TO_ACTIVATE: 16, - - // General descriptions of where items are installed. - // Installed in this profile. - SCOPE_PROFILE: 1, - // Installed for all of this user's profiles. - SCOPE_USER: 2, - // Installed and owned by the application. - SCOPE_APPLICATION: 4, - // Installed for all users of the computer. - SCOPE_SYSTEM: 8, - // Installed temporarily - SCOPE_TEMPORARY: 16, - // The combination of all scopes. - SCOPE_ALL: 31, - - // Add-on type is expected to be displayed in the UI in a list. - VIEW_TYPE_LIST: "list", - - // Constants describing how add-on types behave. - - // If no add-ons of a type are installed, then the category for that add-on - // type should be hidden in the UI. - TYPE_UI_HIDE_EMPTY: 16, - // Indicates that this add-on type supports the ask-to-activate state. - // That is, add-ons of this type can be set to be optionally enabled - // on a case-by-case basis. - TYPE_SUPPORTS_ASK_TO_ACTIVATE: 32, - // The add-on type natively supports undo for restartless uninstalls. - // If this flag is not specified, the UI is expected to handle this via - // disabling the add-on, and performing the actual uninstall at a later time. - TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL: 64, - - // Constants for Addon.applyBackgroundUpdates. - // Indicates that the Addon should not update automatically. - AUTOUPDATE_DISABLE: 0, - // Indicates that the Addon should update automatically only if - // that's the global default. - AUTOUPDATE_DEFAULT: 1, - // Indicates that the Addon should update automatically. - AUTOUPDATE_ENABLE: 2, - - // Constants for how Addon options should be shown. - // Options will be opened in a new window - OPTIONS_TYPE_DIALOG: 1, - // Options will be displayed within the AM detail view - OPTIONS_TYPE_INLINE: 2, - // Options will be displayed in a new tab, if possible - OPTIONS_TYPE_TAB: 3, - // Same as OPTIONS_TYPE_INLINE, but no Preferences button will be shown. - // Used to indicate that only non-interactive information will be shown. - OPTIONS_TYPE_INLINE_INFO: 4, - // Similar to OPTIONS_TYPE_INLINE, but rather than generating inline - // options from a specially-formatted XUL file, the contents of the - // file are simply displayed in an inline <browser> element. - OPTIONS_TYPE_INLINE_BROWSER: 5, - - // Constants for displayed or hidden options notifications - // Options notification will be displayed - OPTIONS_NOTIFICATION_DISPLAYED: "addon-options-displayed", - // Options notification will be hidden - OPTIONS_NOTIFICATION_HIDDEN: "addon-options-hidden", - - // Constants for getStartupChanges, addStartupChange and removeStartupChange - // Add-ons that were detected as installed during startup. Doesn't include - // add-ons that were pending installation the last time the application ran. - STARTUP_CHANGE_INSTALLED: "installed", - // Add-ons that were detected as changed during startup. This includes an - // add-on moving to a different location, changing version or just having - // been detected as possibly changed. - STARTUP_CHANGE_CHANGED: "changed", - // Add-ons that were detected as uninstalled during startup. Doesn't include - // add-ons that were pending uninstallation the last time the application ran. - STARTUP_CHANGE_UNINSTALLED: "uninstalled", - // Add-ons that were detected as disabled during startup, normally because of - // an application change making an add-on incompatible. Doesn't include - // add-ons that were pending being disabled the last time the application ran. - STARTUP_CHANGE_DISABLED: "disabled", - // Add-ons that were detected as enabled during startup, normally because of - // an application change making an add-on compatible. Doesn't include - // add-ons that were pending being enabled the last time the application ran. - STARTUP_CHANGE_ENABLED: "enabled", - - // Constants for Addon.signedState. Any states that should cause an add-on - // to be unusable in builds that require signing should have negative values. - // Add-on signing is not required, e.g. because the pref is disabled. - SIGNEDSTATE_NOT_REQUIRED: undefined, - // Add-on is signed but signature verification has failed. - SIGNEDSTATE_BROKEN: -2, - // Add-on may be signed but by an certificate that doesn't chain to our - // our trusted certificate. - SIGNEDSTATE_UNKNOWN: -1, - // Add-on is unsigned. - SIGNEDSTATE_MISSING: 0, - // Add-on is preliminarily reviewed. - SIGNEDSTATE_PRELIMINARY: 1, - // Add-on is fully reviewed. - SIGNEDSTATE_SIGNED: 2, - // Add-on is system add-on. - SIGNEDSTATE_SYSTEM: 3, - - // Constants for the Addon.userDisabled property - // Indicates that the userDisabled state of this add-on is currently - // ask-to-activate. That is, it can be conditionally enabled on a - // case-by-case basis. - STATE_ASK_TO_ACTIVATE: "askToActivate", - - get __AddonManagerInternal__() { - return AppConstants.DEBUG ? AddonManagerInternal : undefined; - }, - - get isReady() { - return gStartupComplete && !gShutdownInProgress; - }, - - init() { - this._stateToString = new Map(); - for (let [name, value] of this._states) { - this[name] = value; - this._stateToString.set(value, name); - } - this._errorToString = new Map(); - for (let [name, value] of this._errors) { - this[name] = value; - this._errorToString.set(value, name); - } - }, - - stateToString(state) { - return this._stateToString.get(state); - }, - - errorToString(err) { - return err ? this._errorToString.get(err) : null; - }, - - getInstallForURL: function(aUrl, aCallback, aMimetype, - aHash, aName, aIcons, - aVersion, aBrowser) { - AddonManagerInternal.getInstallForURL(aUrl, aCallback, aMimetype, aHash, - aName, aIcons, aVersion, aBrowser); - }, - - getInstallForFile: function(aFile, aCallback, aMimetype) { - AddonManagerInternal.getInstallForFile(aFile, aCallback, aMimetype); - }, - - /** - * Gets an array of add-on IDs that changed during the most recent startup. - * - * @param aType - * The type of startup change to get - * @return An array of add-on IDs - */ - getStartupChanges: function(aType) { - if (!(aType in AddonManagerInternal.startupChanges)) - return []; - return AddonManagerInternal.startupChanges[aType].slice(0); - }, - - getAddonByID: function(aID, aCallback) { - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - AddonManagerInternal.getAddonByID(aID) - .then(makeSafe(aCallback)) - .catch(logger.error); - }, - - getAddonBySyncGUID: function(aGUID, aCallback) { - AddonManagerInternal.getAddonBySyncGUID(aGUID, aCallback); - }, - - getAddonsByIDs: function(aIDs, aCallback) { - if (typeof aCallback != "function") - throw Components.Exception("aCallback must be a function", - Cr.NS_ERROR_INVALID_ARG); - - AddonManagerInternal.getAddonsByIDs(aIDs) - .then(makeSafe(aCallback)) - .catch(logger.error); - }, - - getAddonsWithOperationsByTypes: function(aTypes, aCallback) { - AddonManagerInternal.getAddonsWithOperationsByTypes(aTypes, aCallback); - }, - - getAddonsByTypes: function(aTypes, aCallback) { - AddonManagerInternal.getAddonsByTypes(aTypes, aCallback); - }, - - getAllAddons: function(aCallback) { - AddonManagerInternal.getAllAddons(aCallback); - }, - - getInstallsByTypes: function(aTypes, aCallback) { - AddonManagerInternal.getInstallsByTypes(aTypes, aCallback); - }, - - getAllInstalls: function(aCallback) { - AddonManagerInternal.getAllInstalls(aCallback); - }, - - mapURIToAddonID: function(aURI) { - return AddonManagerInternal.mapURIToAddonID(aURI); - }, - - isInstallEnabled: function(aType) { - return AddonManagerInternal.isInstallEnabled(aType); - }, - - isInstallAllowed: function(aType, aInstallingPrincipal) { - return AddonManagerInternal.isInstallAllowed(aType, aInstallingPrincipal); - }, - - installAddonsFromWebpage: function(aType, aBrowser, aInstallingPrincipal, - aInstalls) { - AddonManagerInternal.installAddonsFromWebpage(aType, aBrowser, - aInstallingPrincipal, - aInstalls); - }, - - installTemporaryAddon: function(aDirectory) { - return AddonManagerInternal.installTemporaryAddon(aDirectory); - }, - - installAddonFromSources: function(aDirectory) { - return AddonManagerInternal.installAddonFromSources(aDirectory); - }, - - getAddonByInstanceID: function(aInstanceID) { - return AddonManagerInternal.getAddonByInstanceID(aInstanceID); - }, - - addManagerListener: function(aListener) { - AddonManagerInternal.addManagerListener(aListener); - }, - - removeManagerListener: function(aListener) { - AddonManagerInternal.removeManagerListener(aListener); - }, - - addInstallListener: function(aListener) { - AddonManagerInternal.addInstallListener(aListener); - }, - - removeInstallListener: function(aListener) { - AddonManagerInternal.removeInstallListener(aListener); - }, - - getUpgradeListener: function(aId) { - return AddonManagerInternal.upgradeListeners.get(aId); - }, - - addUpgradeListener: function(aInstanceID, aCallback) { - AddonManagerInternal.addUpgradeListener(aInstanceID, aCallback); - }, - - removeUpgradeListener: function(aInstanceID) { - AddonManagerInternal.removeUpgradeListener(aInstanceID); - }, - - addAddonListener: function(aListener) { - AddonManagerInternal.addAddonListener(aListener); - }, - - removeAddonListener: function(aListener) { - AddonManagerInternal.removeAddonListener(aListener); - }, - - addTypeListener: function(aListener) { - AddonManagerInternal.addTypeListener(aListener); - }, - - removeTypeListener: function(aListener) { - AddonManagerInternal.removeTypeListener(aListener); - }, - - get addonTypes() { - return AddonManagerInternal.addonTypes; - }, - - /** - * Determines whether an Addon should auto-update or not. - * - * @param aAddon - * The Addon representing the add-on - * @return true if the addon should auto-update, false otherwise. - */ - shouldAutoUpdate: function(aAddon) { - if (!aAddon || typeof aAddon != "object") - throw Components.Exception("aAddon must be specified", - Cr.NS_ERROR_INVALID_ARG); - - if (!("applyBackgroundUpdates" in aAddon)) - return false; - if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_ENABLE) - return true; - if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE) - return false; - return this.autoUpdateDefault; - }, - - get checkCompatibility() { - return AddonManagerInternal.checkCompatibility; - }, - - set checkCompatibility(aValue) { - AddonManagerInternal.checkCompatibility = aValue; - }, - - get strictCompatibility() { - return AddonManagerInternal.strictCompatibility; - }, - - set strictCompatibility(aValue) { - AddonManagerInternal.strictCompatibility = aValue; - }, - - get checkUpdateSecurityDefault() { - return AddonManagerInternal.checkUpdateSecurityDefault; - }, - - get checkUpdateSecurity() { - return AddonManagerInternal.checkUpdateSecurity; - }, - - set checkUpdateSecurity(aValue) { - AddonManagerInternal.checkUpdateSecurity = aValue; - }, - - get updateEnabled() { - return AddonManagerInternal.updateEnabled; - }, - - set updateEnabled(aValue) { - AddonManagerInternal.updateEnabled = aValue; - }, - - get autoUpdateDefault() { - return AddonManagerInternal.autoUpdateDefault; - }, - - set autoUpdateDefault(aValue) { - AddonManagerInternal.autoUpdateDefault = aValue; - }, - - get hotfixID() { - return AddonManagerInternal.hotfixID; - }, - - escapeAddonURI: function(aAddon, aUri, aAppVersion) { - return AddonManagerInternal.escapeAddonURI(aAddon, aUri, aAppVersion); - }, - - getPreferredIconURL: function(aAddon, aSize, aWindow = undefined) { - return AddonManagerInternal.getPreferredIconURL(aAddon, aSize, aWindow); - }, - - get webAPI() { - return AddonManagerInternal.webAPI; - }, - - get shutdown() { - return gShutdownBarrier.client; - }, -}; - -this.AddonManager.init(); - -// load the timestamps module into AddonManagerInternal -Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", AddonManagerInternal); -Object.freeze(AddonManagerInternal); -Object.freeze(AddonManagerPrivate); -Object.freeze(AddonManager); diff --git a/toolkit/mozapps/webextensions/AddonManagerWebAPI.cpp b/toolkit/mozapps/webextensions/AddonManagerWebAPI.cpp deleted file mode 100644 index 3f2a7a529..000000000 --- a/toolkit/mozapps/webextensions/AddonManagerWebAPI.cpp +++ /dev/null @@ -1,171 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* 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 "AddonManagerWebAPI.h" - -#include "mozilla/dom/Navigator.h" -#include "mozilla/dom/NavigatorBinding.h" - -#include "mozilla/Preferences.h" -#include "nsGlobalWindow.h" - -#include "nsIDocShell.h" -#include "nsIScriptObjectPrincipal.h" - -namespace mozilla { -using namespace mozilla::dom; - -static bool -IsValidHost(const nsACString& host) { - // This is ugly, but Preferences.h doesn't have support - // for default prefs or locked prefs - nsCOMPtr<nsIPrefService> prefService (do_GetService(NS_PREFSERVICE_CONTRACTID)); - nsCOMPtr<nsIPrefBranch> prefs; - if (prefService) { - prefService->GetDefaultBranch(nullptr, getter_AddRefs(prefs)); - bool isEnabled; - if (NS_SUCCEEDED(prefs->GetBoolPref("xpinstall.enabled", &isEnabled)) && !isEnabled) { - bool isLocked; - prefs->PrefIsLocked("xpinstall.enabled", &isLocked); - if (isLocked) { - return false; - } - } - } - - if (host.Equals("addons.mozilla.org") || - host.Equals("discovery.addons.mozilla.org") || - host.Equals("testpilot.firefox.com")) { - return true; - } - - // When testing allow access to the developer sites. - if (Preferences::GetBool("extensions.webapi.testing", false)) { - if (host.LowerCaseEqualsLiteral("addons.allizom.org") || - host.LowerCaseEqualsLiteral("discovery.addons.allizom.org") || - host.LowerCaseEqualsLiteral("addons-dev.allizom.org") || - host.LowerCaseEqualsLiteral("discovery.addons-dev.allizom.org") || - host.LowerCaseEqualsLiteral("testpilot.stage.mozaws.net") || - host.LowerCaseEqualsLiteral("testpilot.dev.mozaws.net") || - host.LowerCaseEqualsLiteral("example.com")) { - return true; - } - } - - return false; -} - -// Checks if the given uri is secure and matches one of the hosts allowed to -// access the API. -bool -AddonManagerWebAPI::IsValidSite(nsIURI* uri) -{ - if (!uri) { - return false; - } - - bool isSecure; - nsresult rv = uri->SchemeIs("https", &isSecure); - if (NS_FAILED(rv) || !isSecure) { - return false; - } - - nsAutoCString host; - rv = uri->GetHost(host); - if (NS_FAILED(rv)) { - return false; - } - - return IsValidHost(host); -} - -bool -AddonManagerWebAPI::IsAPIEnabled(JSContext* cx, JSObject* obj) -{ - nsGlobalWindow* global = xpc::WindowGlobalOrNull(obj); - if (!global) { - return false; - } - - nsCOMPtr<nsPIDOMWindowInner> win = global->AsInner(); - if (!win) { - return false; - } - - // Check that the current window and all parent frames are allowed access to - // the API. - while (win) { - nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(win); - if (!sop) { - return false; - } - - nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); - if (!principal) { - return false; - } - - // Reaching a window with a system principal means we have reached - // privileged UI of some kind so stop at this point and allow access. - if (principal->GetIsSystemPrincipal()) { - return true; - } - - nsCOMPtr<nsIDocShell> docShell = win->GetDocShell(); - if (!docShell) { - // This window has been torn down so don't allow access to the API. - return false; - } - - if (!IsValidSite(win->GetDocumentURI())) { - return false; - } - - // Checks whether there is a parent frame of the same type. This won't cross - // mozbrowser or chrome boundaries. - nsCOMPtr<nsIDocShellTreeItem> parent; - nsresult rv = docShell->GetSameTypeParent(getter_AddRefs(parent)); - if (NS_FAILED(rv)) { - return false; - } - - if (!parent) { - // No parent means we've hit a mozbrowser or chrome boundary so allow - // access to the API. - return true; - } - - nsIDocument* doc = win->GetDoc(); - if (!doc) { - return false; - } - - doc = doc->GetParentDocument(); - if (!doc) { - // Getting here means something has been torn down so fail safe. - return false; - } - - - win = doc->GetInnerWindow(); - } - - // Found a document with no inner window, don't grant access to the API. - return false; -} - -namespace dom { - -bool -AddonManagerPermissions::IsHostPermitted(const GlobalObject& /*unused*/, const nsAString& host) -{ - return IsValidHost(NS_ConvertUTF16toUTF8(host)); -} - -} // namespace mozilla::dom - - -} // namespace mozilla diff --git a/toolkit/mozapps/webextensions/AddonManagerWebAPI.h b/toolkit/mozapps/webextensions/AddonManagerWebAPI.h deleted file mode 100644 index 6830bc91f..000000000 --- a/toolkit/mozapps/webextensions/AddonManagerWebAPI.h +++ /dev/null @@ -1,33 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* 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 addonmanagerwebapi_h_ -#define addonmanagerwebapi_h_ - -#include "nsPIDOMWindow.h" - -namespace mozilla { - -class AddonManagerWebAPI { -public: - static bool IsAPIEnabled(JSContext* cx, JSObject* obj); - -private: - static bool IsValidSite(nsIURI* uri); -}; - -namespace dom { - -class AddonManagerPermissions { -public: - static bool IsHostPermitted(const GlobalObject&, const nsAString& host); -}; - -} // namespace mozilla::dom - -} // namespace mozilla - -#endif // addonmanagerwebapi_h_ diff --git a/toolkit/mozapps/webextensions/AddonPathService.cpp b/toolkit/mozapps/webextensions/AddonPathService.cpp deleted file mode 100644 index 8a405c0ea..000000000 --- a/toolkit/mozapps/webextensions/AddonPathService.cpp +++ /dev/null @@ -1,258 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* 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 "AddonPathService.h" - -#include "amIAddonManager.h" -#include "nsIURI.h" -#include "nsXULAppAPI.h" -#include "jsapi.h" -#include "nsServiceManagerUtils.h" -#include "nsLiteralString.h" -#include "nsThreadUtils.h" -#include "nsIIOService.h" -#include "nsNetUtil.h" -#include "nsIAddonPolicyService.h" -#include "nsIFileURL.h" -#include "nsIResProtocolHandler.h" -#include "nsIChromeRegistry.h" -#include "nsIJARURI.h" -#include "nsJSUtils.h" -#include "mozilla/dom/ScriptSettings.h" -#include "mozilla/dom/ToJSValue.h" -#include "mozilla/AddonPathService.h" -#include "mozilla/Omnijar.h" - -#include <algorithm> - -namespace mozilla { - -struct PathEntryComparator -{ - typedef AddonPathService::PathEntry PathEntry; - - bool Equals(const PathEntry& entry1, const PathEntry& entry2) const - { - return entry1.mPath == entry2.mPath; - } - - bool LessThan(const PathEntry& entry1, const PathEntry& entry2) const - { - return entry1.mPath < entry2.mPath; - } -}; - -AddonPathService::AddonPathService() -{ -} - -AddonPathService::~AddonPathService() -{ - sInstance = nullptr; -} - -NS_IMPL_ISUPPORTS(AddonPathService, amIAddonPathService) - -AddonPathService *AddonPathService::sInstance; - -/* static */ AddonPathService* -AddonPathService::GetInstance() -{ - if (!sInstance) { - sInstance = new AddonPathService(); - } - NS_ADDREF(sInstance); - return sInstance; -} - -static JSAddonId* -ConvertAddonId(const nsAString& addonIdString) -{ - AutoSafeJSContext cx; - JS::RootedValue strv(cx); - if (!mozilla::dom::ToJSValue(cx, addonIdString, &strv)) { - return nullptr; - } - JS::RootedString str(cx, strv.toString()); - return JS::NewAddonId(cx, str); -} - -JSAddonId* -AddonPathService::Find(const nsAString& path) -{ - // Use binary search to find the nearest entry that is <= |path|. - PathEntryComparator comparator; - unsigned index = mPaths.IndexOfFirstElementGt(PathEntry(path, nullptr), comparator); - if (index == 0) { - return nullptr; - } - const PathEntry& entry = mPaths[index - 1]; - - // Return the entry's addon if its path is a prefix of |path|. - if (StringBeginsWith(path, entry.mPath)) { - return entry.mAddonId; - } - return nullptr; -} - -NS_IMETHODIMP -AddonPathService::FindAddonId(const nsAString& path, nsAString& addonIdString) -{ - if (JSAddonId* id = Find(path)) { - JSFlatString* flat = JS_ASSERT_STRING_IS_FLAT(JS::StringOfAddonId(id)); - AssignJSFlatString(addonIdString, flat); - } - return NS_OK; -} - -/* static */ JSAddonId* -AddonPathService::FindAddonId(const nsAString& path) -{ - // If no service has been created, then we're not going to find anything. - if (!sInstance) { - return nullptr; - } - - return sInstance->Find(path); -} - -NS_IMETHODIMP -AddonPathService::InsertPath(const nsAString& path, const nsAString& addonIdString) -{ - JSAddonId* addonId = ConvertAddonId(addonIdString); - - // Add the new path in sorted order. - PathEntryComparator comparator; - mPaths.InsertElementSorted(PathEntry(path, addonId), comparator); - return NS_OK; -} - -NS_IMETHODIMP -AddonPathService::MapURIToAddonId(nsIURI* aURI, nsAString& addonIdString) -{ - if (JSAddonId* id = MapURIToAddonID(aURI)) { - JSFlatString* flat = JS_ASSERT_STRING_IS_FLAT(JS::StringOfAddonId(id)); - AssignJSFlatString(addonIdString, flat); - } - return NS_OK; -} - -static nsresult -ResolveURI(nsIURI* aURI, nsAString& out) -{ - bool equals; - nsresult rv; - nsCOMPtr<nsIURI> uri; - nsAutoCString spec; - - // Resolve resource:// URIs. At the end of this if/else block, we - // have both spec and uri variables identifying the same URI. - if (NS_SUCCEEDED(aURI->SchemeIs("resource", &equals)) && equals) { - nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - nsCOMPtr<nsIProtocolHandler> ph; - rv = ioService->GetProtocolHandler("resource", getter_AddRefs(ph)); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - nsCOMPtr<nsIResProtocolHandler> irph(do_QueryInterface(ph, &rv)); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - rv = irph->ResolveURI(aURI, spec); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - rv = ioService->NewURI(spec, nullptr, nullptr, getter_AddRefs(uri)); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - } else if (NS_SUCCEEDED(aURI->SchemeIs("chrome", &equals)) && equals) { - nsCOMPtr<nsIChromeRegistry> chromeReg = - mozilla::services::GetChromeRegistryService(); - if (NS_WARN_IF(!chromeReg)) - return NS_ERROR_UNEXPECTED; - - rv = chromeReg->ConvertChromeURL(aURI, getter_AddRefs(uri)); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - } else { - uri = aURI; - } - - if (NS_SUCCEEDED(uri->SchemeIs("jar", &equals)) && equals) { - nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - nsCOMPtr<nsIURI> jarFileURI; - rv = jarURI->GetJARFile(getter_AddRefs(jarFileURI)); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - return ResolveURI(jarFileURI, out); - } - - if (NS_SUCCEEDED(uri->SchemeIs("file", &equals)) && equals) { - nsCOMPtr<nsIFileURL> baseFileURL = do_QueryInterface(uri, &rv); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - nsCOMPtr<nsIFile> file; - rv = baseFileURL->GetFile(getter_AddRefs(file)); - if (NS_WARN_IF(NS_FAILED(rv))) - return rv; - - return file->GetPath(out); - } - return NS_ERROR_FAILURE; -} - -JSAddonId* -MapURIToAddonID(nsIURI* aURI) -{ - if (!NS_IsMainThread() || !XRE_IsParentProcess()) { - return nullptr; - } - - bool equals; - nsresult rv; - if (NS_SUCCEEDED(aURI->SchemeIs("moz-extension", &equals)) && equals) { - nsCOMPtr<nsIAddonPolicyService> service = do_GetService("@mozilla.org/addons/policy-service;1"); - if (service) { - nsString addonId; - rv = service->ExtensionURIToAddonId(aURI, addonId); - if (NS_FAILED(rv)) - return nullptr; - - return ConvertAddonId(addonId); - } - } - - nsAutoString filePath; - rv = ResolveURI(aURI, filePath); - if (NS_FAILED(rv)) - return nullptr; - - nsCOMPtr<nsIFile> greJar = Omnijar::GetPath(Omnijar::GRE); - nsCOMPtr<nsIFile> appJar = Omnijar::GetPath(Omnijar::APP); - if (greJar && appJar) { - nsAutoString greJarString, appJarString; - if (NS_FAILED(greJar->GetPath(greJarString)) || NS_FAILED(appJar->GetPath(appJarString))) - return nullptr; - - // If |aURI| is part of either Omnijar, then it can't be part of an - // add-on. This catches pretty much all URLs for Firefox content. - if (filePath.Equals(greJarString) || filePath.Equals(appJarString)) - return nullptr; - } - - // If it's not part of Firefox, we resort to binary searching through the - // add-on paths. - return AddonPathService::FindAddonId(filePath); -} - -} // namespace mozilla diff --git a/toolkit/mozapps/webextensions/AddonPathService.h b/toolkit/mozapps/webextensions/AddonPathService.h deleted file mode 100644 index f739b018f..000000000 --- a/toolkit/mozapps/webextensions/AddonPathService.h +++ /dev/null @@ -1,55 +0,0 @@ -//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-/ -/* 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 AddonPathService_h -#define AddonPathService_h - -#include "amIAddonPathService.h" -#include "nsString.h" -#include "nsTArray.h" - -class nsIURI; -class JSAddonId; - -namespace mozilla { - -JSAddonId* -MapURIToAddonID(nsIURI* aURI); - -class AddonPathService final : public amIAddonPathService -{ -public: - AddonPathService(); - - static AddonPathService* GetInstance(); - - JSAddonId* Find(const nsAString& path); - static JSAddonId* FindAddonId(const nsAString& path); - - NS_DECL_ISUPPORTS - NS_DECL_AMIADDONPATHSERVICE - - struct PathEntry - { - nsString mPath; - JSAddonId* mAddonId; - - PathEntry(const nsAString& aPath, JSAddonId* aAddonId) - : mPath(aPath), mAddonId(aAddonId) - {} - }; - -private: - virtual ~AddonPathService(); - - // Paths are stored sorted in order of their mPath. - nsTArray<PathEntry> mPaths; - - static AddonPathService* sInstance; -}; - -} // namespace mozilla - -#endif diff --git a/toolkit/mozapps/webextensions/LightweightThemeManager.jsm b/toolkit/mozapps/webextensions/LightweightThemeManager.jsm deleted file mode 100644 index 5dd41831d..000000000 --- a/toolkit/mozapps/webextensions/LightweightThemeManager.jsm +++ /dev/null @@ -1,909 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["LightweightThemeManager"]; - -const Cc = Components.classes; -const Ci = Components.interfaces; - -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://gre/modules/AddonManager.jsm"); -/* globals AddonManagerPrivate*/ -Components.utils.import("resource://gre/modules/Services.jsm"); - -const ID_SUFFIX = "@personas.mozilla.org"; -const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect"; -const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; -const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; -const ADDON_TYPE = "theme"; - -const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; - -const STRING_TYPE_NAME = "type.%ID%.name"; - -const DEFAULT_MAX_USED_THEMES_COUNT = 30; - -const MAX_PREVIEW_SECONDS = 30; - -const MANDATORY = ["id", "name", "headerURL"]; -const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL", - "previewURL", "author", "description", "homepageURL", - "updateURL", "version"]; - -const PERSIST_ENABLED = true; -const PERSIST_BYPASS_CACHE = false; -const PERSIST_FILES = { - headerURL: "lightweighttheme-header", - footerURL: "lightweighttheme-footer" -}; - -XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer", - "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest", - "resource://gre/modules/ServiceRequest.jsm"); - - -XPCOMUtils.defineLazyGetter(this, "_prefs", () => { - return Services.prefs.getBranch("lightweightThemes."); -}); - -Object.defineProperty(this, "_maxUsedThemes", { - get: function() { - delete this._maxUsedThemes; - try { - this._maxUsedThemes = _prefs.getIntPref("maxUsedThemes"); - } - catch (e) { - this._maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT; - } - return this._maxUsedThemes; - }, - - set: function(val) { - delete this._maxUsedThemes; - return this._maxUsedThemes = val; - }, - configurable: true, -}); - -// Holds the ID of the theme being enabled or disabled while sending out the -// events so cached AddonWrapper instances can return correct values for -// permissions and pendingOperations -var _themeIDBeingEnabled = null; -var _themeIDBeingDisabled = null; - -// Convert from the old storage format (in which the order of usedThemes -// was combined with isThemeSelected to determine which theme was selected) -// to the new one (where a selectedThemeID determines which theme is selected). -(function() { - let wasThemeSelected = false; - try { - wasThemeSelected = _prefs.getBoolPref("isThemeSelected"); - } catch (e) { } - - if (wasThemeSelected) { - _prefs.clearUserPref("isThemeSelected"); - let themes = []; - try { - themes = JSON.parse(_prefs.getComplexValue("usedThemes", - Ci.nsISupportsString).data); - } catch (e) { } - - if (Array.isArray(themes) && themes[0]) { - _prefs.setCharPref("selectedThemeID", themes[0].id); - } - } -})(); - -this.LightweightThemeManager = { - get name() { - return "LightweightThemeManager"; - }, - - // Themes that can be added for an application. They can't be removed, and - // will always show up at the top of the list. - _builtInThemes: new Map(), - - get usedThemes () { - let themes = []; - try { - themes = JSON.parse(_prefs.getComplexValue("usedThemes", - Ci.nsISupportsString).data); - } catch (e) { } - - themes.push(...this._builtInThemes.values()); - return themes; - }, - - get currentTheme () { - let selectedThemeID = null; - try { - selectedThemeID = _prefs.getCharPref("selectedThemeID"); - } catch (e) {} - - let data = null; - if (selectedThemeID) { - data = this.getUsedTheme(selectedThemeID); - } - return data; - }, - - get currentThemeForDisplay () { - var data = this.currentTheme; - - if (data && PERSIST_ENABLED) { - for (let key in PERSIST_FILES) { - try { - if (data[key] && _prefs.getBoolPref("persisted." + key)) - data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec - + "?" + data.id + ";" + _version(data); - } catch (e) {} - } - } - - return data; - }, - - set currentTheme (aData) { - return _setCurrentTheme(aData, false); - }, - - setLocalTheme: function(aData) { - _setCurrentTheme(aData, true); - }, - - getUsedTheme: function(aId) { - var usedThemes = this.usedThemes; - for (let usedTheme of usedThemes) { - if (usedTheme.id == aId) - return usedTheme; - } - return null; - }, - - forgetUsedTheme: function(aId) { - let theme = this.getUsedTheme(aId); - if (!theme || LightweightThemeManager._builtInThemes.has(theme.id)) - return; - - let wrapper = new AddonWrapper(theme); - AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); - - var currentTheme = this.currentTheme; - if (currentTheme && currentTheme.id == aId) { - this.themeChanged(null); - AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false); - } - - _updateUsedThemes(_usedThemesExceptId(aId)); - AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); - }, - - addBuiltInTheme: function(theme) { - if (!theme || !theme.id || this.usedThemes.some(t => t.id == theme.id)) { - throw new Error("Trying to add invalid builtIn theme"); - } - - this._builtInThemes.set(theme.id, theme); - - if (_prefs.getCharPref("selectedThemeID") == theme.id) { - this.currentTheme = theme; - } - }, - - forgetBuiltInTheme: function(id) { - if (!this._builtInThemes.has(id)) { - let currentTheme = this.currentTheme; - if (currentTheme && currentTheme.id == id) { - this.currentTheme = null; - } - } - return this._builtInThemes.delete(id); - }, - - clearBuiltInThemes: function() { - for (let id of this._builtInThemes.keys()) { - this.forgetBuiltInTheme(id); - } - }, - - previewTheme: function(aData) { - let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); - cancel.data = false; - Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested", - JSON.stringify(aData)); - if (cancel.data) - return; - - if (_previewTimer) - _previewTimer.cancel(); - else - _previewTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - _previewTimer.initWithCallback(_previewTimerCallback, - MAX_PREVIEW_SECONDS * 1000, - _previewTimer.TYPE_ONE_SHOT); - - _notifyWindows(aData); - }, - - resetPreview: function() { - if (_previewTimer) { - _previewTimer.cancel(); - _previewTimer = null; - _notifyWindows(this.currentThemeForDisplay); - } - }, - - parseTheme: function(aString, aBaseURI) { - try { - return _sanitizeTheme(JSON.parse(aString), aBaseURI, false); - } catch (e) { - return null; - } - }, - - updateCurrentTheme: function() { - try { - if (!_prefs.getBoolPref("update.enabled")) - return; - } catch (e) { - return; - } - - var theme = this.currentTheme; - if (!theme || !theme.updateURL) - return; - - var req = new ServiceRequest(); - - req.mozBackgroundRequest = true; - req.overrideMimeType("text/plain"); - req.open("GET", theme.updateURL, true); - // Prevent the request from reading from the cache. - req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; - // Prevent the request from writing to the cache. - req.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - - req.addEventListener("load", () => { - if (req.status != 200) - return; - - let newData = this.parseTheme(req.responseText, theme.updateURL); - if (!newData || - newData.id != theme.id || - _version(newData) == _version(theme)) - return; - - var currentTheme = this.currentTheme; - if (currentTheme && currentTheme.id == theme.id) - this.currentTheme = newData; - }, false); - - req.send(null); - }, - - /** - * Switches to a new lightweight theme. - * - * @param aData - * The lightweight theme to switch to - */ - themeChanged: function(aData) { - if (_previewTimer) { - _previewTimer.cancel(); - _previewTimer = null; - } - - if (aData) { - let usedThemes = _usedThemesExceptId(aData.id); - usedThemes.unshift(aData); - _updateUsedThemes(usedThemes); - if (PERSIST_ENABLED) { - LightweightThemeImageOptimizer.purge(); - _persistImages(aData, function() { - _notifyWindows(this.currentThemeForDisplay); - }.bind(this)); - } - } - - if (aData) - _prefs.setCharPref("selectedThemeID", aData.id); - else - _prefs.setCharPref("selectedThemeID", ""); - - _notifyWindows(aData); - Services.obs.notifyObservers(null, "lightweight-theme-changed", null); - }, - - /** - * Starts the Addons provider and enables the new lightweight theme if - * necessary. - */ - startup: function() { - if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) { - let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); - if (id) - this.themeChanged(this.getUsedTheme(id)); - else - this.themeChanged(null); - Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); - } - - _prefs.addObserver("", _prefObserver, false); - }, - - /** - * Shuts down the provider. - */ - shutdown: function() { - _prefs.removeObserver("", _prefObserver); - }, - - /** - * Called when a new add-on has been enabled when only one add-on of that type - * can be enabled. - * - * @param aId - * The ID of the newly enabled add-on - * @param aType - * The type of the newly enabled add-on - * @param aPendingRestart - * true if the newly enabled add-on will only become enabled after a - * restart - */ - addonChanged: function(aId, aType, aPendingRestart) { - if (aType != ADDON_TYPE) - return; - - let id = _getInternalID(aId); - let current = this.currentTheme; - - try { - let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); - if (id == next && aPendingRestart) - return; - - Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); - if (next) { - AddonManagerPrivate.callAddonListeners("onOperationCancelled", - new AddonWrapper(this.getUsedTheme(next))); - } - else if (id == current.id) { - AddonManagerPrivate.callAddonListeners("onOperationCancelled", - new AddonWrapper(current)); - return; - } - } - catch (e) { - } - - if (current) { - if (current.id == id) - return; - _themeIDBeingDisabled = current.id; - let wrapper = new AddonWrapper(current); - if (aPendingRestart) { - Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, ""); - AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true); - } - else { - AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false); - this.themeChanged(null); - AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); - } - _themeIDBeingDisabled = null; - } - - if (id) { - let theme = this.getUsedTheme(id); - _themeIDBeingEnabled = id; - let wrapper = new AddonWrapper(theme); - if (aPendingRestart) { - AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true); - Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id); - - // Flush the preferences to disk so they survive any crash - Services.prefs.savePrefFile(null); - } - else { - AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false); - this.themeChanged(theme); - AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); - } - _themeIDBeingEnabled = null; - } - }, - - /** - * Called to get an Addon with a particular ID. - * - * @param aId - * The ID of the add-on to retrieve - * @param aCallback - * A callback to pass the Addon to - */ - getAddonByID: function(aId, aCallback) { - let id = _getInternalID(aId); - if (!id) { - aCallback(null); - return; - } - - let theme = this.getUsedTheme(id); - if (!theme) { - aCallback(null); - return; - } - - aCallback(new AddonWrapper(theme)); - }, - - /** - * Called to get Addons of a particular type. - * - * @param aTypes - * An array of types to fetch. Can be null to get all types. - * @param aCallback - * A callback to pass an array of Addons to - */ - getAddonsByTypes: function(aTypes, aCallback) { - if (aTypes && aTypes.indexOf(ADDON_TYPE) == -1) { - aCallback([]); - return; - } - - aCallback(this.usedThemes.map(a => new AddonWrapper(a))); - }, -}; - -const wrapperMap = new WeakMap(); -let themeFor = wrapper => wrapperMap.get(wrapper); - -/** - * The AddonWrapper wraps lightweight theme to provide the data visible to - * consumers of the AddonManager API. - */ -function AddonWrapper(aTheme) { - wrapperMap.set(this, aTheme); -} - -AddonWrapper.prototype = { - get id() { - return themeFor(this).id + ID_SUFFIX; - }, - - get type() { - return ADDON_TYPE; - }, - - get isActive() { - let current = LightweightThemeManager.currentTheme; - if (current) - return themeFor(this).id == current.id; - return false; - }, - - get name() { - return themeFor(this).name; - }, - - get version() { - let theme = themeFor(this); - return "version" in theme ? theme.version : ""; - }, - - get creator() { - let theme = themeFor(this); - return "author" in theme ? new AddonManagerPrivate.AddonAuthor(theme.author) : null; - }, - - get screenshots() { - let url = themeFor(this).previewURL; - return [new AddonManagerPrivate.AddonScreenshot(url)]; - }, - - get pendingOperations() { - let pending = AddonManager.PENDING_NONE; - if (this.isActive == this.userDisabled) - pending |= this.isActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE; - return pending; - }, - - get operationsRequiringRestart() { - // If a non-default theme is in use then a restart will be required to - // enable lightweight themes unless dynamic theme switching is enabled - if (Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) { - try { - if (Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED)) - return AddonManager.OP_NEEDS_RESTART_NONE; - } - catch (e) { - } - return AddonManager.OP_NEEDS_RESTART_ENABLE; - } - - return AddonManager.OP_NEEDS_RESTART_NONE; - }, - - get size() { - // The size changes depending on whether the theme is in use or not, this is - // probably not worth exposing. - return null; - }, - - get permissions() { - let permissions = 0; - - // Do not allow uninstall of builtIn themes. - if (!LightweightThemeManager._builtInThemes.has(themeFor(this).id)) - permissions = AddonManager.PERM_CAN_UNINSTALL; - if (this.userDisabled) - permissions |= AddonManager.PERM_CAN_ENABLE; - else - permissions |= AddonManager.PERM_CAN_DISABLE; - return permissions; - }, - - get userDisabled() { - let id = themeFor(this).id; - if (_themeIDBeingEnabled == id) - return false; - if (_themeIDBeingDisabled == id) - return true; - - try { - let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); - return id != toSelect; - } - catch (e) { - let current = LightweightThemeManager.currentTheme; - return !current || current.id != id; - } - }, - - set userDisabled(val) { - if (val == this.userDisabled) - return val; - - if (val) - LightweightThemeManager.currentTheme = null; - else - LightweightThemeManager.currentTheme = themeFor(this); - - return val; - }, - - // Lightweight themes are never disabled by the application - get appDisabled() { - return false; - }, - - // Lightweight themes are always compatible - get isCompatible() { - return true; - }, - - get isPlatformCompatible() { - return true; - }, - - get scope() { - return AddonManager.SCOPE_PROFILE; - }, - - get foreignInstall() { - return false; - }, - - uninstall: function() { - LightweightThemeManager.forgetUsedTheme(themeFor(this).id); - }, - - cancelUninstall: function() { - throw new Error("Theme is not marked to be uninstalled"); - }, - - findUpdates: function(listener, reason, appVersion, platformVersion) { - AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion); - }, - - // Lightweight themes are always compatible - isCompatibleWith: function(appVersion, platformVersion) { - return true; - }, - - // Lightweight themes are always securely updated - get providesUpdatesSecurely() { - return true; - }, - - // Lightweight themes are never blocklisted - get blocklistState() { - return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; - } -}; - -["description", "homepageURL", "iconURL"].forEach(function(prop) { - Object.defineProperty(AddonWrapper.prototype, prop, { - get: function() { - let theme = themeFor(this); - return prop in theme ? theme[prop] : null; - }, - enumarable: true, - }); -}); - -["installDate", "updateDate"].forEach(function(prop) { - Object.defineProperty(AddonWrapper.prototype, prop, { - get: function() { - let theme = themeFor(this); - return prop in theme ? new Date(theme[prop]) : null; - }, - enumarable: true, - }); -}); - -/** - * Converts the ID used by the public AddonManager API to an lightweight theme - * ID. - * - * @param id - * The ID to be converted - * - * @return the lightweight theme ID or null if the ID was not for a lightweight - * theme. - */ -function _getInternalID(id) { - if (!id) - return null; - let len = id.length - ID_SUFFIX.length; - if (len > 0 && id.substring(len) == ID_SUFFIX) - return id.substring(0, len); - return null; -} - -function _setCurrentTheme(aData, aLocal) { - aData = _sanitizeTheme(aData, null, aLocal); - - let needsRestart = (ADDON_TYPE == "theme") && - Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN); - - let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); - cancel.data = false; - Services.obs.notifyObservers(cancel, "lightweight-theme-change-requested", - JSON.stringify(aData)); - - if (aData) { - let theme = LightweightThemeManager.getUsedTheme(aData.id); - let isInstall = !theme || theme.version != aData.version; - if (isInstall) { - aData.updateDate = Date.now(); - if (theme && "installDate" in theme) - aData.installDate = theme.installDate; - else - aData.installDate = aData.updateDate; - - var oldWrapper = theme ? new AddonWrapper(theme) : null; - var wrapper = new AddonWrapper(aData); - AddonManagerPrivate.callInstallListeners("onExternalInstall", null, - wrapper, oldWrapper, false); - AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false); - } - - let current = LightweightThemeManager.currentTheme; - let usedThemes = _usedThemesExceptId(aData.id); - if (current && current.id != aData.id) - usedThemes.splice(1, 0, aData); - else - usedThemes.unshift(aData); - _updateUsedThemes(usedThemes); - - if (isInstall) - AddonManagerPrivate.callAddonListeners("onInstalled", wrapper); - } - - if (cancel.data) - return null; - - AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null, - ADDON_TYPE, needsRestart); - - return LightweightThemeManager.currentTheme; -} - -function _sanitizeTheme(aData, aBaseURI, aLocal) { - if (!aData || typeof aData != "object") - return null; - - var resourceProtocols = ["http", "https", "resource"]; - if (aLocal) - resourceProtocols.push("file"); - var resourceProtocolExp = new RegExp("^(" + resourceProtocols.join("|") + "):"); - - function sanitizeProperty(prop) { - if (!(prop in aData)) - return null; - if (typeof aData[prop] != "string") - return null; - let val = aData[prop].trim(); - if (!val) - return null; - - if (!/URL$/.test(prop)) - return val; - - try { - val = _makeURI(val, aBaseURI ? _makeURI(aBaseURI) : null).spec; - if ((prop == "updateURL" ? /^https:/ : resourceProtocolExp).test(val)) - return val; - return null; - } - catch (e) { - return null; - } - } - - let result = {}; - for (let mandatoryProperty of MANDATORY) { - let val = sanitizeProperty(mandatoryProperty); - if (!val) - throw Components.results.NS_ERROR_INVALID_ARG; - result[mandatoryProperty] = val; - } - - for (let optionalProperty of OPTIONAL) { - let val = sanitizeProperty(optionalProperty); - if (!val) - continue; - result[optionalProperty] = val; - } - - return result; -} - -function _usedThemesExceptId(aId) { - return LightweightThemeManager.usedThemes.filter(function(t) { - return "id" in t && t.id != aId; - }); -} - -function _version(aThemeData) { - return aThemeData.version || ""; -} - -function _makeURI(aURL, aBaseURI) { - return Services.io.newURI(aURL, null, aBaseURI); -} - -function _updateUsedThemes(aList) { - // Remove app-specific themes before saving them to the usedThemes pref. - aList = aList.filter(theme => !LightweightThemeManager._builtInThemes.has(theme.id)); - - // Send uninstall events for all themes that need to be removed. - while (aList.length > _maxUsedThemes) { - let wrapper = new AddonWrapper(aList[aList.length - 1]); - AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); - aList.pop(); - AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); - } - - var str = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - str.data = JSON.stringify(aList); - _prefs.setComplexValue("usedThemes", Ci.nsISupportsString, str); - - Services.obs.notifyObservers(null, "lightweight-theme-list-changed", null); -} - -function _notifyWindows(aThemeData) { - Services.obs.notifyObservers(null, "lightweight-theme-styling-update", - JSON.stringify(aThemeData)); -} - -var _previewTimer; -var _previewTimerCallback = { - notify: function() { - LightweightThemeManager.resetPreview(); - } -}; - -/** - * Called when any of the lightweightThemes preferences are changed. - */ -function _prefObserver(aSubject, aTopic, aData) { - switch (aData) { - case "maxUsedThemes": - try { - _maxUsedThemes = _prefs.getIntPref(aData); - } - catch (e) { - _maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT; - } - // Update the theme list to remove any themes over the number we keep - _updateUsedThemes(LightweightThemeManager.usedThemes); - break; - } -} - -function _persistImages(aData, aCallback) { - function onSuccess(key) { - return function () { - let current = LightweightThemeManager.currentTheme; - if (current && current.id == aData.id) { - _prefs.setBoolPref("persisted." + key, true); - } - if (--numFilesToPersist == 0 && aCallback) { - aCallback(); - } - }; - } - - let numFilesToPersist = 0; - for (let key in PERSIST_FILES) { - _prefs.setBoolPref("persisted." + key, false); - if (aData[key]) { - numFilesToPersist++; - _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key)); - } - } -} - -function _getLocalImageURI(localFileName) { - var localFile = Services.dirsvc.get("ProfD", Ci.nsIFile); - localFile.append(localFileName); - return Services.io.newFileURI(localFile); -} - -function _persistImage(sourceURL, localFileName, successCallback) { - if (/^(file|resource):/.test(sourceURL)) - return; - - var targetURI = _getLocalImageURI(localFileName); - var sourceURI = _makeURI(sourceURL); - - var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] - .createInstance(Ci.nsIWebBrowserPersist); - - persist.persistFlags = - Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | - Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION | - (PERSIST_BYPASS_CACHE ? - Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE : - Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE); - - persist.progressListener = new _persistProgressListener(successCallback); - - persist.saveURI(sourceURI, null, - null, Ci.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE, - null, null, targetURI, null); -} - -function _persistProgressListener(successCallback) { - this.onLocationChange = function() {}; - this.onProgressChange = function() {}; - this.onStatusChange = function() {}; - this.onSecurityChange = function() {}; - this.onStateChange = function(aWebProgress, aRequest, aStateFlags, aStatus) { - if (aRequest && - aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && - aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { - try { - if (aRequest.QueryInterface(Ci.nsIHttpChannel).requestSucceeded) { - // success - successCallback(); - return; - } - } catch (e) { } - // failure - } - }; -} - -AddonManagerPrivate.registerProvider(LightweightThemeManager, [ - new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 5000) -]); diff --git a/toolkit/mozapps/webextensions/addonManager.js b/toolkit/mozapps/webextensions/addonManager.js deleted file mode 100644 index d34cbaf62..000000000 --- a/toolkit/mozapps/webextensions/addonManager.js +++ /dev/null @@ -1,296 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * This component serves as integration between the platform and AddonManager. - * It is responsible for initializing and shutting down the AddonManager as well - * as passing new installs from webpages to the AddonManager. - */ - -"use strict"; - -const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -// The old XPInstall error codes -const EXECUTION_ERROR = -203; -const CANT_READ_ARCHIVE = -207; -const USER_CANCELLED = -210; -const DOWNLOAD_ERROR = -228; -const UNSUPPORTED_TYPE = -244; -const SUCCESS = 0; - -const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled"; -const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage"; -const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback"; - -const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest"; -const MSG_PROMISE_RESULT = "WebAPIPromiseResult"; -const MSG_INSTALL_EVENT = "WebAPIInstallEvent"; -const MSG_INSTALL_CLEANUP = "WebAPICleanup"; -const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest"; -const MSG_ADDON_EVENT = "WebAPIAddonEvent"; - -const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js"; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -var gSingleton = null; - -function amManager() { - Cu.import("resource://gre/modules/AddonManager.jsm"); - /* globals AddonManagerPrivate*/ - - Services.mm.loadFrameScript(CHILD_SCRIPT, true); - Services.mm.addMessageListener(MSG_INSTALL_ENABLED, this); - Services.mm.addMessageListener(MSG_INSTALL_ADDONS, this); - Services.mm.addMessageListener(MSG_PROMISE_REQUEST, this); - Services.mm.addMessageListener(MSG_INSTALL_CLEANUP, this); - Services.mm.addMessageListener(MSG_ADDON_EVENT_REQ, this); - - Services.obs.addObserver(this, "message-manager-close", false); - Services.obs.addObserver(this, "message-manager-disconnect", false); - - AddonManager.webAPI.setEventHandler(this.sendEvent); - - // Needed so receiveMessage can be called directly by JS callers - this.wrappedJSObject = this; -} - -amManager.prototype = { - observe: function(aSubject, aTopic, aData) { - switch (aTopic) { - case "addons-startup": - AddonManagerPrivate.startup(); - break; - - case "message-manager-close": - case "message-manager-disconnect": - this.childClosed(aSubject); - break; - } - }, - - /** - * @see amIAddonManager.idl - */ - mapURIToAddonID: function(uri, id) { - id.value = AddonManager.mapURIToAddonID(uri); - return !!id.value; - }, - - /** - * @see amIWebInstaller.idl - */ - isInstallEnabled: function(aMimetype, aReferer) { - return AddonManager.isInstallEnabled(aMimetype); - }, - - /** - * @see amIWebInstaller.idl - */ - installAddonsFromWebpage: function(aMimetype, aBrowser, aInstallingPrincipal, - aUris, aHashes, aNames, aIcons, aCallback) { - if (aUris.length == 0) - return false; - - let retval = true; - if (!AddonManager.isInstallAllowed(aMimetype, aInstallingPrincipal)) { - aCallback = null; - retval = false; - } - - let installs = []; - function buildNextInstall() { - if (aUris.length == 0) { - AddonManager.installAddonsFromWebpage(aMimetype, aBrowser, aInstallingPrincipal, installs); - return; - } - let uri = aUris.shift(); - AddonManager.getInstallForURL(uri, function(aInstall) { - function callCallback(aUri, aStatus) { - try { - aCallback.onInstallEnded(aUri, aStatus); - } - catch (e) { - Components.utils.reportError(e); - } - } - - if (aInstall) { - installs.push(aInstall); - if (aCallback) { - aInstall.addListener({ - onDownloadCancelled: function(aInstall) { - callCallback(uri, USER_CANCELLED); - }, - - onDownloadFailed: function(aInstall) { - if (aInstall.error == AddonManager.ERROR_CORRUPT_FILE) - callCallback(uri, CANT_READ_ARCHIVE); - else - callCallback(uri, DOWNLOAD_ERROR); - }, - - onInstallFailed: function(aInstall) { - callCallback(uri, EXECUTION_ERROR); - }, - - onInstallEnded: function(aInstall, aStatus) { - callCallback(uri, SUCCESS); - } - }); - } - } - else if (aCallback) { - aCallback.onInstallEnded(uri, UNSUPPORTED_TYPE); - } - buildNextInstall(); - }, aMimetype, aHashes.shift(), aNames.shift(), aIcons.shift(), null, aBrowser); - } - buildNextInstall(); - - return retval; - }, - - notify: function(aTimer) { - AddonManagerPrivate.backgroundUpdateTimerHandler(); - }, - - // Maps message manager instances for content processes to the associated - // AddonListener instances. - addonListeners: new Map(), - - _addAddonListener(target) { - if (!this.addonListeners.has(target)) { - let handler = (event, id, needsRestart) => { - target.sendAsyncMessage(MSG_ADDON_EVENT, {event, id, needsRestart}); - }; - let listener = { - onEnabling: (addon, needsRestart) => handler("onEnabling", addon.id, needsRestart), - onEnabled: (addon) => handler("onEnabled", addon.id, false), - onDisabling: (addon, needsRestart) => handler("onDisabling", addon.id, needsRestart), - onDisabled: (addon) => handler("onDisabled", addon.id, false), - onInstalling: (addon, needsRestart) => handler("onInstalling", addon.id, needsRestart), - onInstalled: (addon) => handler("onInstalled", addon.id, false), - onUninstalling: (addon, needsRestart) => handler("onUninstalling", addon.id, needsRestart), - onUninstalled: (addon) => handler("onUninstalled", addon.id, false), - onOperationCancelled: (addon) => handler("onOperationCancelled", addon.id, false), - }; - this.addonListeners.set(target, listener); - AddonManager.addAddonListener(listener); - } - }, - - _removeAddonListener(target) { - if (this.addonListeners.has(target)) { - AddonManager.removeAddonListener(this.addonListeners.get(target)); - this.addonListeners.delete(target); - } - }, - - /** - * messageManager callback function. - * - * Listens to requests from child processes for InstallTrigger - * activity, and sends back callbacks. - */ - receiveMessage: function(aMessage) { - let payload = aMessage.data; - - switch (aMessage.name) { - case MSG_INSTALL_ENABLED: - return AddonManager.isInstallEnabled(payload.mimetype); - - case MSG_INSTALL_ADDONS: { - let callback = null; - if (payload.callbackID != -1) { - let mm = aMessage.target.messageManager; - callback = { - onInstallEnded: function(url, status) { - mm.sendAsyncMessage(MSG_INSTALL_CALLBACK, { - callbackID: payload.callbackID, - url: url, - status: status - }); - }, - }; - } - - return this.installAddonsFromWebpage(payload.mimetype, - aMessage.target, payload.triggeringPrincipal, payload.uris, - payload.hashes, payload.names, payload.icons, callback); - } - - case MSG_PROMISE_REQUEST: { - let mm = aMessage.target.messageManager; - let resolve = (value) => { - mm.sendAsyncMessage(MSG_PROMISE_RESULT, { - callbackID: payload.callbackID, - resolve: value - }); - } - let reject = (value) => { - mm.sendAsyncMessage(MSG_PROMISE_RESULT, { - callbackID: payload.callbackID, - reject: value - }); - } - - let API = AddonManager.webAPI; - if (payload.type in API) { - API[payload.type](aMessage.target, ...payload.args).then(resolve, reject); - } - else { - reject("Unknown Add-on API request."); - } - break; - } - - case MSG_INSTALL_CLEANUP: { - AddonManager.webAPI.clearInstalls(payload.ids); - break; - } - - case MSG_ADDON_EVENT_REQ: { - let target = aMessage.target.messageManager; - if (payload.enabled) { - this._addAddonListener(target); - } else { - this._removeAddonListener(target); - } - } - } - return undefined; - }, - - childClosed(target) { - AddonManager.webAPI.clearInstallsFrom(target); - this._removeAddonListener(target); - }, - - sendEvent(mm, data) { - mm.sendAsyncMessage(MSG_INSTALL_EVENT, data); - }, - - classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"), - _xpcom_factory: { - createInstance: function(aOuter, aIid) { - if (aOuter != null) - throw Components.Exception("Component does not support aggregation", - Cr.NS_ERROR_NO_AGGREGATION); - - if (!gSingleton) - gSingleton = new amManager(); - return gSingleton.QueryInterface(aIid); - } - }, - QueryInterface: XPCOMUtils.generateQI([Ci.amIAddonManager, - Ci.amIWebInstaller, - Ci.nsITimerCallback, - Ci.nsIObserver, - Ci.nsIMessageListener]) -}; - -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([amManager]); diff --git a/toolkit/mozapps/webextensions/amInstallTrigger.js b/toolkit/mozapps/webextensions/amInstallTrigger.js deleted file mode 100644 index 382791d32..000000000 --- a/toolkit/mozapps/webextensions/amInstallTrigger.js +++ /dev/null @@ -1,240 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); - -const XPINSTALL_MIMETYPE = "application/x-xpinstall"; - -const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled"; -const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage"; -const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback"; - - -var log = Log.repository.getLogger("AddonManager.InstallTrigger"); -log.level = Log.Level[Preferences.get("extensions.logging.enabled", false) ? "Warn" : "Trace"]; - -function CallbackObject(id, callback, urls, mediator) { - this.id = id; - this.callback = callback; - this.urls = new Set(urls); - this.callCallback = function(url, status) { - try { - this.callback(url, status); - } - catch (e) { - log.warn("InstallTrigger callback threw an exception: " + e); - } - - this.urls.delete(url); - if (this.urls.size == 0) - mediator._callbacks.delete(id); - }; -} - -function RemoteMediator(window) { - window.QueryInterface(Ci.nsIInterfaceRequestor); - let utils = window.getInterface(Ci.nsIDOMWindowUtils); - this._windowID = utils.currentInnerWindowID; - - this.mm = window - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - this.mm.addWeakMessageListener(MSG_INSTALL_CALLBACK, this); - - this._lastCallbackID = 0; - this._callbacks = new Map(); -} - -RemoteMediator.prototype = { - receiveMessage: function(message) { - if (message.name == MSG_INSTALL_CALLBACK) { - let payload = message.data; - let callbackHandler = this._callbacks.get(payload.callbackID); - if (callbackHandler) { - callbackHandler.callCallback(payload.url, payload.status); - } - } - }, - - enabled: function(url) { - let params = { - mimetype: XPINSTALL_MIMETYPE - }; - return this.mm.sendSyncMessage(MSG_INSTALL_ENABLED, params)[0]; - }, - - install: function(installs, principal, callback, window) { - let callbackID = this._addCallback(callback, installs.uris); - - installs.mimetype = XPINSTALL_MIMETYPE; - installs.triggeringPrincipal = principal; - installs.callbackID = callbackID; - - if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { - // When running in the main process this might be a frame inside an - // in-content UI page, walk up to find the first frame element in a chrome - // privileged document - let element = window.frameElement; - let ssm = Services.scriptSecurityManager; - while (element && !ssm.isSystemPrincipal(element.ownerDocument.nodePrincipal)) - element = element.ownerDocument.defaultView.frameElement; - - if (element) { - let listener = Cc["@mozilla.org/addons/integration;1"]. - getService(Ci.nsIMessageListener); - return listener.wrappedJSObject.receiveMessage({ - name: MSG_INSTALL_ADDONS, - target: element, - data: installs, - }); - } - } - - // Fall back to sending through the message manager - let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - - return messageManager.sendSyncMessage(MSG_INSTALL_ADDONS, installs)[0]; - }, - - _addCallback: function(callback, urls) { - if (!callback || typeof callback != "function") - return -1; - - let callbackID = this._windowID + "-" + ++this._lastCallbackID; - let callbackObject = new CallbackObject(callbackID, callback, urls, this); - this._callbacks.set(callbackID, callbackObject); - return callbackID; - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference]) -}; - - -function InstallTrigger() { -} - -InstallTrigger.prototype = { - // Here be magic. We've declared ourselves as providing the - // nsIDOMGlobalPropertyInitializer interface, and are registered in the - // "JavaScript-global-property" category in the XPCOM category manager. This - // means that for newly created windows, XPCOM will createinstance this - // object, and then call init, passing in the window for which we need to - // provide an instance. We then initialize ourselves and return the webidl - // version of this object using the webidl-provided _create method, which - // XPCOM will then duly expose as a property value on the window. All this - // indirection is necessary because webidl does not (yet) support statics - // (bug 863952). See bug 926712 for more details about this implementation. - init: function(window) { - this._window = window; - this._principal = window.document.nodePrincipal; - this._url = window.document.documentURIObject; - - try { - this._mediator = new RemoteMediator(window); - } catch (ex) { - // If we can't set up IPC (e.g., because this is a top-level window - // or something), then don't expose InstallTrigger. - return null; - } - - return window.InstallTriggerImpl._create(window, this); - }, - - enabled: function() { - return this._mediator.enabled(this._url.spec); - }, - - updateEnabled: function() { - return this.enabled(); - }, - - install: function(installs, callback) { - let installData = { - uris: [], - hashes: [], - names: [], - icons: [], - }; - - for (let name of Object.keys(installs)) { - let item = installs[name]; - if (typeof item === "string") { - item = { URL: item }; - } - if (!item.URL) { - throw new this._window.Error("Missing URL property for '" + name + "'"); - } - - let url = this._resolveURL(item.URL); - if (!this._checkLoadURIFromScript(url)) { - throw new this._window.Error("Insufficient permissions to install: " + url.spec); - } - - let iconUrl = null; - if (item.IconURL) { - iconUrl = this._resolveURL(item.IconURL); - if (!this._checkLoadURIFromScript(iconUrl)) { - iconUrl = null; // If page can't load the icon, just ignore it - } - } - - installData.uris.push(url.spec); - installData.hashes.push(item.Hash || null); - installData.names.push(name); - installData.icons.push(iconUrl ? iconUrl.spec : null); - } - - return this._mediator.install(installData, this._principal, callback, this._window); - }, - - startSoftwareUpdate: function(url, flags) { - let filename = Services.io.newURI(url, null, null) - .QueryInterface(Ci.nsIURL) - .filename; - let args = {}; - args[filename] = { "URL": url }; - return this.install(args); - }, - - installChrome: function(type, url, skin) { - return this.startSoftwareUpdate(url); - }, - - _resolveURL: function(url) { - return Services.io.newURI(url, null, this._url); - }, - - _checkLoadURIFromScript: function(uri) { - let secman = Services.scriptSecurityManager; - try { - secman.checkLoadURIWithPrincipal(this._principal, - uri, - secman.DISALLOW_INHERIT_PRINCIPAL); - return true; - } - catch (e) { - return false; - } - }, - - classID: Components.ID("{9df8ef2b-94da-45c9-ab9f-132eb55fddf1}"), - contractID: "@mozilla.org/addons/installtrigger;1", - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer]) -}; - - - -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InstallTrigger]); diff --git a/toolkit/mozapps/webextensions/amWebAPI.js b/toolkit/mozapps/webextensions/amWebAPI.js deleted file mode 100644 index 5ad0d23f1..000000000 --- a/toolkit/mozapps/webextensions/amWebAPI.js +++ /dev/null @@ -1,269 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); - -const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest"; -const MSG_PROMISE_RESULT = "WebAPIPromiseResult"; -const MSG_INSTALL_EVENT = "WebAPIInstallEvent"; -const MSG_INSTALL_CLEANUP = "WebAPICleanup"; -const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest"; -const MSG_ADDON_EVENT = "WebAPIAddonEvent"; - -class APIBroker { - constructor(mm) { - this.mm = mm; - - this._promises = new Map(); - - // _installMap maps integer ids to DOM AddonInstall instances - this._installMap = new Map(); - - this.mm.addMessageListener(MSG_PROMISE_RESULT, this); - this.mm.addMessageListener(MSG_INSTALL_EVENT, this); - - this._eventListener = null; - } - - receiveMessage(message) { - let payload = message.data; - - switch (message.name) { - case MSG_PROMISE_RESULT: { - if (!this._promises.has(payload.callbackID)) { - return; - } - - let resolve = this._promises.get(payload.callbackID); - this._promises.delete(payload.callbackID); - resolve(payload); - break; - } - - case MSG_INSTALL_EVENT: { - let install = this._installMap.get(payload.id); - if (!install) { - let err = new Error(`Got install event for unknown install ${payload.id}`); - Cu.reportError(err); - return; - } - install._dispatch(payload); - break; - } - - case MSG_ADDON_EVENT: { - if (this._eventListener) { - this._eventListener(payload); - } - } - } - } - - sendRequest(type, ...args) { - return new Promise(resolve => { - let callbackID = APIBroker._nextID++; - - this._promises.set(callbackID, resolve); - this.mm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args }); - }); - } - - setAddonListener(callback) { - this._eventListener = callback; - if (callback) { - this.mm.addMessageListener(MSG_ADDON_EVENT, this); - this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: true}); - } else { - this.mm.removeMessageListener(MSG_ADDON_EVENT, this); - this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: false}); - } - } - - sendCleanup(ids) { - this.setAddonListener(null); - this.mm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids }); - } -} - -APIBroker._nextID = 0; - -// Base class for building classes to back content-exposed interfaces. -class APIObject { - init(window, broker, properties) { - this.window = window; - this.broker = broker; - - // Copy any provided properties onto this object, webidl bindings - // will only expose to content what should be exposed. - for (let key of Object.keys(properties)) { - this[key] = properties[key]; - } - } - - /** - * Helper to implement an asychronous method visible to content, where - * the method is implemented by sending a message to the parent process - * and then wrapping the returned object or error in an appropriate object. - * This helper method ensures that: - * - Returned Promise objects are from the content window - * - Rejected Promises have Error objects from the content window - * - Only non-internal errors are exposed to the caller - * - * @param {string} apiRequest The command to invoke in the parent process. - * @param {array<cloneable>} apiArgs The arguments to include with the - * request to the parent process. - * @param {function} resultConvert If provided, a function called with the - * result from the parent process as an - * argument. Used to convert the result - * into something appropriate for content. - * @returns {Promise<any>} A Promise suitable for passing directly to content. - */ - _apiTask(apiRequest, apiArgs, resultConverter) { - let win = this.window; - let broker = this.broker; - return new win.Promise((resolve, reject) => { - Task.spawn(function*() { - let result = yield broker.sendRequest(apiRequest, ...apiArgs); - if ("reject" in result) { - let err = new win.Error(result.reject.message); - // We don't currently put any other properties onto Errors - // generated by mozAddonManager. If/when we do, they will - // need to get copied here. - reject(err); - return; - } - - let obj = result.resolve; - if (resultConverter) { - obj = resultConverter(obj); - } - resolve(obj); - }).catch(err => { - Cu.reportError(err); - reject(new win.Error("Unexpected internal error")); - }); - }); - } -} - -class Addon extends APIObject { - constructor(...args) { - super(); - this.init(...args); - } - - uninstall() { - return this._apiTask("addonUninstall", [this.id]); - } - - setEnabled(value) { - return this._apiTask("addonSetEnabled", [this.id, value]); - } -} - -class AddonInstall extends APIObject { - constructor(window, broker, properties) { - super(); - this.init(window, broker, properties); - - broker._installMap.set(properties.id, this); - } - - _dispatch(data) { - // The message for the event includes updated copies of all install - // properties. Use the usual "let webidl filter visible properties" trick. - for (let key of Object.keys(data)) { - this[key] = data[key]; - } - - let event = new this.window.Event(data.event); - this.__DOM_IMPL__.dispatchEvent(event); - } - - install() { - return this._apiTask("addonInstallDoInstall", [this.id]); - } - - cancel() { - return this._apiTask("addonInstallCancel", [this.id]); - } -} - -class WebAPI extends APIObject { - constructor() { - super(); - this.allInstalls = []; - this.listenerCount = 0; - } - - init(window) { - let mm = window - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - let broker = new APIBroker(mm); - - super.init(window, broker, {}); - - window.addEventListener("unload", event => { - this.broker.sendCleanup(this.allInstalls); - }); - } - - getAddonByID(id) { - return this._apiTask("getAddonByID", [id], addonInfo => { - if (!addonInfo) { - return null; - } - let addon = new Addon(this.window, this.broker, addonInfo); - return this.window.Addon._create(this.window, addon); - }); - } - - createInstall(options) { - return this._apiTask("createInstall", [options], installInfo => { - if (!installInfo) { - return null; - } - let install = new AddonInstall(this.window, this.broker, installInfo); - this.allInstalls.push(installInfo.id); - return this.window.AddonInstall._create(this.window, install); - }); - } - - eventListenerWasAdded(type) { - if (this.listenerCount == 0) { - this.broker.setAddonListener(data => { - let event = new this.window.AddonEvent(data.event, data); - this.__DOM_IMPL__.dispatchEvent(event); - }); - } - this.listenerCount++; - } - - eventListenerWasRemoved(type) { - this.listenerCount--; - if (this.listenerCount == 0) { - this.broker.setAddonListener(null); - } - } - - QueryInterface(iid) { - if (iid.equals(WebAPI.classID) || iid.equals(Ci.nsISupports) - || iid.equals(Ci.nsIDOMGlobalPropertyInitializer)) { - return this; - } - return Cr.NS_ERROR_NO_INTERFACE; - } -} - -WebAPI.prototype.classID = Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"); -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebAPI]); diff --git a/toolkit/mozapps/webextensions/amWebInstallListener.js b/toolkit/mozapps/webextensions/amWebInstallListener.js deleted file mode 100644 index 0bcc345e8..000000000 --- a/toolkit/mozapps/webextensions/amWebInstallListener.js +++ /dev/null @@ -1,348 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * This is a default implementation of amIWebInstallListener that should work - * for most applications but can be overriden. It notifies the observer service - * about blocked installs. For normal installs it pops up an install - * confirmation when all the add-ons have been downloaded. - */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "PromptUtils", "resource://gre/modules/SharedPromptUtils.jsm"); - -const URI_XPINSTALL_DIALOG = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; - -// Installation can begin from any of these states -const READY_STATES = [ - AddonManager.STATE_AVAILABLE, - AddonManager.STATE_DOWNLOAD_FAILED, - AddonManager.STATE_INSTALL_FAILED, - AddonManager.STATE_CANCELLED -]; - -Cu.import("resource://gre/modules/Log.jsm"); -const LOGGER_ID = "addons.weblistener"; - -// Create a new logger for use by the Addons Web Listener -// (Requires AddonManager.jsm) -var logger = Log.repository.getLogger(LOGGER_ID); - -function notifyObservers(aTopic, aBrowser, aUri, aInstalls) { - let info = { - browser: aBrowser, - originatingURI: aUri, - installs: aInstalls, - - QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) - }; - Services.obs.notifyObservers(info, aTopic, null); -} - -/** - * Creates a new installer to monitor downloads and prompt to install when - * ready - * - * @param aBrowser - * The browser that started the installations - * @param aUrl - * The URL that started the installations - * @param aInstalls - * An array of AddonInstalls - */ -function Installer(aBrowser, aUrl, aInstalls) { - this.browser = aBrowser; - this.url = aUrl; - this.downloads = aInstalls; - this.installed = []; - - notifyObservers("addon-install-started", aBrowser, aUrl, aInstalls); - - for (let install of aInstalls) { - install.addListener(this); - - // Start downloading if it hasn't already begun - if (READY_STATES.indexOf(install.state) != -1) - install.install(); - } - - this.checkAllDownloaded(); -} - -Installer.prototype = { - browser: null, - downloads: null, - installed: null, - isDownloading: true, - - /** - * Checks if all downloads are now complete and if so prompts to install. - */ - checkAllDownloaded: function() { - // Prevent re-entrancy caused by the confirmation dialog cancelling unwanted - // installs. - if (!this.isDownloading) - return; - - var failed = []; - var installs = []; - - for (let install of this.downloads) { - switch (install.state) { - case AddonManager.STATE_AVAILABLE: - case AddonManager.STATE_DOWNLOADING: - // Exit early if any add-ons haven't started downloading yet or are - // still downloading - return; - case AddonManager.STATE_DOWNLOAD_FAILED: - failed.push(install); - break; - case AddonManager.STATE_DOWNLOADED: - // App disabled items are not compatible and so fail to install - if (install.addon.appDisabled) - failed.push(install); - else - installs.push(install); - - if (install.linkedInstalls) { - for (let linkedInstall of install.linkedInstalls) { - linkedInstall.addListener(this); - // Corrupt or incompatible items fail to install - if (linkedInstall.state == AddonManager.STATE_DOWNLOAD_FAILED || linkedInstall.addon.appDisabled) - failed.push(linkedInstall); - else - installs.push(linkedInstall); - } - } - break; - case AddonManager.STATE_CANCELLED: - // Just ignore cancelled downloads - break; - default: - logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " + - install.state); - } - } - - this.isDownloading = false; - this.downloads = installs; - - if (failed.length > 0) { - // Stop listening and cancel any installs that are failed because of - // compatibility reasons. - for (let install of failed) { - if (install.state == AddonManager.STATE_DOWNLOADED) { - install.removeListener(this); - install.cancel(); - } - } - notifyObservers("addon-install-failed", this.browser, this.url, failed); - } - - // If none of the downloads were successful then exit early - if (this.downloads.length == 0) - return; - - // Check for a custom installation prompt that may be provided by the - // applicaton - if ("@mozilla.org/addons/web-install-prompt;1" in Cc) { - try { - let prompt = Cc["@mozilla.org/addons/web-install-prompt;1"]. - getService(Ci.amIWebInstallPrompt); - prompt.confirm(this.browser, this.url, this.downloads, this.downloads.length); - return; - } - catch (e) {} - } - - if (Preferences.get("xpinstall.customConfirmationUI", false)) { - notifyObservers("addon-install-confirmation", this.browser, this.url, this.downloads); - return; - } - - let args = {}; - args.url = this.url; - args.installs = this.downloads; - args.wrappedJSObject = args; - - try { - Cc["@mozilla.org/base/telemetry;1"]. - getService(Ci.nsITelemetry). - getHistogramById("SECURITY_UI"). - add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL); - let parentWindow = null; - if (this.browser) { - parentWindow = this.browser.ownerDocument.defaultView; - PromptUtils.fireDialogEvent(parentWindow, "DOMWillOpenModalDialog", this.browser); - } - Services.ww.openWindow(parentWindow, URI_XPINSTALL_DIALOG, - null, "chrome,modal,centerscreen", args); - } catch (e) { - logger.warn("Exception showing install confirmation dialog", e); - for (let install of this.downloads) { - install.removeListener(this); - // Cancel the installs, as currently there is no way to make them fail - // from here. - install.cancel(); - } - notifyObservers("addon-install-cancelled", this.browser, this.url, - this.downloads); - } - }, - - /** - * Checks if all installs are now complete and if so notifies observers. - */ - checkAllInstalled: function() { - var failed = []; - - for (let install of this.downloads) { - switch (install.state) { - case AddonManager.STATE_DOWNLOADED: - case AddonManager.STATE_INSTALLING: - // Exit early if any add-ons haven't started installing yet or are - // still installing - return; - case AddonManager.STATE_INSTALL_FAILED: - failed.push(install); - break; - } - } - - this.downloads = null; - - if (failed.length > 0) - notifyObservers("addon-install-failed", this.browser, this.url, failed); - - if (this.installed.length > 0) - notifyObservers("addon-install-complete", this.browser, this.url, this.installed); - this.installed = null; - }, - - onDownloadCancelled: function(aInstall) { - aInstall.removeListener(this); - this.checkAllDownloaded(); - }, - - onDownloadFailed: function(aInstall) { - aInstall.removeListener(this); - this.checkAllDownloaded(); - }, - - onDownloadEnded: function(aInstall) { - this.checkAllDownloaded(); - return false; - }, - - onInstallCancelled: function(aInstall) { - aInstall.removeListener(this); - this.checkAllInstalled(); - }, - - onInstallFailed: function(aInstall) { - aInstall.removeListener(this); - this.checkAllInstalled(); - }, - - onInstallEnded: function(aInstall) { - aInstall.removeListener(this); - this.installed.push(aInstall); - - // If installing a theme that is disabled and can be enabled then enable it - if (aInstall.addon.type == "theme" && - aInstall.addon.userDisabled == true && - aInstall.addon.appDisabled == false) { - aInstall.addon.userDisabled = false; - } - - this.checkAllInstalled(); - } -}; - -function extWebInstallListener() { -} - -extWebInstallListener.prototype = { - /** - * @see amIWebInstallListener.idl - */ - onWebInstallDisabled: function(aBrowser, aUri, aInstalls) { - let info = { - browser: aBrowser, - originatingURI: aUri, - installs: aInstalls, - - QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) - }; - Services.obs.notifyObservers(info, "addon-install-disabled", null); - }, - - /** - * @see amIWebInstallListener.idl - */ - onWebInstallOriginBlocked: function(aBrowser, aUri, aInstalls) { - let info = { - browser: aBrowser, - originatingURI: aUri, - installs: aInstalls, - - install: function() { - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) - }; - Services.obs.notifyObservers(info, "addon-install-origin-blocked", null); - - return false; - }, - - /** - * @see amIWebInstallListener.idl - */ - onWebInstallBlocked: function(aBrowser, aUri, aInstalls) { - let info = { - browser: aBrowser, - originatingURI: aUri, - installs: aInstalls, - - install: function() { - new Installer(this.browser, this.originatingURI, this.installs); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) - }; - Services.obs.notifyObservers(info, "addon-install-blocked", null); - - return false; - }, - - /** - * @see amIWebInstallListener.idl - */ - onWebInstallRequested: function(aBrowser, aUri, aInstalls) { - new Installer(aBrowser, aUri, aInstalls); - - // We start the installs ourself - return false; - }, - - classDescription: "XPI Install Handler", - contractID: "@mozilla.org/addons/web-install-listener;1", - classID: Components.ID("{0f38e086-89a3-40a5-8ffc-9b694de1d04a}"), - QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallListener, - Ci.amIWebInstallListener2]) -}; - -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([extWebInstallListener]); diff --git a/toolkit/mozapps/webextensions/content/about.js b/toolkit/mozapps/webextensions/content/about.js deleted file mode 100644 index 4f8fb353e..000000000 --- a/toolkit/mozapps/webextensions/content/about.js +++ /dev/null @@ -1,103 +0,0 @@ -// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- - -/* 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-globals-from ../../../content/contentAreaUtils.js */ - -var Cu = Components.utils; -Cu.import("resource://gre/modules/AddonManager.jsm"); - -function init() { - var addon = window.arguments[0]; - var extensionsStrings = document.getElementById("extensionsStrings"); - - document.documentElement.setAttribute("addontype", addon.type); - - var iconURL = AddonManager.getPreferredIconURL(addon, 48, window); - if (iconURL) { - var extensionIcon = document.getElementById("extensionIcon"); - extensionIcon.src = iconURL; - } - - document.title = extensionsStrings.getFormattedString("aboutWindowTitle", [addon.name]); - var extensionName = document.getElementById("extensionName"); - extensionName.textContent = addon.name; - - var extensionVersion = document.getElementById("extensionVersion"); - if (addon.version) - extensionVersion.setAttribute("value", extensionsStrings.getFormattedString("aboutWindowVersionString", [addon.version])); - else - extensionVersion.hidden = true; - - var extensionDescription = document.getElementById("extensionDescription"); - if (addon.description) - extensionDescription.textContent = addon.description; - else - extensionDescription.hidden = true; - - var numDetails = 0; - - var extensionCreator = document.getElementById("extensionCreator"); - if (addon.creator) { - extensionCreator.setAttribute("value", addon.creator); - numDetails++; - } else { - extensionCreator.hidden = true; - var extensionCreatorLabel = document.getElementById("extensionCreatorLabel"); - extensionCreatorLabel.hidden = true; - } - - var extensionHomepage = document.getElementById("extensionHomepage"); - var homepageURL = addon.homepageURL; - if (homepageURL) { - extensionHomepage.setAttribute("homepageURL", homepageURL); - extensionHomepage.setAttribute("tooltiptext", homepageURL); - numDetails++; - } else { - extensionHomepage.hidden = true; - } - - numDetails += appendToList("extensionDevelopers", "developersBox", addon.developers); - numDetails += appendToList("extensionTranslators", "translatorsBox", addon.translators); - numDetails += appendToList("extensionContributors", "contributorsBox", addon.contributors); - - if (numDetails == 0) { - var groove = document.getElementById("groove"); - groove.hidden = true; - var extensionDetailsBox = document.getElementById("extensionDetailsBox"); - extensionDetailsBox.hidden = true; - } - - var acceptButton = document.documentElement.getButton("accept"); - acceptButton.label = extensionsStrings.getString("aboutWindowCloseButton"); - - setTimeout(sizeToContent, 0); -} - -function appendToList(aHeaderId, aNodeId, aItems) { - var header = document.getElementById(aHeaderId); - var node = document.getElementById(aNodeId); - - if (!aItems || aItems.length == 0) { - header.hidden = true; - return 0; - } - - for (let currentItem of aItems) { - var label = document.createElement("label"); - label.textContent = currentItem; - label.setAttribute("class", "contributor"); - node.appendChild(label); - } - - return aItems.length; -} - -function loadHomepage(aEvent) { - window.close(); - openURL(aEvent.target.getAttribute("homepageURL")); -} diff --git a/toolkit/mozapps/webextensions/content/eula.js b/toolkit/mozapps/webextensions/content/eula.js deleted file mode 100644 index 537ee7284..000000000 --- a/toolkit/mozapps/webextensions/content/eula.js +++ /dev/null @@ -1,25 +0,0 @@ -// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- - -/* 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"; - -var Cu = Components.utils; -Cu.import("resource://gre/modules/AddonManager.jsm"); - -function Startup() { - var bundle = document.getElementById("extensionsStrings"); - var addon = window.arguments[0].addon; - - document.documentElement.setAttribute("addontype", addon.type); - - var iconURL = AddonManager.getPreferredIconURL(addon, 48, window); - if (iconURL) - document.getElementById("icon").src = iconURL; - - var label = document.createTextNode(bundle.getFormattedString("eulaHeader", [addon.name])); - document.getElementById("heading").appendChild(label); - document.getElementById("eula").value = addon.eula; -} diff --git a/toolkit/mozapps/webextensions/content/extensions.css b/toolkit/mozapps/webextensions/content/extensions.css deleted file mode 100644 index cb5313365..000000000 --- a/toolkit/mozapps/webextensions/content/extensions.css +++ /dev/null @@ -1,270 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -@namespace xhtml "http://www.w3.org/1999/xhtml"; - -/* HTML link elements do weird things to the layout if they are not hidden */ -xhtml|link { - display: none; -} - -#categories { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#categories-list"); -} - -.category { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#category"); -} - -.sort-controls { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#sorters"); -} - -.addon[status="installed"] { - -moz-box-orient: vertical; - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-generic"); -} - -.addon[status="installing"] { - -moz-box-orient: vertical; - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-installing"); -} - -.addon[pending="uninstall"] { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-uninstalled"); -} - -.creator { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#creator-link"); -} - -.meta-rating { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#rating"); -} - -.download-progress, .download-progress[mode="undetermined"] { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#download-progress"); -} - -.install-status { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#install-status"); -} - -.detail-row { - -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#detail-row"); -} - -.text-list { - white-space: pre-line; - -moz-user-select: element; -} - -setting, row[unsupported="true"] { - display: none; -} - -setting[type="bool"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-bool"); -} - -setting[type="bool"][localized="true"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-localized-bool"); -} - -setting[type="bool"]:not([learnmore]) .preferences-learnmore, -setting[type="boolint"]:not([learnmore]) .preferences-learnmore { - visibility: collapse; -} - -setting[type="boolint"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-boolint"); -} - -setting[type="integer"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-integer"); -} - -setting[type="integer"]:not([size]) textbox { - -moz-box-flex: 1; -} - -setting[type="control"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-control"); -} - -setting[type="string"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-string"); -} - -setting[type="color"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-color"); -} - -setting[type="file"], -setting[type="directory"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-path"); -} - -setting[type="radio"], -setting[type="menulist"] { - display: -moz-grid-line; - -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-multi"); -} - -#addonitem-popup > menuitem[disabled="true"] { - display: none; -} - -#addonitem-popup[addontype="theme"] > #menuitem_enableItem, -#addonitem-popup[addontype="theme"] > #menuitem_disableItem, -#addonitem-popup:not([addontype="theme"]) > #menuitem_enableTheme, -#addonitem-popup:not([addontype="theme"]) > #menuitem_disableTheme { - display: none; -} - -#show-disabled-unsigned-extensions .button-text { - margin-inline-start: 3px !important; - margin-inline-end: 2px !important; -} - -#header-searching:not([active]) { - visibility: hidden; -} - -#search-list[local="false"] > .addon[remote="false"], -#search-list[remote="false"] > .addon[remote="true"] { - visibility: collapse; -} - -#detail-view { - overflow: auto; -} - -.addon:not([notification="warning"]) .warning, -.addon:not([notification="error"]) .error, -.addon:not([notification="info"]) .info, -.addon:not([pending]) .pending, -.addon:not([upgrade="true"]) .update-postfix, -.addon[active="true"] .disabled-postfix, -.addon[pending="install"] .update-postfix, -.addon[pending="install"] .disabled-postfix, -#detail-view:not([notification="warning"]) .warning, -#detail-view:not([notification="error"]) .error, -#detail-view:not([notification="info"]) .info, -#detail-view:not([pending]) .pending, -#detail-view:not([upgrade="true"]) .update-postfix, -#detail-view[active="true"] .disabled-postfix, -#detail-view[loading] .detail-view-container, -#detail-view:not([loading]) .alert-container, -.detail-row:not([value]), -#search-list[remote="false"] #search-allresults-link { - display: none; -} - -#addons-page:not([warning]) #list-view > .global-warning-container { - display: none; -} -#addon-list .date-updated { - display: none; -} - -.view-pane:not(#updates-view) .addon .relnotes-toggle, -.view-pane:not(#updates-view) .addon .include-update, -#updates-view:not([updatetype="available"]) .addon .include-update, -#updates-view[updatetype="available"] .addon .update-available-notice { - display: none; -} - -#addons-page:not([warning]) .global-warning, -#addons-page:not([warning="safemode"]) .global-warning-safemode, -#addons-page:not([warning="checkcompatibility"]) .global-warning-checkcompatibility, -#addons-page:not([warning="updatesecurity"]) .global-warning-updatesecurity { - display: none; -} - -/* Plugins aren't yet disabled by safemode (bug 342333), - so don't show that warning when viewing plugins. */ -#addons-page[warning="safemode"] .view-pane[type="plugin"] .global-warning-container, -#addons-page[warning="safemode"] #detail-view[loading="true"] .global-warning { - display: none; -} - -#addons-page .view-pane:not([type="plugin"]) #plugindeprecation-notice { - display: none; -} - -#addons-page .view-pane:not([type="experiment"]) .experiment-info-container { - display: none; -} - -.addon .relnotes { - -moz-user-select: text; -} -#detail-name, #detail-desc, #detail-fulldesc { - -moz-user-select: text; -} - -/* Make sure we're not animating hidden images. See bug 623739. */ -#view-port:not([selectedIndex="0"]) #discover-view .loading, -#discover-view:not([selectedIndex="0"]) .loading { - display: none; -} - -/* Elements in unselected richlistitems cannot be focused */ -richlistitem:not([selected]) * { - -moz-user-focus: ignore; -} - -#header-search { - width: 22em; -} - -#header-utils-btn { - -moz-user-focus: normal; -} - -.discover-button[disabled="true"] { - display: none; -} - -#experiments-learn-more[disabled="true"] { - display: none; -} - -#experiments-change-telemetry[disabled="true"] { - display: none; -} - -.view-pane[type="experiment"] .error, -.view-pane[type="experiment"] .warning, -.view-pane[type="experiment"] .addon:not([pending="uninstall"]) .pending, -.view-pane[type="experiment"] .disabled-postfix, -.view-pane[type="experiment"] .update-postfix, -.view-pane[type="experiment"] .addon-control.enable, -.view-pane[type="experiment"] .addon-control.disable, -#detail-view[type="experiment"] .alert-container, -#detail-view[type="experiment"] #detail-version, -#detail-view[type="experiment"] #detail-creator, -#detail-view[type="experiment"] #detail-enable-btn, -#detail-view[type="experiment"] #detail-disable-btn { - display: none; -} - -.view-pane:not([type="experiment"]) .experiment-container, -.view-pane:not([type="experiment"]) #detail-experiment-container { - display: none; -} - -.addon[type="experiment"][status="installing"] .experiment-time, -.addon[type="experiment"][status="installing"] .experiment-state { - display: none; -} diff --git a/toolkit/mozapps/webextensions/content/extensions.js b/toolkit/mozapps/webextensions/content/extensions.js deleted file mode 100644 index 3159eb1e1..000000000 --- a/toolkit/mozapps/webextensions/content/extensions.js +++ /dev/null @@ -1,3827 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -/* import-globals-from ../../../content/contentAreaUtils.js */ -/* globals XMLStylesheetProcessingInstruction*/ - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; -var Cr = Components.results; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/DownloadUtils.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); -Cu.import("resource://gre/modules/addons/AddonRepository.jsm"); - -const CONSTANTS = {}; -Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS); -const SIGNING_REQUIRED = CONSTANTS.REQUIRE_SIGNING ? - true : - Services.prefs.getBoolPref("xpinstall.signatures.required"); - -XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", - "resource://gre/modules/PluralForm.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Preferences", - "resource://gre/modules/Preferences.jsm"); - -const PREF_DISCOVERURL = "extensions.webservice.discoverURL"; -const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane"; -const PREF_XPI_ENABLED = "xpinstall.enabled"; -const PREF_MAXRESULTS = "extensions.getAddons.maxResults"; -const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; -const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled"; -const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden"; -const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory"; - -const LOADING_MSG_DELAY = 100; - -const SEARCH_SCORE_MULTIPLIER_NAME = 2; -const SEARCH_SCORE_MULTIPLIER_DESCRIPTION = 2; - -// Use integers so search scores are sortable by nsIXULSortService -const SEARCH_SCORE_MATCH_WHOLEWORD = 10; -const SEARCH_SCORE_MATCH_WORDBOUNDRY = 6; -const SEARCH_SCORE_MATCH_SUBSTRING = 3; - -const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds) -const UPDATES_RELEASENOTES_TRANSFORMFILE = "chrome://mozapps/content/extensions/updateinfo.xsl"; - -const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml" - -var gViewDefault = "addons://discover/"; - -var gStrings = {}; -XPCOMUtils.defineLazyServiceGetter(gStrings, "bundleSvc", - "@mozilla.org/intl/stringbundle;1", - "nsIStringBundleService"); - -XPCOMUtils.defineLazyGetter(gStrings, "brand", function() { - return this.bundleSvc.createBundle("chrome://branding/locale/brand.properties"); -}); -XPCOMUtils.defineLazyGetter(gStrings, "ext", function() { - return this.bundleSvc.createBundle("chrome://mozapps/locale/extensions/extensions.properties"); -}); -XPCOMUtils.defineLazyGetter(gStrings, "dl", function() { - return this.bundleSvc.createBundle("chrome://mozapps/locale/downloads/downloads.properties"); -}); - -XPCOMUtils.defineLazyGetter(gStrings, "brandShortName", function() { - return this.brand.GetStringFromName("brandShortName"); -}); -XPCOMUtils.defineLazyGetter(gStrings, "appVersion", function() { - return Services.appinfo.version; -}); - -document.addEventListener("load", initialize, true); -window.addEventListener("unload", shutdown, false); - -class MessageDispatcher { - constructor(target) { - this.listeners = new Map(); - this.target = target; - } - - addMessageListener(name, handler) { - if (!this.listeners.has(name)) { - this.listeners.set(name, new Set()); - } - - this.listeners.get(name).add(handler); - } - - removeMessageListener(name, handler) { - if (this.listeners.has(name)) { - this.listeners.get(name).delete(handler); - } - } - - sendAsyncMessage(name, data) { - for (let handler of this.listeners.get(name) || new Set()) { - Promise.resolve().then(() => { - handler.receiveMessage({ - name, - data, - target: this.target, - }); - }); - } - } -} - -/** - * A mock FrameMessageManager global to allow frame scripts to run in - * non-top-level, non-remote <browser>s as if they were top-level or - * remote. - * - * @param {Element} browser - * A XUL <browser> element. - */ -class FakeFrameMessageManager { - constructor(browser) { - let dispatcher = new MessageDispatcher(browser); - let frameDispatcher = new MessageDispatcher(null); - - this.sendAsyncMessage = frameDispatcher.sendAsyncMessage.bind(frameDispatcher); - this.addMessageListener = dispatcher.addMessageListener.bind(dispatcher); - this.removeMessageListener = dispatcher.removeMessageListener.bind(dispatcher); - - this.frame = { - get content() { - return browser.contentWindow; - }, - - get docShell() { - return browser.docShell; - }, - - addEventListener: browser.addEventListener.bind(browser), - removeEventListener: browser.removeEventListener.bind(browser), - - sendAsyncMessage: dispatcher.sendAsyncMessage.bind(dispatcher), - addMessageListener: frameDispatcher.addMessageListener.bind(frameDispatcher), - removeMessageListener: frameDispatcher.removeMessageListener.bind(frameDispatcher), - } - } - - loadFrameScript(url) { - Services.scriptloader.loadSubScript(url, Object.create(this.frame)); - } -} - -var gPendingInitializations = 1; -Object.defineProperty(this, "gIsInitializing", { - get: () => gPendingInitializations > 0 -}); - -function initialize(event) { - // XXXbz this listener gets _all_ load events for all nodes in the - // document... but relies on not being called "too early". - if (event.target instanceof XMLStylesheetProcessingInstruction) { - return; - } - document.removeEventListener("load", initialize, true); - - let globalCommandSet = document.getElementById("globalCommandSet"); - globalCommandSet.addEventListener("command", function(event) { - gViewController.doCommand(event.target.id); - }); - - let viewCommandSet = document.getElementById("viewCommandSet"); - viewCommandSet.addEventListener("commandupdate", function(event) { - gViewController.updateCommands(); - }); - viewCommandSet.addEventListener("command", function(event) { - gViewController.doCommand(event.target.id); - }); - - let detailScreenshot = document.getElementById("detail-screenshot"); - detailScreenshot.addEventListener("load", function(event) { - this.removeAttribute("loading"); - }); - detailScreenshot.addEventListener("error", function(event) { - this.setAttribute("loading", "error"); - }); - - let addonPage = document.getElementById("addons-page"); - addonPage.addEventListener("dragenter", function(event) { - gDragDrop.onDragOver(event); - }); - addonPage.addEventListener("dragover", function(event) { - gDragDrop.onDragOver(event); - }); - addonPage.addEventListener("drop", function(event) { - gDragDrop.onDrop(event); - }); - addonPage.addEventListener("keypress", function(event) { - gHeader.onKeyPress(event); - }); - - if (!isDiscoverEnabled()) { - gViewDefault = "addons://list/extension"; - } - - gViewController.initialize(); - gCategories.initialize(); - gHeader.initialize(); - gEventManager.initialize(); - Services.obs.addObserver(sendEMPong, "EM-ping", false); - Services.obs.notifyObservers(window, "EM-loaded", ""); - - // If the initial view has already been selected (by a call to loadView from - // the above notifications) then bail out now - if (gViewController.initialViewSelected) - return; - - // If there is a history state to restore then use that - if (window.history.state) { - gViewController.updateState(window.history.state); - return; - } - - // Default to the last selected category - var view = gCategories.node.value; - - // Allow passing in a view through the window arguments - if ("arguments" in window && window.arguments.length > 0 && - window.arguments[0] !== null && "view" in window.arguments[0]) { - view = window.arguments[0].view; - } - - gViewController.loadInitialView(view); -} - -function notifyInitialized() { - if (!gIsInitializing) - return; - - gPendingInitializations--; - if (!gIsInitializing) { - var event = document.createEvent("Events"); - event.initEvent("Initialized", true, true); - document.dispatchEvent(event); - } -} - -function shutdown() { - gCategories.shutdown(); - gSearchView.shutdown(); - gEventManager.shutdown(); - gViewController.shutdown(); - Services.obs.removeObserver(sendEMPong, "EM-ping"); -} - -function sendEMPong(aSubject, aTopic, aData) { - Services.obs.notifyObservers(window, "EM-pong", ""); -} - -// Used by external callers to load a specific view into the manager -function loadView(aViewId) { - if (!gViewController.initialViewSelected) { - // The caller opened the window and immediately loaded the view so it - // should be the initial history entry - - gViewController.loadInitialView(aViewId); - } else { - gViewController.loadView(aViewId); - } -} - -function isCorrectlySigned(aAddon) { - // Add-ons without an "isCorrectlySigned" property are correctly signed as - // they aren't the correct type for signing. - return aAddon.isCorrectlySigned !== false; -} - -function isDiscoverEnabled() { - if (Services.prefs.getPrefType(PREF_DISCOVERURL) == Services.prefs.PREF_INVALID) - return false; - - try { - if (!Services.prefs.getBoolPref(PREF_DISCOVER_ENABLED)) - return false; - } catch (e) {} - - try { - if (!Services.prefs.getBoolPref(PREF_XPI_ENABLED)) - return false; - } catch (e) {} - - return true; -} - -/** - * Obtain the main DOMWindow for the current context. - */ -function getMainWindow() { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem) - .rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); -} - -function getBrowserElement() { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .chromeEventHandler; -} - -/** - * Obtain the DOMWindow that can open a preferences pane. - * - * This is essentially "get the browser chrome window" with the added check - * that the supposed browser chrome window is capable of opening a preferences - * pane. - * - * This may return null if we can't find the browser chrome window. - */ -function getMainWindowWithPreferencesPane() { - let mainWindow = getMainWindow(); - if (mainWindow && "openAdvancedPreferences" in mainWindow) { - return mainWindow; - } - return null; -} - -/** - * A wrapper around the HTML5 session history service that allows the browser - * back/forward controls to work within the manager - */ -var HTML5History = { - get index() { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .sessionHistory.index; - }, - - get canGoBack() { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .canGoBack; - }, - - get canGoForward() { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .canGoForward; - }, - - back: function() { - window.history.back(); - gViewController.updateCommand("cmd_back"); - gViewController.updateCommand("cmd_forward"); - }, - - forward: function() { - window.history.forward(); - gViewController.updateCommand("cmd_back"); - gViewController.updateCommand("cmd_forward"); - }, - - pushState: function(aState) { - window.history.pushState(aState, document.title); - }, - - replaceState: function(aState) { - window.history.replaceState(aState, document.title); - }, - - popState: function() { - function onStatePopped(aEvent) { - window.removeEventListener("popstate", onStatePopped, true); - // TODO To ensure we can't go forward again we put an additional entry - // for the current state into the history. Ideally we would just strip - // the history but there doesn't seem to be a way to do that. Bug 590661 - window.history.pushState(aEvent.state, document.title); - } - window.addEventListener("popstate", onStatePopped, true); - window.history.back(); - gViewController.updateCommand("cmd_back"); - gViewController.updateCommand("cmd_forward"); - } -}; - -/** - * A wrapper around a fake history service - */ -var FakeHistory = { - pos: 0, - states: [null], - - get index() { - return this.pos; - }, - - get canGoBack() { - return this.pos > 0; - }, - - get canGoForward() { - return (this.pos + 1) < this.states.length; - }, - - back: function() { - if (this.pos == 0) - throw Components.Exception("Cannot go back from this point"); - - this.pos--; - gViewController.updateState(this.states[this.pos]); - gViewController.updateCommand("cmd_back"); - gViewController.updateCommand("cmd_forward"); - }, - - forward: function() { - if ((this.pos + 1) >= this.states.length) - throw Components.Exception("Cannot go forward from this point"); - - this.pos++; - gViewController.updateState(this.states[this.pos]); - gViewController.updateCommand("cmd_back"); - gViewController.updateCommand("cmd_forward"); - }, - - pushState: function(aState) { - this.pos++; - this.states.splice(this.pos, this.states.length); - this.states.push(aState); - }, - - replaceState: function(aState) { - this.states[this.pos] = aState; - }, - - popState: function() { - if (this.pos == 0) - throw Components.Exception("Cannot popState from this view"); - - this.states.splice(this.pos, this.states.length); - this.pos--; - - gViewController.updateState(this.states[this.pos]); - gViewController.updateCommand("cmd_back"); - gViewController.updateCommand("cmd_forward"); - } -}; - -// If the window has a session history then use the HTML5 History wrapper -// otherwise use our fake history implementation -if (window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .sessionHistory) { - var gHistory = HTML5History; -} -else { - gHistory = FakeHistory; -} - -var gEventManager = { - _listeners: {}, - _installListeners: [], - - initialize: function() { - const ADDON_EVENTS = ["onEnabling", "onEnabled", "onDisabling", - "onDisabled", "onUninstalling", "onUninstalled", - "onInstalled", "onOperationCancelled", - "onUpdateAvailable", "onUpdateFinished", - "onCompatibilityUpdateAvailable", - "onPropertyChanged"]; - for (let evt of ADDON_EVENTS) { - let event = evt; - this[event] = (...aArgs) => this.delegateAddonEvent(event, aArgs); - } - - const INSTALL_EVENTS = ["onNewInstall", "onDownloadStarted", - "onDownloadEnded", "onDownloadFailed", - "onDownloadProgress", "onDownloadCancelled", - "onInstallStarted", "onInstallEnded", - "onInstallFailed", "onInstallCancelled", - "onExternalInstall"]; - for (let evt of INSTALL_EVENTS) { - let event = evt; - this[event] = (...aArgs) => this.delegateInstallEvent(event, aArgs); - } - - AddonManager.addManagerListener(this); - AddonManager.addInstallListener(this); - AddonManager.addAddonListener(this); - - this.refreshGlobalWarning(); - this.refreshAutoUpdateDefault(); - - var contextMenu = document.getElementById("addonitem-popup"); - contextMenu.addEventListener("popupshowing", function() { - var addon = gViewController.currentViewObj.getSelectedAddon(); - contextMenu.setAttribute("addontype", addon.type); - - var menuSep = document.getElementById("addonitem-menuseparator"); - var countMenuItemsBeforeSep = 0; - for (let child of contextMenu.children) { - if (child == menuSep) { - break; - } - if (child.nodeName == "menuitem" && - gViewController.isCommandEnabled(child.command)) { - countMenuItemsBeforeSep++; - } - } - - // Hide the separator if there are no visible menu items before it - menuSep.hidden = (countMenuItemsBeforeSep == 0); - - }, false); - - let addonTooltip = document.getElementById("addonitem-tooltip"); - addonTooltip.addEventListener("popupshowing", function() { - let addonItem = addonTooltip.triggerNode; - // The way the test triggers the tooltip the richlistitem is the - // tooltipNode but in normal use it is the anonymous node. This allows - // any case - if (addonItem.localName != "richlistitem") - addonItem = document.getBindingParent(addonItem); - - let tiptext = addonItem.getAttribute("name"); - - if (addonItem.mAddon) { - if (shouldShowVersionNumber(addonItem.mAddon)) { - tiptext += " " + (addonItem.hasAttribute("upgrade") ? addonItem.mManualUpdate.version - : addonItem.mAddon.version); - } - } - else if (shouldShowVersionNumber(addonItem.mInstall)) { - tiptext += " " + addonItem.mInstall.version; - } - - addonTooltip.label = tiptext; - }, false); - }, - - shutdown: function() { - AddonManager.removeManagerListener(this); - AddonManager.removeInstallListener(this); - AddonManager.removeAddonListener(this); - }, - - registerAddonListener: function(aListener, aAddonId) { - if (!(aAddonId in this._listeners)) - this._listeners[aAddonId] = []; - else if (this._listeners[aAddonId].indexOf(aListener) != -1) - return; - this._listeners[aAddonId].push(aListener); - }, - - unregisterAddonListener: function(aListener, aAddonId) { - if (!(aAddonId in this._listeners)) - return; - var index = this._listeners[aAddonId].indexOf(aListener); - if (index == -1) - return; - this._listeners[aAddonId].splice(index, 1); - }, - - registerInstallListener: function(aListener) { - if (this._installListeners.indexOf(aListener) != -1) - return; - this._installListeners.push(aListener); - }, - - unregisterInstallListener: function(aListener) { - var i = this._installListeners.indexOf(aListener); - if (i == -1) - return; - this._installListeners.splice(i, 1); - }, - - delegateAddonEvent: function(aEvent, aParams) { - var addon = aParams.shift(); - if (!(addon.id in this._listeners)) - return; - - var listeners = this._listeners[addon.id]; - for (let listener of listeners) { - if (!(aEvent in listener)) - continue; - try { - listener[aEvent].apply(listener, aParams); - } catch (e) { - // this shouldn't be fatal - Cu.reportError(e); - } - } - }, - - delegateInstallEvent: function(aEvent, aParams) { - var existingAddon = aEvent == "onExternalInstall" ? aParams[1] : aParams[0].existingAddon; - // If the install is an update then send the event to all listeners - // registered for the existing add-on - if (existingAddon) - this.delegateAddonEvent(aEvent, [existingAddon].concat(aParams)); - - for (let listener of this._installListeners) { - if (!(aEvent in listener)) - continue; - try { - listener[aEvent].apply(listener, aParams); - } catch (e) { - // this shouldn't be fatal - Cu.reportError(e); - } - } - }, - - refreshGlobalWarning: function() { - var page = document.getElementById("addons-page"); - - if (Services.appinfo.inSafeMode) { - page.setAttribute("warning", "safemode"); - return; - } - - if (AddonManager.checkUpdateSecurityDefault && - !AddonManager.checkUpdateSecurity) { - page.setAttribute("warning", "updatesecurity"); - return; - } - - if (!AddonManager.checkCompatibility) { - page.setAttribute("warning", "checkcompatibility"); - return; - } - - page.removeAttribute("warning"); - }, - - refreshAutoUpdateDefault: function() { - var updateEnabled = AddonManager.updateEnabled; - var autoUpdateDefault = AddonManager.autoUpdateDefault; - - // The checkbox needs to reflect that both prefs need to be true - // for updates to be checked for and applied automatically - document.getElementById("utils-autoUpdateDefault") - .setAttribute("checked", updateEnabled && autoUpdateDefault); - - document.getElementById("utils-resetAddonUpdatesToAutomatic").hidden = !autoUpdateDefault; - document.getElementById("utils-resetAddonUpdatesToManual").hidden = autoUpdateDefault; - }, - - onCompatibilityModeChanged: function() { - this.refreshGlobalWarning(); - }, - - onCheckUpdateSecurityChanged: function() { - this.refreshGlobalWarning(); - }, - - onUpdateModeChanged: function() { - this.refreshAutoUpdateDefault(); - } -}; - - -var gViewController = { - viewPort: null, - currentViewId: "", - currentViewObj: null, - currentViewRequest: 0, - viewObjects: {}, - viewChangeCallback: null, - initialViewSelected: false, - lastHistoryIndex: -1, - - initialize: function() { - this.viewPort = document.getElementById("view-port"); - this.headeredViews = document.getElementById("headered-views"); - this.headeredViewsDeck = document.getElementById("headered-views-content"); - - this.viewObjects["search"] = gSearchView; - this.viewObjects["discover"] = gDiscoverView; - this.viewObjects["list"] = gListView; - this.viewObjects["detail"] = gDetailView; - this.viewObjects["updates"] = gUpdatesView; - - for (let type in this.viewObjects) { - let view = this.viewObjects[type]; - view.initialize(); - } - - window.controllers.appendController(this); - - window.addEventListener("popstate", function(e) { - gViewController.updateState(e.state); - }, - false); - }, - - shutdown: function() { - if (this.currentViewObj) - this.currentViewObj.hide(); - this.currentViewRequest = 0; - - for (let type in this.viewObjects) { - let view = this.viewObjects[type]; - if ("shutdown" in view) { - try { - view.shutdown(); - } catch (e) { - // this shouldn't be fatal - Cu.reportError(e); - } - } - } - - window.controllers.removeController(this); - }, - - updateState: function(state) { - try { - this.loadViewInternal(state.view, state.previousView, state); - this.lastHistoryIndex = gHistory.index; - } - catch (e) { - // The attempt to load the view failed, try moving further along history - if (this.lastHistoryIndex > gHistory.index) { - if (gHistory.canGoBack) - gHistory.back(); - else - gViewController.replaceView(gViewDefault); - } else if (gHistory.canGoForward) { - gHistory.forward(); - } else { - gViewController.replaceView(gViewDefault); - } - } - }, - - parseViewId: function(aViewId) { - var matchRegex = /^addons:\/\/([^\/]+)\/(.*)$/; - var [, viewType, viewParam] = aViewId.match(matchRegex) || []; - return {type: viewType, param: decodeURIComponent(viewParam)}; - }, - - get isLoading() { - return !this.currentViewObj || this.currentViewObj.node.hasAttribute("loading"); - }, - - loadView: function(aViewId) { - var isRefresh = false; - if (aViewId == this.currentViewId) { - if (this.isLoading) - return; - if (!("refresh" in this.currentViewObj)) - return; - if (!this.currentViewObj.canRefresh()) - return; - isRefresh = true; - } - - var state = { - view: aViewId, - previousView: this.currentViewId - }; - if (!isRefresh) { - gHistory.pushState(state); - this.lastHistoryIndex = gHistory.index; - } - this.loadViewInternal(aViewId, this.currentViewId, state); - }, - - // Replaces the existing view with a new one, rewriting the current history - // entry to match. - replaceView: function(aViewId) { - if (aViewId == this.currentViewId) - return; - - var state = { - view: aViewId, - previousView: null - }; - gHistory.replaceState(state); - this.loadViewInternal(aViewId, null, state); - }, - - loadInitialView: function(aViewId) { - var state = { - view: aViewId, - previousView: null - }; - gHistory.replaceState(state); - - this.loadViewInternal(aViewId, null, state); - this.initialViewSelected = true; - notifyInitialized(); - }, - - get displayedView() { - if (this.viewPort.selectedPanel == this.headeredViews) { - return this.headeredViewsDeck.selectedPanel; - } - return this.viewPort.selectedPanel; - }, - - set displayedView(view) { - let node = view.node; - if (node.parentNode == this.headeredViewsDeck) { - this.headeredViewsDeck.selectedPanel = node; - this.viewPort.selectedPanel = this.headeredViews; - } else { - this.viewPort.selectedPanel = node; - } - }, - - loadViewInternal: function(aViewId, aPreviousView, aState) { - var view = this.parseViewId(aViewId); - - if (!view.type || !(view.type in this.viewObjects)) - throw Components.Exception("Invalid view: " + view.type); - - var viewObj = this.viewObjects[view.type]; - if (!viewObj.node) - throw Components.Exception("Root node doesn't exist for '" + view.type + "' view"); - - if (this.currentViewObj && aViewId != aPreviousView) { - try { - let canHide = this.currentViewObj.hide(); - if (canHide === false) - return; - this.displayedView.removeAttribute("loading"); - } catch (e) { - // this shouldn't be fatal - Cu.reportError(e); - } - } - - gCategories.select(aViewId, aPreviousView); - - this.currentViewId = aViewId; - this.currentViewObj = viewObj; - - this.displayedView = this.currentViewObj; - this.currentViewObj.node.setAttribute("loading", "true"); - this.currentViewObj.node.focus(); - - if (aViewId == aPreviousView) - this.currentViewObj.refresh(view.param, ++this.currentViewRequest, aState); - else - this.currentViewObj.show(view.param, ++this.currentViewRequest, aState); - }, - - // Moves back in the document history and removes the current history entry - popState: function(aCallback) { - this.viewChangeCallback = aCallback; - gHistory.popState(); - }, - - notifyViewChanged: function() { - this.displayedView.removeAttribute("loading"); - - if (this.viewChangeCallback) { - this.viewChangeCallback(); - this.viewChangeCallback = null; - } - - var event = document.createEvent("Events"); - event.initEvent("ViewChanged", true, true); - this.currentViewObj.node.dispatchEvent(event); - }, - - commands: { - cmd_back: { - isEnabled: function() { - return gHistory.canGoBack; - }, - doCommand: function() { - gHistory.back(); - } - }, - - cmd_forward: { - isEnabled: function() { - return gHistory.canGoForward; - }, - doCommand: function() { - gHistory.forward(); - } - }, - - cmd_focusSearch: { - isEnabled: () => true, - doCommand: function() { - gHeader.focusSearchBox(); - } - }, - - cmd_restartApp: { - isEnabled: function() { - return true; - }, - doCommand: function() { - let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]. - createInstance(Ci.nsISupportsPRBool); - Services.obs.notifyObservers(cancelQuit, "quit-application-requested", - "restart"); - if (cancelQuit.data) - return; // somebody canceled our quit request - - let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]. - getService(Ci.nsIAppStartup); - appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); - } - }, - - cmd_enableCheckCompatibility: { - isEnabled: function() { - return true; - }, - doCommand: function() { - AddonManager.checkCompatibility = true; - } - }, - - cmd_enableUpdateSecurity: { - isEnabled: function() { - return true; - }, - doCommand: function() { - AddonManager.checkUpdateSecurity = true; - } - }, - - cmd_toggleAutoUpdateDefault: { - isEnabled: function() { - return true; - }, - doCommand: function() { - if (!AddonManager.updateEnabled || !AddonManager.autoUpdateDefault) { - // One or both of the prefs is false, i.e. the checkbox is not checked. - // Now toggle both to true. If the user wants us to auto-update - // add-ons, we also need to auto-check for updates. - AddonManager.updateEnabled = true; - AddonManager.autoUpdateDefault = true; - } else { - // Both prefs are true, i.e. the checkbox is checked. - // Toggle the auto pref to false, but don't touch the enabled check. - AddonManager.autoUpdateDefault = false; - } - } - }, - - cmd_resetAddonAutoUpdate: { - isEnabled: function() { - return true; - }, - doCommand: function() { - AddonManager.getAllAddons(function(aAddonList) { - for (let addon of aAddonList) { - if ("applyBackgroundUpdates" in addon) - addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; - } - }); - } - }, - - cmd_goToDiscoverPane: { - isEnabled: function() { - return gDiscoverView.enabled; - }, - doCommand: function() { - gViewController.loadView("addons://discover/"); - } - }, - - cmd_goToRecentUpdates: { - isEnabled: function() { - return true; - }, - doCommand: function() { - gViewController.loadView("addons://updates/recent"); - } - }, - - cmd_goToAvailableUpdates: { - isEnabled: function() { - return true; - }, - doCommand: function() { - gViewController.loadView("addons://updates/available"); - } - }, - - cmd_showItemDetails: { - isEnabled: function(aAddon) { - return !!aAddon && (gViewController.currentViewObj != gDetailView); - }, - doCommand: function(aAddon, aScrollToPreferences) { - gViewController.loadView("addons://detail/" + - encodeURIComponent(aAddon.id) + - (aScrollToPreferences ? "/preferences" : "")); - } - }, - - cmd_findAllUpdates: { - inProgress: false, - isEnabled: function() { - return !this.inProgress; - }, - doCommand: function() { - this.inProgress = true; - gViewController.updateCommand("cmd_findAllUpdates"); - document.getElementById("updates-noneFound").hidden = true; - document.getElementById("updates-progress").hidden = false; - document.getElementById("updates-manualUpdatesFound-btn").hidden = true; - - var pendingChecks = 0; - var numUpdated = 0; - var numManualUpdates = 0; - var restartNeeded = false; - - let updateStatus = () => { - if (pendingChecks > 0) - return; - - this.inProgress = false; - gViewController.updateCommand("cmd_findAllUpdates"); - document.getElementById("updates-progress").hidden = true; - gUpdatesView.maybeRefresh(); - - if (numManualUpdates > 0 && numUpdated == 0) { - document.getElementById("updates-manualUpdatesFound-btn").hidden = false; - return; - } - - if (numUpdated == 0) { - document.getElementById("updates-noneFound").hidden = false; - return; - } - - if (restartNeeded) { - document.getElementById("updates-downloaded").hidden = false; - document.getElementById("updates-restart-btn").hidden = false; - } else { - document.getElementById("updates-installed").hidden = false; - } - } - - var updateInstallListener = { - onDownloadFailed: function() { - pendingChecks--; - updateStatus(); - }, - onInstallFailed: function() { - pendingChecks--; - updateStatus(); - }, - onInstallEnded: function(aInstall, aAddon) { - pendingChecks--; - numUpdated++; - if (isPending(aInstall.existingAddon, "upgrade")) - restartNeeded = true; - updateStatus(); - } - }; - - var updateCheckListener = { - onUpdateAvailable: function(aAddon, aInstall) { - gEventManager.delegateAddonEvent("onUpdateAvailable", - [aAddon, aInstall]); - if (AddonManager.shouldAutoUpdate(aAddon)) { - aInstall.addListener(updateInstallListener); - aInstall.install(); - } else { - pendingChecks--; - numManualUpdates++; - updateStatus(); - } - }, - onNoUpdateAvailable: function(aAddon) { - pendingChecks--; - updateStatus(); - }, - onUpdateFinished: function(aAddon, aError) { - gEventManager.delegateAddonEvent("onUpdateFinished", - [aAddon, aError]); - } - }; - - AddonManager.getAddonsByTypes(null, function(aAddonList) { - for (let addon of aAddonList) { - if (addon.permissions & AddonManager.PERM_CAN_UPGRADE) { - pendingChecks++; - addon.findUpdates(updateCheckListener, - AddonManager.UPDATE_WHEN_USER_REQUESTED); - } - } - - if (pendingChecks == 0) - updateStatus(); - }); - } - }, - - cmd_findItemUpdates: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - return hasPermission(aAddon, "upgrade"); - }, - doCommand: function(aAddon) { - var listener = { - onUpdateAvailable: function(aAddon, aInstall) { - gEventManager.delegateAddonEvent("onUpdateAvailable", - [aAddon, aInstall]); - if (AddonManager.shouldAutoUpdate(aAddon)) - aInstall.install(); - }, - onNoUpdateAvailable: function(aAddon) { - gEventManager.delegateAddonEvent("onNoUpdateAvailable", - [aAddon]); - } - }; - gEventManager.delegateAddonEvent("onCheckingUpdate", [aAddon]); - aAddon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED); - } - }, - - cmd_showItemPreferences: { - isEnabled: function(aAddon) { - if (!aAddon || - (!aAddon.isActive && !aAddon.isGMPlugin) || - !aAddon.optionsURL) { - return false; - } - if (gViewController.currentViewObj == gDetailView && - (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE || - aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER)) { - return false; - } - if (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO) - return false; - return true; - }, - doCommand: function(aAddon) { - if (hasInlineOptions(aAddon)) { - gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true); - return; - } - var optionsURL = aAddon.optionsURL; - if (aAddon.optionsType == AddonManager.OPTIONS_TYPE_TAB && - openOptionsInTab(optionsURL)) { - return; - } - var windows = Services.wm.getEnumerator(null); - while (windows.hasMoreElements()) { - var win = windows.getNext(); - if (win.closed) { - continue; - } - if (win.document.documentURI == optionsURL) { - win.focus(); - return; - } - } - var features = "chrome,titlebar,toolbar,centerscreen"; - try { - var instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply"); - features += instantApply ? ",dialog=no" : ",modal"; - } catch (e) { - features += ",modal"; - } - openDialog(optionsURL, "", features); - } - }, - - cmd_showItemAbout: { - isEnabled: function(aAddon) { - // XXXunf This may be applicable to install items too. See bug 561260 - return !!aAddon; - }, - doCommand: function(aAddon) { - var aboutURL = aAddon.aboutURL; - if (aboutURL) - openDialog(aboutURL, "", "chrome,centerscreen,modal", aAddon); - else - openDialog("chrome://mozapps/content/extensions/about.xul", - "", "chrome,centerscreen,modal", aAddon); - } - }, - - cmd_enableItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - let addonType = AddonManager.addonTypes[aAddon.type]; - return (!(addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) && - hasPermission(aAddon, "enable")); - }, - doCommand: function(aAddon) { - aAddon.userDisabled = false; - }, - getTooltip: function(aAddon) { - if (!aAddon) - return ""; - if (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE) - return gStrings.ext.GetStringFromName("enableAddonRestartRequiredTooltip"); - return gStrings.ext.GetStringFromName("enableAddonTooltip"); - } - }, - - cmd_disableItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - let addonType = AddonManager.addonTypes[aAddon.type]; - return (!(addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) && - hasPermission(aAddon, "disable")); - }, - doCommand: function(aAddon) { - aAddon.userDisabled = true; - }, - getTooltip: function(aAddon) { - if (!aAddon) - return ""; - if (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE) - return gStrings.ext.GetStringFromName("disableAddonRestartRequiredTooltip"); - return gStrings.ext.GetStringFromName("disableAddonTooltip"); - } - }, - - cmd_installItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - return aAddon.install && aAddon.install.state == AddonManager.STATE_AVAILABLE; - }, - doCommand: function(aAddon) { - function doInstall() { - gViewController.currentViewObj.getListItemForID(aAddon.id)._installStatus.installRemote(); - } - - if (gViewController.currentViewObj == gDetailView) - gViewController.popState(doInstall); - else - doInstall(); - } - }, - - cmd_purchaseItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - return !!aAddon.purchaseURL; - }, - doCommand: function(aAddon) { - openURL(aAddon.purchaseURL); - } - }, - - cmd_uninstallItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - return hasPermission(aAddon, "uninstall"); - }, - doCommand: function(aAddon) { - if (gViewController.currentViewObj != gDetailView) { - aAddon.uninstall(); - return; - } - - gViewController.popState(function() { - gViewController.currentViewObj.getListItemForID(aAddon.id).uninstall(); - }); - }, - getTooltip: function(aAddon) { - if (!aAddon) - return ""; - if (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL) - return gStrings.ext.GetStringFromName("uninstallAddonRestartRequiredTooltip"); - return gStrings.ext.GetStringFromName("uninstallAddonTooltip"); - } - }, - - cmd_cancelUninstallItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - return isPending(aAddon, "uninstall"); - }, - doCommand: function(aAddon) { - aAddon.cancelUninstall(); - } - }, - - cmd_installFromFile: { - isEnabled: function() { - return true; - }, - doCommand: function() { - const nsIFilePicker = Ci.nsIFilePicker; - var fp = Cc["@mozilla.org/filepicker;1"] - .createInstance(nsIFilePicker); - fp.init(window, - gStrings.ext.GetStringFromName("installFromFile.dialogTitle"), - nsIFilePicker.modeOpenMultiple); - try { - fp.appendFilter(gStrings.ext.GetStringFromName("installFromFile.filterName"), - "*.xpi;*.jar"); - fp.appendFilters(nsIFilePicker.filterAll); - } catch (e) { } - - if (fp.show() != nsIFilePicker.returnOK) - return; - - var files = fp.files; - var installs = []; - - function buildNextInstall() { - if (!files.hasMoreElements()) { - if (installs.length > 0) { - // Display the normal install confirmation for the installs - let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"]. - getService(Ci.amIWebInstallListener); - webInstaller.onWebInstallRequested(getBrowserElement(), - document.documentURIObject, - installs); - } - return; - } - - var file = files.getNext(); - AddonManager.getInstallForFile(file, function(aInstall) { - installs.push(aInstall); - buildNextInstall(); - }); - } - - buildNextInstall(); - } - }, - - cmd_debugAddons: { - isEnabled: function() { - return true; - }, - doCommand: function() { - let mainWindow = getMainWindow(); - if ("switchToTabHavingURI" in mainWindow) { - mainWindow.switchToTabHavingURI("about:debugging#addons", true); - } - }, - }, - - cmd_cancelOperation: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - return aAddon.pendingOperations != AddonManager.PENDING_NONE; - }, - doCommand: function(aAddon) { - if (isPending(aAddon, "install")) { - aAddon.install.cancel(); - } else if (isPending(aAddon, "upgrade")) { - aAddon.pendingUpgrade.install.cancel(); - } else if (isPending(aAddon, "uninstall")) { - aAddon.cancelUninstall(); - } else if (isPending(aAddon, "enable")) { - aAddon.userDisabled = true; - } else if (isPending(aAddon, "disable")) { - aAddon.userDisabled = false; - } - } - }, - - cmd_contribute: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - return ("contributionURL" in aAddon && aAddon.contributionURL); - }, - doCommand: function(aAddon) { - openURL(aAddon.contributionURL); - } - }, - - cmd_askToActivateItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - let addonType = AddonManager.addonTypes[aAddon.type]; - return ((addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) && - hasPermission(aAddon, "ask_to_activate")); - }, - doCommand: function(aAddon) { - aAddon.userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE; - } - }, - - cmd_alwaysActivateItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - let addonType = AddonManager.addonTypes[aAddon.type]; - return ((addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) && - hasPermission(aAddon, "enable")); - }, - doCommand: function(aAddon) { - aAddon.userDisabled = false; - } - }, - - cmd_neverActivateItem: { - isEnabled: function(aAddon) { - if (!aAddon) - return false; - let addonType = AddonManager.addonTypes[aAddon.type]; - return ((addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) && - hasPermission(aAddon, "disable")); - }, - doCommand: function(aAddon) { - aAddon.userDisabled = true; - } - }, - - cmd_showUnsignedExtensions: { - isEnabled: function() { - return true; - }, - doCommand: function() { - gViewController.loadView("addons://list/extension?unsigned=true"); - }, - }, - - cmd_showAllExtensions: { - isEnabled: function() { - return true; - }, - doCommand: function() { - gViewController.loadView("addons://list/extension"); - }, - }, - }, - - supportsCommand: function(aCommand) { - return (aCommand in this.commands); - }, - - isCommandEnabled: function(aCommand) { - if (!this.supportsCommand(aCommand)) - return false; - var addon = this.currentViewObj.getSelectedAddon(); - return this.commands[aCommand].isEnabled(addon); - }, - - updateCommands: function() { - // wait until the view is initialized - if (!this.currentViewObj) - return; - var addon = this.currentViewObj.getSelectedAddon(); - for (let commandId in this.commands) - this.updateCommand(commandId, addon); - }, - - updateCommand: function(aCommandId, aAddon) { - if (typeof aAddon == "undefined") - aAddon = this.currentViewObj.getSelectedAddon(); - var cmd = this.commands[aCommandId]; - var cmdElt = document.getElementById(aCommandId); - cmdElt.setAttribute("disabled", !cmd.isEnabled(aAddon)); - if ("getTooltip" in cmd) { - let tooltip = cmd.getTooltip(aAddon); - if (tooltip) - cmdElt.setAttribute("tooltiptext", tooltip); - else - cmdElt.removeAttribute("tooltiptext"); - } - }, - - doCommand: function(aCommand, aAddon) { - if (!this.supportsCommand(aCommand)) - return; - var cmd = this.commands[aCommand]; - if (!aAddon) - aAddon = this.currentViewObj.getSelectedAddon(); - if (!cmd.isEnabled(aAddon)) - return; - cmd.doCommand(aAddon); - }, - - onEvent: function() {} -}; - -function hasInlineOptions(aAddon) { - return (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE || - aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER || - aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO); -} - -function openOptionsInTab(optionsURL) { - let mainWindow = getMainWindow(); - if ("switchToTabHavingURI" in mainWindow) { - mainWindow.switchToTabHavingURI(optionsURL, true); - return true; - } - return false; -} - -function formatDate(aDate) { - const locale = Cc["@mozilla.org/chrome/chrome-registry;1"] - .getService(Ci.nsIXULChromeRegistry) - .getSelectedLocale("global", true); - const dtOptions = { year: 'numeric', month: 'long', day: 'numeric' }; - return aDate.toLocaleDateString(locale, dtOptions); -} - - -function hasPermission(aAddon, aPerm) { - var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()]; - return !!(aAddon.permissions & perm); -} - - -function isPending(aAddon, aAction) { - var action = AddonManager["PENDING_" + aAction.toUpperCase()]; - return !!(aAddon.pendingOperations & action); -} - -function isInState(aInstall, aState) { - var state = AddonManager["STATE_" + aState.toUpperCase()]; - return aInstall.state == state; -} - -function shouldShowVersionNumber(aAddon) { - if (!aAddon.version) - return false; - - // The version number is hidden for lightweight themes. - if (aAddon.type == "theme") - return !/@personas\.mozilla\.org$/.test(aAddon.id); - - return true; -} - -function createItem(aObj, aIsInstall, aIsRemote) { - let item = document.createElement("richlistitem"); - - item.setAttribute("class", "addon addon-view"); - item.setAttribute("name", aObj.name); - item.setAttribute("type", aObj.type); - item.setAttribute("remote", !!aIsRemote); - - if (aIsInstall) { - item.mInstall = aObj; - - if (aObj.state != AddonManager.STATE_INSTALLED) { - item.setAttribute("status", "installing"); - return item; - } - aObj = aObj.addon; - } - - item.mAddon = aObj; - - item.setAttribute("status", "installed"); - - // set only attributes needed for sorting and XBL binding, - // the binding handles the rest - item.setAttribute("value", aObj.id); - - return item; -} - -function sortElements(aElements, aSortBy, aAscending) { - // aSortBy is an Array of attributes to sort by, in decending - // order of priority. - - const DATE_FIELDS = ["updateDate"]; - const NUMERIC_FIELDS = ["size", "relevancescore", "purchaseAmount"]; - - // We're going to group add-ons into the following buckets: - // - // enabledInstalled - // * Enabled - // * Incompatible but enabled because compatibility checking is off - // * Waiting to be installed - // * Waiting to be enabled - // - // pendingDisable - // * Waiting to be disabled - // - // pendingUninstall - // * Waiting to be removed - // - // disabledIncompatibleBlocked - // * Disabled - // * Incompatible - // * Blocklisted - - const UISTATE_ORDER = ["enabled", "askToActivate", "pendingDisable", - "pendingUninstall", "disabled"]; - - function dateCompare(a, b) { - var aTime = a.getTime(); - var bTime = b.getTime(); - if (aTime < bTime) - return -1; - if (aTime > bTime) - return 1; - return 0; - } - - function numberCompare(a, b) { - return a - b; - } - - function stringCompare(a, b) { - return a.localeCompare(b); - } - - function uiStateCompare(a, b) { - // If we're in descending order, swap a and b, because - // we don't ever want to have descending uiStates - if (!aAscending) - [a, b] = [b, a]; - - return (UISTATE_ORDER.indexOf(a) - UISTATE_ORDER.indexOf(b)); - } - - function getValue(aObj, aKey) { - if (!aObj) - return null; - - if (aObj.hasAttribute(aKey)) - return aObj.getAttribute(aKey); - - var addon = aObj.mAddon || aObj.mInstall; - var addonType = aObj.mAddon && AddonManager.addonTypes[aObj.mAddon.type]; - - if (!addon) - return null; - - if (aKey == "uiState") { - if (addon.pendingOperations == AddonManager.PENDING_DISABLE) - return "pendingDisable"; - if (addon.pendingOperations == AddonManager.PENDING_UNINSTALL) - return "pendingUninstall"; - if (!addon.isActive && - (addon.pendingOperations != AddonManager.PENDING_ENABLE && - addon.pendingOperations != AddonManager.PENDING_INSTALL)) - return "disabled"; - if (addonType && (addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) && - addon.userDisabled == AddonManager.STATE_ASK_TO_ACTIVATE) - return "askToActivate"; - return "enabled"; - } - - return addon[aKey]; - } - - // aSortFuncs will hold the sorting functions that we'll - // use per element, in the correct order. - var aSortFuncs = []; - - for (let i = 0; i < aSortBy.length; i++) { - var sortBy = aSortBy[i]; - - aSortFuncs[i] = stringCompare; - - if (sortBy == "uiState") - aSortFuncs[i] = uiStateCompare; - else if (DATE_FIELDS.indexOf(sortBy) != -1) - aSortFuncs[i] = dateCompare; - else if (NUMERIC_FIELDS.indexOf(sortBy) != -1) - aSortFuncs[i] = numberCompare; - } - - - aElements.sort(function(a, b) { - if (!aAscending) - [a, b] = [b, a]; - - for (let i = 0; i < aSortFuncs.length; i++) { - var sortBy = aSortBy[i]; - var aValue = getValue(a, sortBy); - var bValue = getValue(b, sortBy); - - if (!aValue && !bValue) - return 0; - if (!aValue) - return -1; - if (!bValue) - return 1; - if (aValue != bValue) { - var result = aSortFuncs[i](aValue, bValue); - - if (result != 0) - return result; - } - } - - // If we got here, then all values of a and b - // must have been equal. - return 0; - - }); -} - -function sortList(aList, aSortBy, aAscending) { - var elements = Array.slice(aList.childNodes, 0); - sortElements(elements, [aSortBy], aAscending); - - while (aList.listChild) - aList.removeChild(aList.lastChild); - - for (let element of elements) - aList.appendChild(element); -} - -function getAddonsAndInstalls(aType, aCallback) { - let addons = null, installs = null; - let types = (aType != null) ? [aType] : null; - - AddonManager.getAddonsByTypes(types, function(aAddonsList) { - addons = aAddonsList.filter(a => !a.hidden); - if (installs != null) - aCallback(addons, installs); - }); - - AddonManager.getInstallsByTypes(types, function(aInstallsList) { - // skip over upgrade installs and non-active installs - installs = aInstallsList.filter(function(aInstall) { - return !(aInstall.existingAddon || - aInstall.state == AddonManager.STATE_AVAILABLE); - }); - - if (addons != null) - aCallback(addons, installs) - }); -} - -function doPendingUninstalls(aListBox) { - // Uninstalling add-ons can mutate the list so find the add-ons first then - // uninstall them - var items = []; - var listitem = aListBox.firstChild; - while (listitem) { - if (listitem.getAttribute("pending") == "uninstall" && - !(listitem.opRequiresRestart("UNINSTALL"))) - items.push(listitem.mAddon); - listitem = listitem.nextSibling; - } - - for (let addon of items) - addon.uninstall(); -} - -var gCategories = { - node: null, - _search: null, - - initialize: function() { - this.node = document.getElementById("categories"); - this._search = this.get("addons://search/"); - - var types = AddonManager.addonTypes; - for (var type in types) - this.onTypeAdded(types[type]); - - AddonManager.addTypeListener(this); - - try { - this.node.value = Services.prefs.getCharPref(PREF_UI_LASTCATEGORY); - } catch (e) { } - - // If there was no last view or no existing category matched the last view - // then the list will default to selecting the search category and we never - // want to show that as the first view so switch to the default category - if (!this.node.selectedItem || this.node.selectedItem == this._search) - this.node.value = gViewDefault; - - this.node.addEventListener("select", () => { - this.maybeHideSearch(); - gViewController.loadView(this.node.selectedItem.value); - }, false); - - this.node.addEventListener("click", (aEvent) => { - var selectedItem = this.node.selectedItem; - if (aEvent.target.localName == "richlistitem" && - aEvent.target == selectedItem) { - var viewId = selectedItem.value; - - if (gViewController.parseViewId(viewId).type == "search") { - viewId += encodeURIComponent(gHeader.searchQuery); - } - - gViewController.loadView(viewId); - } - }, false); - }, - - shutdown: function() { - AddonManager.removeTypeListener(this); - }, - - _insertCategory: function(aId, aName, aView, aPriority, aStartHidden) { - // If this category already exists then don't re-add it - if (document.getElementById("category-" + aId)) - return; - - var category = document.createElement("richlistitem"); - category.setAttribute("id", "category-" + aId); - category.setAttribute("value", aView); - category.setAttribute("class", "category"); - category.setAttribute("name", aName); - category.setAttribute("tooltiptext", aName); - category.setAttribute("priority", aPriority); - category.setAttribute("hidden", aStartHidden); - - var node; - for (node of this.node.children) { - var nodePriority = parseInt(node.getAttribute("priority")); - // If the new type's priority is higher than this one then this is the - // insertion point - if (aPriority < nodePriority) - break; - // If the new type's priority is lower than this one then this is isn't - // the insertion point - if (aPriority > nodePriority) - continue; - // If the priorities are equal and the new type's name is earlier - // alphabetically then this is the insertion point - if (String.localeCompare(aName, node.getAttribute("name")) < 0) - break; - } - - this.node.insertBefore(category, node); - }, - - _removeCategory: function(aId) { - var category = document.getElementById("category-" + aId); - if (!category) - return; - - // If this category is currently selected then switch to the default view - if (this.node.selectedItem == category) - gViewController.replaceView(gViewDefault); - - this.node.removeChild(category); - }, - - onTypeAdded: function(aType) { - // Ignore types that we don't have a view object for - if (!(aType.viewType in gViewController.viewObjects)) - return; - - var aViewId = "addons://" + aType.viewType + "/" + aType.id; - - var startHidden = false; - if (aType.flags & AddonManager.TYPE_UI_HIDE_EMPTY) { - var prefName = PREF_UI_TYPE_HIDDEN.replace("%TYPE%", aType.id); - try { - startHidden = Services.prefs.getBoolPref(prefName); - } - catch (e) { - // Default to hidden - startHidden = true; - } - - gPendingInitializations++; - getAddonsAndInstalls(aType.id, (aAddonsList, aInstallsList) => { - var hidden = (aAddonsList.length == 0 && aInstallsList.length == 0); - var item = this.get(aViewId); - - // Don't load view that is becoming hidden - if (hidden && aViewId == gViewController.currentViewId) - gViewController.loadView(gViewDefault); - - item.hidden = hidden; - Services.prefs.setBoolPref(prefName, hidden); - - if (aAddonsList.length > 0 || aInstallsList.length > 0) { - notifyInitialized(); - return; - } - - gEventManager.registerInstallListener({ - onDownloadStarted: function(aInstall) { - this._maybeShowCategory(aInstall); - }, - - onInstallStarted: function(aInstall) { - this._maybeShowCategory(aInstall); - }, - - onInstallEnded: function(aInstall, aAddon) { - this._maybeShowCategory(aAddon); - }, - - onExternalInstall: function(aAddon, aExistingAddon, aRequiresRestart) { - this._maybeShowCategory(aAddon); - }, - - _maybeShowCategory: aAddon => { - if (aType.id == aAddon.type) { - this.get(aViewId).hidden = false; - Services.prefs.setBoolPref(prefName, false); - gEventManager.unregisterInstallListener(this); - } - } - }); - - notifyInitialized(); - }); - } - - this._insertCategory(aType.id, aType.name, aViewId, aType.uiPriority, - startHidden); - }, - - onTypeRemoved: function(aType) { - this._removeCategory(aType.id); - }, - - get selected() { - return this.node.selectedItem ? this.node.selectedItem.value : null; - }, - - select: function(aId, aPreviousView) { - var view = gViewController.parseViewId(aId); - if (view.type == "detail" && aPreviousView) { - aId = aPreviousView; - view = gViewController.parseViewId(aPreviousView); - } - aId = aId.replace(/\?.*/, ""); - - Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, aId); - - if (this.node.selectedItem && - this.node.selectedItem.value == aId) { - this.node.selectedItem.hidden = false; - this.node.selectedItem.disabled = false; - return; - } - - var item; - if (view.type == "search") - item = this._search; - else - item = this.get(aId); - - if (item) { - item.hidden = false; - item.disabled = false; - this.node.suppressOnSelect = true; - this.node.selectedItem = item; - this.node.suppressOnSelect = false; - this.node.ensureElementIsVisible(item); - - this.maybeHideSearch(); - } - }, - - get: function(aId) { - var items = document.getElementsByAttribute("value", aId); - if (items.length) - return items[0]; - return null; - }, - - setBadge: function(aId, aCount) { - let item = this.get(aId); - if (item) - item.badgeCount = aCount; - }, - - maybeHideSearch: function() { - var view = gViewController.parseViewId(this.node.selectedItem.value); - this._search.disabled = view.type != "search"; - } -}; - - -var gHeader = { - _search: null, - _dest: "", - - initialize: function() { - this._search = document.getElementById("header-search"); - - this._search.addEventListener("command", function(aEvent) { - var query = aEvent.target.value; - if (query.length == 0) - return; - - gViewController.loadView("addons://search/" + encodeURIComponent(query)); - }, false); - - function updateNavButtonVisibility() { - var shouldShow = gHeader.shouldShowNavButtons; - document.getElementById("back-btn").hidden = !shouldShow; - document.getElementById("forward-btn").hidden = !shouldShow; - } - - window.addEventListener("focus", function(aEvent) { - if (aEvent.target == window) - updateNavButtonVisibility(); - }, false); - - updateNavButtonVisibility(); - }, - - focusSearchBox: function() { - this._search.focus(); - }, - - onKeyPress: function(aEvent) { - if (String.fromCharCode(aEvent.charCode) == "/") { - this.focusSearchBox(); - return; - } - }, - - get shouldShowNavButtons() { - var docshellItem = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem); - - // If there is no outer frame then make the buttons visible - if (docshellItem.rootTreeItem == docshellItem) - return true; - - var outerWin = docshellItem.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - var outerDoc = outerWin.document; - var node = outerDoc.getElementById("back-button"); - // If the outer frame has no back-button then make the buttons visible - if (!node) - return true; - - // If the back-button or any of its parents are hidden then make the buttons - // visible - while (node != outerDoc) { - var style = outerWin.getComputedStyle(node, ""); - if (style.display == "none") - return true; - if (style.visibility != "visible") - return true; - node = node.parentNode; - } - - return false; - }, - - get searchQuery() { - return this._search.value; - }, - - set searchQuery(aQuery) { - this._search.value = aQuery; - }, -}; - - -var gDiscoverView = { - node: null, - enabled: true, - // Set to true after the view is first shown. If initialization completes - // after this then it must also load the discover homepage - loaded: false, - _browser: null, - _loading: null, - _error: null, - homepageURL: null, - _loadListeners: [], - hideHeader: true, - - initialize: function() { - this.enabled = isDiscoverEnabled(); - if (!this.enabled) { - gCategories.get("addons://discover/").hidden = true; - return; - } - - this.node = document.getElementById("discover-view"); - this._loading = document.getElementById("discover-loading"); - this._error = document.getElementById("discover-error"); - this._browser = document.getElementById("discover-browser"); - - let compatMode = "normal"; - if (!AddonManager.checkCompatibility) - compatMode = "ignore"; - else if (AddonManager.strictCompatibility) - compatMode = "strict"; - - var url = Services.prefs.getCharPref(PREF_DISCOVERURL); - url = url.replace("%COMPATIBILITY_MODE%", compatMode); - url = Services.urlFormatter.formatURL(url); - - let setURL = (aURL) => { - try { - this.homepageURL = Services.io.newURI(aURL, null, null); - } catch (e) { - this.showError(); - notifyInitialized(); - return; - } - - this._browser.homePage = this.homepageURL.spec; - this._browser.addProgressListener(this); - - if (this.loaded) - this._loadURL(this.homepageURL.spec, false, notifyInitialized); - else - notifyInitialized(); - } - - if (Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED) == false) { - setURL(url); - return; - } - - gPendingInitializations++; - AddonManager.getAllAddons(function(aAddons) { - var list = {}; - for (let addon of aAddons) { - var prefName = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", - addon.id); - try { - if (!Services.prefs.getBoolPref(prefName)) - continue; - } catch (e) { } - list[addon.id] = { - name: addon.name, - version: addon.version, - type: addon.type, - userDisabled: addon.userDisabled, - isCompatible: addon.isCompatible, - isBlocklisted: addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED - } - } - - setURL(url + "#" + JSON.stringify(list)); - }); - }, - - destroy: function() { - try { - this._browser.removeProgressListener(this); - } - catch (e) { - // Ignore the case when the listener wasn't already registered - } - }, - - show: function(aParam, aRequest, aState, aIsRefresh) { - gViewController.updateCommands(); - - // If we're being told to load a specific URL then just do that - if (aState && "url" in aState) { - this.loaded = true; - this._loadURL(aState.url); - } - - // If the view has loaded before and still at the homepage (if refreshing), - // and the error page is not visible then there is nothing else to do - if (this.loaded && this.node.selectedPanel != this._error && - (!aIsRefresh || (this._browser.currentURI && - this._browser.currentURI.spec == this._browser.homePage))) { - gViewController.notifyViewChanged(); - return; - } - - this.loaded = true; - - // No homepage means initialization isn't complete, the browser will get - // loaded once initialization is complete - if (!this.homepageURL) { - this._loadListeners.push(gViewController.notifyViewChanged.bind(gViewController)); - return; - } - - this._loadURL(this.homepageURL.spec, aIsRefresh, - gViewController.notifyViewChanged.bind(gViewController)); - }, - - canRefresh: function() { - if (this._browser.currentURI && - this._browser.currentURI.spec == this._browser.homePage) - return false; - return true; - }, - - refresh: function(aParam, aRequest, aState) { - this.show(aParam, aRequest, aState, true); - }, - - hide: function() { }, - - showError: function() { - this.node.selectedPanel = this._error; - }, - - _loadURL: function(aURL, aKeepHistory, aCallback) { - if (this._browser.currentURI.spec == aURL) { - if (aCallback) - aCallback(); - return; - } - - if (aCallback) - this._loadListeners.push(aCallback); - - var flags = 0; - if (!aKeepHistory) - flags |= Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; - - this._browser.loadURIWithFlags(aURL, flags); - }, - - onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { - // Ignore the about:blank load - if (aLocation.spec == "about:blank") - return; - - // When using the real session history the inner-frame will update the - // session history automatically, if using the fake history though it must - // be manually updated - if (gHistory == FakeHistory) { - var docshell = aWebProgress.QueryInterface(Ci.nsIDocShell); - - var state = { - view: "addons://discover/", - url: aLocation.spec - }; - - var replaceHistory = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY << 16; - if (docshell.loadType & replaceHistory) - gHistory.replaceState(state); - else - gHistory.pushState(state); - gViewController.lastHistoryIndex = gHistory.index; - } - - gViewController.updateCommands(); - - // If the hostname is the same as the new location's host and either the - // default scheme is insecure or the new location is secure then continue - // with the load - if (aLocation.host == this.homepageURL.host && - (!this.homepageURL.schemeIs("https") || aLocation.schemeIs("https"))) - return; - - // Canceling the request will send an error to onStateChange which will show - // the error page - aRequest.cancel(Components.results.NS_BINDING_ABORTED); - }, - - onSecurityChange: function(aWebProgress, aRequest, aState) { - // Don't care about security if the page is not https - if (!this.homepageURL.schemeIs("https")) - return; - - // If the request was secure then it is ok - if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) - return; - - // Canceling the request will send an error to onStateChange which will show - // the error page - aRequest.cancel(Components.results.NS_BINDING_ABORTED); - }, - - onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { - let transferStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | - Ci.nsIWebProgressListener.STATE_IS_REQUEST | - Ci.nsIWebProgressListener.STATE_TRANSFERRING; - // Once transferring begins show the content - if ((aStateFlags & transferStart) === transferStart) - this.node.selectedPanel = this._browser; - - // Only care about the network events - if (!(aStateFlags & (Ci.nsIWebProgressListener.STATE_IS_NETWORK))) - return; - - // If this is the start of network activity then show the loading page - if (aStateFlags & (Ci.nsIWebProgressListener.STATE_START)) - this.node.selectedPanel = this._loading; - - // Ignore anything except stop events - if (!(aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP))) - return; - - // Consider the successful load of about:blank as still loading - if (aRequest instanceof Ci.nsIChannel && aRequest.URI.spec == "about:blank") - return; - - // If there was an error loading the page or the new hostname is not the - // same as the default hostname or the default scheme is secure and the new - // scheme is insecure then show the error page - const NS_ERROR_PARSED_DATA_CACHED = 0x805D0021; - if (!(Components.isSuccessCode(aStatus) || aStatus == NS_ERROR_PARSED_DATA_CACHED) || - (aRequest && aRequest instanceof Ci.nsIHttpChannel && !aRequest.requestSucceeded)) { - this.showError(); - } else { - // Got a successful load, make sure the browser is visible - this.node.selectedPanel = this._browser; - gViewController.updateCommands(); - } - - var listeners = this._loadListeners; - this._loadListeners = []; - - for (let listener of listeners) - listener(); - }, - - onProgressChange: function() { }, - onStatusChange: function() { }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference]), - - getSelectedAddon: function() { - return null; - } -}; - - -var gCachedAddons = {}; - -var gSearchView = { - node: null, - _filter: null, - _sorters: null, - _loading: null, - _listBox: null, - _emptyNotice: null, - _allResultsLink: null, - _lastQuery: null, - _lastRemoteTotal: 0, - _pendingSearches: 0, - - initialize: function() { - this.node = document.getElementById("search-view"); - this._filter = document.getElementById("search-filter-radiogroup"); - this._sorters = document.getElementById("search-sorters"); - this._sorters.handler = this; - this._loading = document.getElementById("search-loading"); - this._listBox = document.getElementById("search-list"); - this._emptyNotice = document.getElementById("search-list-empty"); - this._allResultsLink = document.getElementById("search-allresults-link"); - - if (!AddonManager.isInstallEnabled("application/x-xpinstall")) - this._filter.hidden = true; - - this._listBox.addEventListener("keydown", aEvent => { - if (aEvent.keyCode == aEvent.DOM_VK_RETURN) { - var item = this._listBox.selectedItem; - if (item) - item.showInDetailView(); - } - }, false); - - this._filter.addEventListener("command", () => this.updateView(), false); - }, - - shutdown: function() { - if (AddonRepository.isSearching) - AddonRepository.cancelSearch(); - }, - - get isSearching() { - return this._pendingSearches > 0; - }, - - show: function(aQuery, aRequest) { - gEventManager.registerInstallListener(this); - - this.showEmptyNotice(false); - this.showAllResultsLink(0); - this.showLoading(true); - this._sorters.showprice = false; - - gHeader.searchQuery = aQuery; - aQuery = aQuery.trim().toLocaleLowerCase(); - if (this._lastQuery == aQuery) { - this.updateView(); - gViewController.notifyViewChanged(); - return; - } - this._lastQuery = aQuery; - - if (AddonRepository.isSearching) - AddonRepository.cancelSearch(); - - while (this._listBox.firstChild.localName == "richlistitem") - this._listBox.removeChild(this._listBox.firstChild); - - gCachedAddons = {}; - this._pendingSearches = 2; - this._sorters.setSort("relevancescore", false); - - var elements = []; - - let createSearchResults = (aObjsList, aIsInstall, aIsRemote) => { - for (let index in aObjsList) { - let obj = aObjsList[index]; - let score = aObjsList.length - index; - if (!aIsRemote && aQuery.length > 0) { - score = this.getMatchScore(obj, aQuery); - if (score == 0) - continue; - } - - let item = createItem(obj, aIsInstall, aIsRemote); - item.setAttribute("relevancescore", score); - if (aIsRemote) { - gCachedAddons[obj.id] = obj; - if (obj.purchaseURL) - this._sorters.showprice = true; - } - - elements.push(item); - } - } - - let finishSearch = (createdCount) => { - if (elements.length > 0) { - sortElements(elements, [this._sorters.sortBy], this._sorters.ascending); - for (let element of elements) - this._listBox.insertBefore(element, this._listBox.lastChild); - this.updateListAttributes(); - } - - this._pendingSearches--; - this.updateView(); - - if (!this.isSearching) - gViewController.notifyViewChanged(); - } - - getAddonsAndInstalls(null, function(aAddons, aInstalls) { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - - createSearchResults(aAddons, false, false); - createSearchResults(aInstalls, true, false); - finishSearch(); - }); - - var maxRemoteResults = 0; - try { - maxRemoteResults = Services.prefs.getIntPref(PREF_MAXRESULTS); - } catch (e) {} - - if (maxRemoteResults <= 0) { - finishSearch(0); - return; - } - - AddonRepository.searchAddons(aQuery, maxRemoteResults, { - searchFailed: () => { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - - this._lastRemoteTotal = 0; - - // XXXunf Better handling of AMO search failure. See bug 579502 - finishSearch(0); // Silently fail - }, - - searchSucceeded: (aAddonsList, aAddonCount, aTotalResults) => { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - - if (aTotalResults > maxRemoteResults) - this._lastRemoteTotal = aTotalResults; - else - this._lastRemoteTotal = 0; - - var createdCount = createSearchResults(aAddonsList, false, true); - finishSearch(createdCount); - } - }); - }, - - showLoading: function(aLoading) { - this._loading.hidden = !aLoading; - this._listBox.hidden = aLoading; - }, - - updateView: function() { - var showLocal = this._filter.value == "local"; - - if (!showLocal && !AddonManager.isInstallEnabled("application/x-xpinstall")) - showLocal = true; - - this._listBox.setAttribute("local", showLocal); - this._listBox.setAttribute("remote", !showLocal); - - this.showLoading(this.isSearching && !showLocal); - if (!this.isSearching) { - var isEmpty = true; - var results = this._listBox.getElementsByTagName("richlistitem"); - for (let result of results) { - var isRemote = (result.getAttribute("remote") == "true"); - if ((isRemote && !showLocal) || (!isRemote && showLocal)) { - isEmpty = false; - break; - } - } - - this.showEmptyNotice(isEmpty); - this.showAllResultsLink(this._lastRemoteTotal); - } - - gViewController.updateCommands(); - }, - - hide: function() { - gEventManager.unregisterInstallListener(this); - doPendingUninstalls(this._listBox); - }, - - getMatchScore: function(aObj, aQuery) { - var score = 0; - score += this.calculateMatchScore(aObj.name, aQuery, - SEARCH_SCORE_MULTIPLIER_NAME); - score += this.calculateMatchScore(aObj.description, aQuery, - SEARCH_SCORE_MULTIPLIER_DESCRIPTION); - return score; - }, - - calculateMatchScore: function(aStr, aQuery, aMultiplier) { - var score = 0; - if (!aStr || aQuery.length == 0) - return score; - - aStr = aStr.trim().toLocaleLowerCase(); - var haystack = aStr.split(/\s+/); - var needles = aQuery.split(/\s+/); - - for (let needle of needles) { - for (let hay of haystack) { - if (hay == needle) { - // matching whole words is best - score += SEARCH_SCORE_MATCH_WHOLEWORD; - } else { - let i = hay.indexOf(needle); - if (i == 0) // matching on word boundries is also good - score += SEARCH_SCORE_MATCH_WORDBOUNDRY; - else if (i > 0) // substring matches not so good - score += SEARCH_SCORE_MATCH_SUBSTRING; - } - } - } - - // give progressively higher score for longer queries, since longer queries - // are more likely to be unique and therefore more relevant. - if (needles.length > 1 && aStr.indexOf(aQuery) != -1) - score += needles.length; - - return score * aMultiplier; - }, - - showEmptyNotice: function(aShow) { - this._emptyNotice.hidden = !aShow; - this._listBox.hidden = aShow; - }, - - showAllResultsLink: function(aTotalResults) { - if (aTotalResults == 0) { - this._allResultsLink.hidden = true; - return; - } - - var linkStr = gStrings.ext.GetStringFromName("showAllSearchResults"); - linkStr = PluralForm.get(aTotalResults, linkStr); - linkStr = linkStr.replace("#1", aTotalResults); - this._allResultsLink.setAttribute("value", linkStr); - - this._allResultsLink.setAttribute("href", - AddonRepository.getSearchURL(this._lastQuery)); - this._allResultsLink.hidden = false; - }, - - updateListAttributes: function() { - var item = this._listBox.querySelector("richlistitem[remote='true'][first]"); - if (item) - item.removeAttribute("first"); - item = this._listBox.querySelector("richlistitem[remote='true'][last]"); - if (item) - item.removeAttribute("last"); - var items = this._listBox.querySelectorAll("richlistitem[remote='true']"); - if (items.length > 0) { - items[0].setAttribute("first", true); - items[items.length - 1].setAttribute("last", true); - } - - item = this._listBox.querySelector("richlistitem:not([remote='true'])[first]"); - if (item) - item.removeAttribute("first"); - item = this._listBox.querySelector("richlistitem:not([remote='true'])[last]"); - if (item) - item.removeAttribute("last"); - items = this._listBox.querySelectorAll("richlistitem:not([remote='true'])"); - if (items.length > 0) { - items[0].setAttribute("first", true); - items[items.length - 1].setAttribute("last", true); - } - - }, - - onSortChanged: function(aSortBy, aAscending) { - var footer = this._listBox.lastChild; - this._listBox.removeChild(footer); - - sortList(this._listBox, aSortBy, aAscending); - this.updateListAttributes(); - - this._listBox.appendChild(footer); - }, - - onDownloadCancelled: function(aInstall) { - this.removeInstall(aInstall); - }, - - onInstallCancelled: function(aInstall) { - this.removeInstall(aInstall); - }, - - removeInstall: function(aInstall) { - for (let item of this._listBox.childNodes) { - if (item.mInstall == aInstall) { - this._listBox.removeChild(item); - return; - } - } - }, - - getSelectedAddon: function() { - var item = this._listBox.selectedItem; - if (item) - return item.mAddon; - return null; - }, - - getListItemForID: function(aId) { - var listitem = this._listBox.firstChild; - while (listitem) { - if (listitem.getAttribute("status") == "installed" && listitem.mAddon.id == aId) - return listitem; - listitem = listitem.nextSibling; - } - return null; - } -}; - - -var gListView = { - node: null, - _listBox: null, - _emptyNotice: null, - _type: null, - - initialize: function() { - this.node = document.getElementById("list-view"); - this._listBox = document.getElementById("addon-list"); - this._emptyNotice = document.getElementById("addon-list-empty"); - - this._listBox.addEventListener("keydown", (aEvent) => { - if (aEvent.keyCode == aEvent.DOM_VK_RETURN) { - var item = this._listBox.selectedItem; - if (item) - item.showInDetailView(); - } - }, false); - - document.getElementById("signing-learn-more").setAttribute("href", - Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"); - - let findSignedAddonsLink = document.getElementById("find-alternative-addons"); - try { - findSignedAddonsLink.setAttribute("href", - Services.urlFormatter.formatURLPref("extensions.getAddons.link.url")); - } catch (e) { - findSignedAddonsLink.classList.remove("text-link"); - } - - try { - document.getElementById("signing-dev-manual-link").setAttribute("href", - Services.prefs.getCharPref("xpinstall.signatures.devInfoURL")); - } catch (e) { - document.getElementById("signing-dev-info").hidden = true; - } - - // To-Do: remove deprecation notice content. - document.getElementById("plugindeprecation-notice").hidden = true; - }, - - show: function(aType, aRequest) { - let showOnlyDisabledUnsigned = false; - if (aType.endsWith("?unsigned=true")) { - aType = aType.replace(/\?.*/, ""); - showOnlyDisabledUnsigned = true; - } - - if (!(aType in AddonManager.addonTypes)) - throw Components.Exception("Attempting to show unknown type " + aType, Cr.NS_ERROR_INVALID_ARG); - - this._type = aType; - this.node.setAttribute("type", aType); - this.showEmptyNotice(false); - - while (this._listBox.itemCount > 0) - this._listBox.removeItemAt(0); - - if (aType == "plugin") { - navigator.plugins.refresh(false); - } - - getAddonsAndInstalls(aType, (aAddonsList, aInstallsList) => { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - - var elements = []; - - for (let addonItem of aAddonsList) - elements.push(createItem(addonItem)); - - for (let installItem of aInstallsList) - elements.push(createItem(installItem, true)); - - this.showEmptyNotice(elements.length == 0); - if (elements.length > 0) { - sortElements(elements, ["uiState", "name"], true); - for (let element of elements) - this._listBox.appendChild(element); - } - - this.filterDisabledUnsigned(showOnlyDisabledUnsigned); - - gEventManager.registerInstallListener(this); - gViewController.updateCommands(); - gViewController.notifyViewChanged(); - }); - }, - - hide: function() { - gEventManager.unregisterInstallListener(this); - doPendingUninstalls(this._listBox); - }, - - filterDisabledUnsigned: function(aFilter = true) { - let foundDisabledUnsigned = false; - - if (SIGNING_REQUIRED) { - for (let item of this._listBox.childNodes) { - if (!isCorrectlySigned(item.mAddon)) - foundDisabledUnsigned = true; - else - item.hidden = aFilter; - } - } - - document.getElementById("show-disabled-unsigned-extensions").hidden = - aFilter || !foundDisabledUnsigned; - - document.getElementById("show-all-extensions").hidden = !aFilter; - document.getElementById("disabled-unsigned-addons-info").hidden = !aFilter; - }, - - showEmptyNotice: function(aShow) { - this._emptyNotice.hidden = !aShow; - this._listBox.hidden = aShow; - }, - - onSortChanged: function(aSortBy, aAscending) { - sortList(this._listBox, aSortBy, aAscending); - }, - - onExternalInstall: function(aAddon, aExistingAddon, aRequiresRestart) { - // The existing list item will take care of upgrade installs - if (aExistingAddon) - return; - - if (aAddon.hidden) - return; - - this.addItem(aAddon); - }, - - onDownloadStarted: function(aInstall) { - this.addItem(aInstall, true); - }, - - onInstallStarted: function(aInstall) { - this.addItem(aInstall, true); - }, - - onDownloadCancelled: function(aInstall) { - this.removeItem(aInstall, true); - }, - - onInstallCancelled: function(aInstall) { - this.removeItem(aInstall, true); - }, - - onInstallEnded: function(aInstall) { - // Remove any install entries for upgrades, their status will appear against - // the existing item - if (aInstall.existingAddon) - this.removeItem(aInstall, true); - }, - - addItem: function(aObj, aIsInstall) { - if (aObj.type != this._type) - return; - - if (aIsInstall && aObj.existingAddon) - return; - - let prop = aIsInstall ? "mInstall" : "mAddon"; - for (let item of this._listBox.childNodes) { - if (item[prop] == aObj) - return; - } - - let item = createItem(aObj, aIsInstall); - this._listBox.insertBefore(item, this._listBox.firstChild); - this.showEmptyNotice(false); - }, - - removeItem: function(aObj, aIsInstall) { - let prop = aIsInstall ? "mInstall" : "mAddon"; - - for (let item of this._listBox.childNodes) { - if (item[prop] == aObj) { - this._listBox.removeChild(item); - this.showEmptyNotice(this._listBox.itemCount == 0); - return; - } - } - }, - - getSelectedAddon: function() { - var item = this._listBox.selectedItem; - if (item) - return item.mAddon; - return null; - }, - - getListItemForID: function(aId) { - var listitem = this._listBox.firstChild; - while (listitem) { - if (listitem.getAttribute("status") == "installed" && listitem.mAddon.id == aId) - return listitem; - listitem = listitem.nextSibling; - } - return null; - } -}; - - -var gDetailView = { - node: null, - _addon: null, - _loadingTimer: null, - _autoUpdate: null, - - initialize: function() { - this.node = document.getElementById("detail-view"); - - this._autoUpdate = document.getElementById("detail-autoUpdate"); - - this._autoUpdate.addEventListener("command", () => { - this._addon.applyBackgroundUpdates = this._autoUpdate.value; - }, true); - }, - - shutdown: function() { - AddonManager.removeManagerListener(this); - }, - - onUpdateModeChanged: function() { - this.onPropertyChanged(["applyBackgroundUpdates"]); - }, - - _updateView: function(aAddon, aIsRemote, aScrollToPreferences) { - AddonManager.addManagerListener(this); - this.clearLoading(); - - this._addon = aAddon; - gEventManager.registerAddonListener(this, aAddon.id); - gEventManager.registerInstallListener(this); - - this.node.setAttribute("type", aAddon.type); - - // If the search category isn't selected then make sure to select the - // correct category - if (gCategories.selected != "addons://search/") - gCategories.select("addons://list/" + aAddon.type); - - document.getElementById("detail-name").textContent = aAddon.name; - var icon = AddonManager.getPreferredIconURL(aAddon, 64, window); - document.getElementById("detail-icon").src = icon ? icon : ""; - document.getElementById("detail-creator").setCreator(aAddon.creator, aAddon.homepageURL); - - var version = document.getElementById("detail-version"); - if (shouldShowVersionNumber(aAddon)) { - version.hidden = false; - version.value = aAddon.version; - } else { - version.hidden = true; - } - - var screenshotbox = document.getElementById("detail-screenshot-box"); - var screenshot = document.getElementById("detail-screenshot"); - if (aAddon.screenshots && aAddon.screenshots.length > 0) { - if (aAddon.screenshots[0].thumbnailURL) { - screenshot.src = aAddon.screenshots[0].thumbnailURL; - screenshot.width = aAddon.screenshots[0].thumbnailWidth; - screenshot.height = aAddon.screenshots[0].thumbnailHeight; - } else { - screenshot.src = aAddon.screenshots[0].url; - screenshot.width = aAddon.screenshots[0].width; - screenshot.height = aAddon.screenshots[0].height; - } - screenshot.setAttribute("loading", "true"); - screenshotbox.hidden = false; - } else { - screenshotbox.hidden = true; - } - - var desc = document.getElementById("detail-desc"); - desc.textContent = aAddon.description; - - var fullDesc = document.getElementById("detail-fulldesc"); - if (aAddon.fullDescription) { - // The following is part of an awful hack to include the licenses for GMP - // plugins without having bug 624602 fixed yet, and intentionally ignores - // localisation. - if (aAddon.isGMPlugin) { - fullDesc.innerHTML = aAddon.fullDescription; - } else { - fullDesc.textContent = aAddon.fullDescription; - } - - fullDesc.hidden = false; - } else { - fullDesc.hidden = true; - } - - var contributions = document.getElementById("detail-contributions"); - if ("contributionURL" in aAddon && aAddon.contributionURL) { - contributions.hidden = false; - var amount = document.getElementById("detail-contrib-suggested"); - if (aAddon.contributionAmount) { - amount.value = gStrings.ext.formatStringFromName("contributionAmount2", - [aAddon.contributionAmount], - 1); - amount.hidden = false; - } else { - amount.hidden = true; - } - } else { - contributions.hidden = true; - } - - if ("purchaseURL" in aAddon && aAddon.purchaseURL) { - var purchase = document.getElementById("detail-purchase-btn"); - purchase.label = gStrings.ext.formatStringFromName("cmd.purchaseAddon.label", - [aAddon.purchaseDisplayAmount], - 1); - purchase.accesskey = gStrings.ext.GetStringFromName("cmd.purchaseAddon.accesskey"); - } - - var updateDateRow = document.getElementById("detail-dateUpdated"); - if (aAddon.updateDate) { - var date = formatDate(aAddon.updateDate); - updateDateRow.value = date; - } else { - updateDateRow.value = null; - } - - // TODO if the add-on was downloaded from releases.mozilla.org link to the - // AMO profile (bug 590344) - if (false) { - document.getElementById("detail-repository-row").hidden = false; - document.getElementById("detail-homepage-row").hidden = true; - var repository = document.getElementById("detail-repository"); - repository.value = aAddon.homepageURL; - repository.href = aAddon.homepageURL; - } else if (aAddon.homepageURL) { - document.getElementById("detail-repository-row").hidden = true; - document.getElementById("detail-homepage-row").hidden = false; - var homepage = document.getElementById("detail-homepage"); - homepage.value = aAddon.homepageURL; - homepage.href = aAddon.homepageURL; - } else { - document.getElementById("detail-repository-row").hidden = true; - document.getElementById("detail-homepage-row").hidden = true; - } - - var rating = document.getElementById("detail-rating"); - if (aAddon.averageRating) { - rating.averageRating = aAddon.averageRating; - rating.hidden = false; - } else { - rating.hidden = true; - } - - var reviews = document.getElementById("detail-reviews"); - if (aAddon.reviewURL) { - var text = gStrings.ext.GetStringFromName("numReviews"); - text = PluralForm.get(aAddon.reviewCount, text) - text = text.replace("#1", aAddon.reviewCount); - reviews.value = text; - reviews.hidden = false; - reviews.href = aAddon.reviewURL; - } else { - reviews.hidden = true; - } - - document.getElementById("detail-rating-row").hidden = !aAddon.averageRating && !aAddon.reviewURL; - - var sizeRow = document.getElementById("detail-size"); - if (aAddon.size && aIsRemote) { - let [size, unit] = DownloadUtils.convertByteUnits(parseInt(aAddon.size)); - let formatted = gStrings.dl.GetStringFromName("doneSize"); - formatted = formatted.replace("#1", size).replace("#2", unit); - sizeRow.value = formatted; - } else { - sizeRow.value = null; - } - - var downloadsRow = document.getElementById("detail-downloads"); - if (aAddon.totalDownloads && aIsRemote) { - var downloads = aAddon.totalDownloads; - downloadsRow.value = downloads; - } else { - downloadsRow.value = null; - } - - var canUpdate = !aIsRemote && hasPermission(aAddon, "upgrade") && aAddon.id != AddonManager.hotfixID; - document.getElementById("detail-updates-row").hidden = !canUpdate; - - if ("applyBackgroundUpdates" in aAddon) { - this._autoUpdate.hidden = false; - this._autoUpdate.value = aAddon.applyBackgroundUpdates; - let hideFindUpdates = AddonManager.shouldAutoUpdate(this._addon); - document.getElementById("detail-findUpdates-btn").hidden = hideFindUpdates; - } else { - this._autoUpdate.hidden = true; - document.getElementById("detail-findUpdates-btn").hidden = false; - } - - document.getElementById("detail-prefs-btn").hidden = !aIsRemote && - !gViewController.commands.cmd_showItemPreferences.isEnabled(aAddon); - - var gridRows = document.querySelectorAll("#detail-grid rows row"); - let first = true; - for (let gridRow of gridRows) { - if (first && window.getComputedStyle(gridRow, null).getPropertyValue("display") != "none") { - gridRow.setAttribute("first-row", true); - first = false; - } else { - gridRow.removeAttribute("first-row"); - } - } - - this.fillSettingsRows(aScrollToPreferences, (function() { - this.updateState(); - gViewController.notifyViewChanged(); - }).bind(this)); - }, - - show: function(aAddonId, aRequest) { - let index = aAddonId.indexOf("/preferences"); - let scrollToPreferences = false; - if (index >= 0) { - aAddonId = aAddonId.substring(0, index); - scrollToPreferences = true; - } - - this._loadingTimer = setTimeout(() => { - this.node.setAttribute("loading-extended", true); - }, LOADING_MSG_DELAY); - - var view = gViewController.currentViewId; - - AddonManager.getAddonByID(aAddonId, (aAddon) => { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - - if (aAddon) { - this._updateView(aAddon, false, scrollToPreferences); - return; - } - - // Look for an add-on pending install - AddonManager.getAllInstalls(aInstalls => { - for (let install of aInstalls) { - if (install.state == AddonManager.STATE_INSTALLED && - install.addon.id == aAddonId) { - this._updateView(install.addon, false); - return; - } - } - - if (aAddonId in gCachedAddons) { - this._updateView(gCachedAddons[aAddonId], true); - return; - } - - // This might happen due to session restore restoring us back to an - // add-on that doesn't exist but otherwise shouldn't normally happen. - // Either way just revert to the default view. - gViewController.replaceView(gViewDefault); - }); - }); - }, - - hide: function() { - AddonManager.removeManagerListener(this); - this.clearLoading(); - if (this._addon) { - if (hasInlineOptions(this._addon)) { - Services.obs.notifyObservers(document, - AddonManager.OPTIONS_NOTIFICATION_HIDDEN, - this._addon.id); - } - - gEventManager.unregisterAddonListener(this, this._addon.id); - gEventManager.unregisterInstallListener(this); - this._addon = null; - - // Flush the preferences to disk so they survive any crash - if (this.node.getElementsByTagName("setting").length) - Services.prefs.savePrefFile(null); - } - }, - - updateState: function() { - gViewController.updateCommands(); - - var pending = this._addon.pendingOperations; - if (pending != AddonManager.PENDING_NONE) { - this.node.removeAttribute("notification"); - - pending = null; - const PENDING_OPERATIONS = ["enable", "disable", "install", "uninstall", - "upgrade"]; - for (let op of PENDING_OPERATIONS) { - if (isPending(this._addon, op)) - pending = op; - } - - this.node.setAttribute("pending", pending); - document.getElementById("detail-pending").textContent = gStrings.ext.formatStringFromName( - "details.notification." + pending, - [this._addon.name, gStrings.brandShortName], 2 - ); - } else { - this.node.removeAttribute("pending"); - - if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { - this.node.setAttribute("notification", "error"); - document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName( - "details.notification.blocked", - [this._addon.name], 1 - ); - var errorLink = document.getElementById("detail-error-link"); - errorLink.value = gStrings.ext.GetStringFromName("details.notification.blocked.link"); - errorLink.href = this._addon.blocklistURL; - errorLink.hidden = false; - } else if (!isCorrectlySigned(this._addon) && SIGNING_REQUIRED) { - this.node.setAttribute("notification", "error"); - document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName( - "details.notification.unsignedAndDisabled", [this._addon.name, gStrings.brandShortName], 2 - ); - let errorLink = document.getElementById("detail-error-link"); - errorLink.value = gStrings.ext.GetStringFromName("details.notification.unsigned.link"); - errorLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; - errorLink.hidden = false; - } else if (!this._addon.isCompatible && (AddonManager.checkCompatibility || - (this._addon.blocklistState != Ci.nsIBlocklistService.STATE_SOFTBLOCKED))) { - this.node.setAttribute("notification", "warning"); - document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName( - "details.notification.incompatible", - [this._addon.name, gStrings.brandShortName, gStrings.appVersion], 3 - ); - document.getElementById("detail-warning-link").hidden = true; - } else if (!isCorrectlySigned(this._addon)) { - this.node.setAttribute("notification", "warning"); - document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName( - "details.notification.unsigned", [this._addon.name, gStrings.brandShortName], 2 - ); - var warningLink = document.getElementById("detail-warning-link"); - warningLink.value = gStrings.ext.GetStringFromName("details.notification.unsigned.link"); - warningLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; - warningLink.hidden = false; - } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { - this.node.setAttribute("notification", "warning"); - document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName( - "details.notification.softblocked", - [this._addon.name], 1 - ); - let warningLink = document.getElementById("detail-warning-link"); - warningLink.value = gStrings.ext.GetStringFromName("details.notification.softblocked.link"); - warningLink.href = this._addon.blocklistURL; - warningLink.hidden = false; - } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) { - this.node.setAttribute("notification", "warning"); - document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName( - "details.notification.outdated", - [this._addon.name], 1 - ); - let warningLink = document.getElementById("detail-warning-link"); - warningLink.value = gStrings.ext.GetStringFromName("details.notification.outdated.link"); - warningLink.href = this._addon.blocklistURL; - warningLink.hidden = false; - } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) { - this.node.setAttribute("notification", "error"); - document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName( - "details.notification.vulnerableUpdatable", - [this._addon.name], 1 - ); - let errorLink = document.getElementById("detail-error-link"); - errorLink.value = gStrings.ext.GetStringFromName("details.notification.vulnerableUpdatable.link"); - errorLink.href = this._addon.blocklistURL; - errorLink.hidden = false; - } else if (this._addon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) { - this.node.setAttribute("notification", "error"); - document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName( - "details.notification.vulnerableNoUpdate", - [this._addon.name], 1 - ); - let errorLink = document.getElementById("detail-error-link"); - errorLink.value = gStrings.ext.GetStringFromName("details.notification.vulnerableNoUpdate.link"); - errorLink.href = this._addon.blocklistURL; - errorLink.hidden = false; - } else if (this._addon.isGMPlugin && !this._addon.isInstalled && - this._addon.isActive) { - this.node.setAttribute("notification", "warning"); - let warning = document.getElementById("detail-warning"); - warning.textContent = - gStrings.ext.formatStringFromName("details.notification.gmpPending", - [this._addon.name], 1); - } else { - this.node.removeAttribute("notification"); - } - } - - let menulist = document.getElementById("detail-state-menulist"); - let addonType = AddonManager.addonTypes[this._addon.type]; - if (addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) { - let askItem = document.getElementById("detail-ask-to-activate-menuitem"); - let alwaysItem = document.getElementById("detail-always-activate-menuitem"); - let neverItem = document.getElementById("detail-never-activate-menuitem"); - let hasActivatePermission = - ["ask_to_activate", "enable", "disable"].some(perm => hasPermission(this._addon, perm)); - - if (!this._addon.isActive) { - menulist.selectedItem = neverItem; - } else if (this._addon.userDisabled == AddonManager.STATE_ASK_TO_ACTIVATE) { - menulist.selectedItem = askItem; - } else { - menulist.selectedItem = alwaysItem; - } - - menulist.disabled = !hasActivatePermission; - menulist.hidden = false; - menulist.classList.add('no-auto-hide'); - } else { - menulist.hidden = true; - } - - this.node.setAttribute("active", this._addon.isActive); - }, - - clearLoading: function() { - if (this._loadingTimer) { - clearTimeout(this._loadingTimer); - this._loadingTimer = null; - } - - this.node.removeAttribute("loading-extended"); - }, - - emptySettingsRows: function() { - var lastRow = document.getElementById("detail-downloads"); - var rows = lastRow.parentNode; - while (lastRow.nextSibling) - rows.removeChild(rows.lastChild); - }, - - fillSettingsRows: function(aScrollToPreferences, aCallback) { - this.emptySettingsRows(); - if (!hasInlineOptions(this._addon)) { - if (aCallback) - aCallback(); - return; - } - - // We can't use a promise for this, since some code (especially in tests) - // relies on us finishing before the ViewChanged event bubbles up to its - // listeners, and promises resolve asynchronously. - let whenViewLoaded = callback => { - if (gViewController.displayedView.hasAttribute("loading")) { - gDetailView.node.addEventListener("ViewChanged", function viewChangedEventListener() { - gDetailView.node.removeEventListener("ViewChanged", viewChangedEventListener); - callback(); - }); - } else { - callback(); - } - }; - - let finish = (firstSetting) => { - // Ensure the page has loaded and force the XBL bindings to be synchronously applied, - // then notify observers. - whenViewLoaded(() => { - if (firstSetting) - firstSetting.clientTop; - Services.obs.notifyObservers(document, - AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, - this._addon.id); - if (aScrollToPreferences) - gDetailView.scrollToPreferencesRows(); - }); - } - - // This function removes and returns the text content of aNode without - // removing any child elements. Removing the text nodes ensures any XBL - // bindings apply properly. - function stripTextNodes(aNode) { - var text = ''; - for (var i = 0; i < aNode.childNodes.length; i++) { - if (aNode.childNodes[i].nodeType != document.ELEMENT_NODE) { - text += aNode.childNodes[i].textContent; - aNode.removeChild(aNode.childNodes[i--]); - } else { - text += stripTextNodes(aNode.childNodes[i]); - } - } - return text; - } - - var rows = document.getElementById("detail-downloads").parentNode; - - try { - if (this._addon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER) { - whenViewLoaded(() => { - this.createOptionsBrowser(rows).then(browser => { - // Make sure the browser is unloaded as soon as we change views, - // rather than waiting for the next detail view to load. - document.addEventListener("ViewChanged", function viewChangedEventListener() { - document.removeEventListener("ViewChanged", viewChangedEventListener); - browser.remove(); - }); - - finish(browser); - }); - }); - - if (aCallback) - aCallback(); - } else { - var xhr = new XMLHttpRequest(); - xhr.open("GET", this._addon.optionsURL, true); - xhr.responseType = "xml"; - xhr.onload = (function() { - var xml = xhr.responseXML; - var settings = xml.querySelectorAll(":root > setting"); - - var firstSetting = null; - for (var setting of settings) { - - var desc = stripTextNodes(setting).trim(); - if (!setting.hasAttribute("desc")) - setting.setAttribute("desc", desc); - - var type = setting.getAttribute("type"); - if (type == "file" || type == "directory") - setting.setAttribute("fullpath", "true"); - - setting = document.importNode(setting, true); - var style = setting.getAttribute("style"); - if (style) { - setting.removeAttribute("style"); - setting.setAttribute("style", style); - } - - rows.appendChild(setting); - var visible = window.getComputedStyle(setting, null).getPropertyValue("display") != "none"; - if (!firstSetting && visible) { - setting.setAttribute("first-row", true); - firstSetting = setting; - } - } - - finish(firstSetting); - - if (aCallback) - aCallback(); - }).bind(this); - xhr.onerror = function(aEvent) { - Cu.reportError("Error " + aEvent.target.status + - " occurred while receiving " + this._addon.optionsURL); - if (aCallback) - aCallback(); - }; - xhr.send(); - } - } catch (e) { - Cu.reportError(e); - if (aCallback) - aCallback(); - } - }, - - scrollToPreferencesRows: function() { - // We find this row, rather than remembering it from above, - // in case it has been changed by the observers. - let firstRow = gDetailView.node.querySelector('setting[first-row="true"]'); - if (firstRow) { - let top = firstRow.boxObject.y; - top -= parseInt(window.getComputedStyle(firstRow, null).getPropertyValue("margin-top")); - - let detailViewBoxObject = gDetailView.node.boxObject; - top -= detailViewBoxObject.y; - - detailViewBoxObject.scrollTo(0, top); - } - }, - - createOptionsBrowser: function(parentNode) { - let browser = document.createElement("browser"); - browser.setAttribute("type", "content"); - browser.setAttribute("disableglobalhistory", "true"); - browser.setAttribute("class", "inline-options-browser"); - - return new Promise((resolve, reject) => { - let messageListener = { - receiveMessage({name, data}) { - if (name === "Extension:BrowserResized") - browser.style.height = `${data.height}px`; - else if (name === "Extension:BrowserContentLoaded") - resolve(browser); - }, - }; - - let onload = () => { - browser.removeEventListener("load", onload, true); - - let mm = new FakeFrameMessageManager(browser); - mm.loadFrameScript("chrome://extensions/content/ext-browser-content.js", - false); - mm.addMessageListener("Extension:BrowserContentLoaded", messageListener); - mm.addMessageListener("Extension:BrowserResized", messageListener); - mm.sendAsyncMessage("Extension:InitBrowser", {fixedWidth: true}); - - browser.setAttribute("src", this._addon.optionsURL); - }; - browser.addEventListener("load", onload, true); - browser.addEventListener("error", reject); - - parentNode.appendChild(browser); - }); - }, - - getSelectedAddon: function() { - return this._addon; - }, - - onEnabling: function() { - this.updateState(); - }, - - onEnabled: function() { - this.updateState(); - this.fillSettingsRows(); - }, - - onDisabling: function(aNeedsRestart) { - this.updateState(); - if (!aNeedsRestart && hasInlineOptions(this._addon)) { - Services.obs.notifyObservers(document, - AddonManager.OPTIONS_NOTIFICATION_HIDDEN, - this._addon.id); - } - }, - - onDisabled: function() { - this.updateState(); - this.emptySettingsRows(); - }, - - onUninstalling: function() { - this.updateState(); - }, - - onUninstalled: function() { - gViewController.popState(); - }, - - onOperationCancelled: function() { - this.updateState(); - }, - - onPropertyChanged: function(aProperties) { - if (aProperties.indexOf("applyBackgroundUpdates") != -1) { - this._autoUpdate.value = this._addon.applyBackgroundUpdates; - let hideFindUpdates = AddonManager.shouldAutoUpdate(this._addon); - document.getElementById("detail-findUpdates-btn").hidden = hideFindUpdates; - } - - if (aProperties.indexOf("appDisabled") != -1 || - aProperties.indexOf("signedState") != -1 || - aProperties.indexOf("userDisabled") != -1) - this.updateState(); - }, - - onExternalInstall: function(aAddon, aExistingAddon, aNeedsRestart) { - // Only care about upgrades for the currently displayed add-on - if (!aExistingAddon || aExistingAddon.id != this._addon.id) - return; - - if (!aNeedsRestart) - this._updateView(aAddon, false); - else - this.updateState(); - }, - - onInstallCancelled: function(aInstall) { - if (aInstall.addon.id == this._addon.id) - gViewController.popState(); - } -}; - - -var gUpdatesView = { - node: null, - _listBox: null, - _emptyNotice: null, - _sorters: null, - _updateSelected: null, - _categoryItem: null, - - initialize: function() { - this.node = document.getElementById("updates-view"); - this._listBox = document.getElementById("updates-list"); - this._emptyNotice = document.getElementById("updates-list-empty"); - this._sorters = document.getElementById("updates-sorters"); - this._sorters.handler = this; - - this._categoryItem = gCategories.get("addons://updates/available"); - - this._updateSelected = document.getElementById("update-selected-btn"); - this._updateSelected.addEventListener("command", function() { - gUpdatesView.installSelected(); - }, false); - - this.updateAvailableCount(true); - - AddonManager.addAddonListener(this); - AddonManager.addInstallListener(this); - }, - - shutdown: function() { - AddonManager.removeAddonListener(this); - AddonManager.removeInstallListener(this); - }, - - show: function(aType, aRequest) { - document.getElementById("empty-availableUpdates-msg").hidden = aType != "available"; - document.getElementById("empty-recentUpdates-msg").hidden = aType != "recent"; - this.showEmptyNotice(false); - - while (this._listBox.itemCount > 0) - this._listBox.removeItemAt(0); - - this.node.setAttribute("updatetype", aType); - if (aType == "recent") - this._showRecentUpdates(aRequest); - else - this._showAvailableUpdates(false, aRequest); - }, - - hide: function() { - this._updateSelected.hidden = true; - this._categoryItem.disabled = this._categoryItem.badgeCount == 0; - doPendingUninstalls(this._listBox); - }, - - _showRecentUpdates: function(aRequest) { - AddonManager.getAllAddons((aAddonsList) => { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - - var elements = []; - let threshold = Date.now() - UPDATES_RECENT_TIMESPAN; - for (let addon of aAddonsList) { - if (addon.hidden || !addon.updateDate || addon.updateDate.getTime() < threshold) - continue; - - elements.push(createItem(addon)); - } - - this.showEmptyNotice(elements.length == 0); - if (elements.length > 0) { - sortElements(elements, [this._sorters.sortBy], this._sorters.ascending); - for (let element of elements) - this._listBox.appendChild(element); - } - - gViewController.notifyViewChanged(); - }); - }, - - _showAvailableUpdates: function(aIsRefresh, aRequest) { - /* Disable the Update Selected button so it can't get clicked - before everything is initialized asynchronously. - It will get re-enabled by maybeDisableUpdateSelected(). */ - this._updateSelected.disabled = true; - - AddonManager.getAllInstalls((aInstallsList) => { - if (!aIsRefresh && gViewController && aRequest && - aRequest != gViewController.currentViewRequest) - return; - - if (aIsRefresh) { - this.showEmptyNotice(false); - this._updateSelected.hidden = true; - - while (this._listBox.childNodes.length > 0) - this._listBox.removeChild(this._listBox.firstChild); - } - - var elements = []; - - for (let install of aInstallsList) { - if (!this.isManualUpdate(install)) - continue; - - let item = createItem(install.existingAddon); - item.setAttribute("upgrade", true); - item.addEventListener("IncludeUpdateChanged", () => { - this.maybeDisableUpdateSelected(); - }, false); - elements.push(item); - } - - this.showEmptyNotice(elements.length == 0); - if (elements.length > 0) { - this._updateSelected.hidden = false; - sortElements(elements, [this._sorters.sortBy], this._sorters.ascending); - for (let element of elements) - this._listBox.appendChild(element); - } - - // ensure badge count is in sync - this._categoryItem.badgeCount = this._listBox.itemCount; - - gViewController.notifyViewChanged(); - }); - }, - - showEmptyNotice: function(aShow) { - this._emptyNotice.hidden = !aShow; - this._listBox.hidden = aShow; - }, - - isManualUpdate: function(aInstall, aOnlyAvailable) { - var isManual = aInstall.existingAddon && - !AddonManager.shouldAutoUpdate(aInstall.existingAddon); - if (isManual && aOnlyAvailable) - return isInState(aInstall, "available"); - return isManual; - }, - - maybeRefresh: function() { - if (gViewController.currentViewId == "addons://updates/available") - this._showAvailableUpdates(true); - this.updateAvailableCount(); - }, - - updateAvailableCount: function(aInitializing) { - if (aInitializing) - gPendingInitializations++; - AddonManager.getAllInstalls((aInstallsList) => { - var count = aInstallsList.filter(aInstall => { - return this.isManualUpdate(aInstall, true); - }).length; - this._categoryItem.disabled = gViewController.currentViewId != "addons://updates/available" && - count == 0; - this._categoryItem.badgeCount = count; - if (aInitializing) - notifyInitialized(); - }); - }, - - maybeDisableUpdateSelected: function() { - for (let item of this._listBox.childNodes) { - if (item.includeUpdate) { - this._updateSelected.disabled = false; - return; - } - } - this._updateSelected.disabled = true; - }, - - installSelected: function() { - for (let item of this._listBox.childNodes) { - if (item.includeUpdate) - item.upgrade(); - } - - this._updateSelected.disabled = true; - }, - - getSelectedAddon: function() { - var item = this._listBox.selectedItem; - if (item) - return item.mAddon; - return null; - }, - - getListItemForID: function(aId) { - var listitem = this._listBox.firstChild; - while (listitem) { - if (listitem.mAddon.id == aId) - return listitem; - listitem = listitem.nextSibling; - } - return null; - }, - - onSortChanged: function(aSortBy, aAscending) { - sortList(this._listBox, aSortBy, aAscending); - }, - - onNewInstall: function(aInstall) { - if (!this.isManualUpdate(aInstall)) - return; - this.maybeRefresh(); - }, - - onInstallStarted: function(aInstall) { - this.updateAvailableCount(); - }, - - onInstallCancelled: function(aInstall) { - if (!this.isManualUpdate(aInstall)) - return; - this.maybeRefresh(); - }, - - onPropertyChanged: function(aAddon, aProperties) { - if (aProperties.indexOf("applyBackgroundUpdates") != -1) - this.updateAvailableCount(); - } -}; - -var gDragDrop = { - onDragOver: function(aEvent) { - var types = aEvent.dataTransfer.types; - if (types.includes("text/uri-list") || - types.includes("text/x-moz-url") || - types.includes("application/x-moz-file")) - aEvent.preventDefault(); - }, - - onDrop: function(aEvent) { - var dataTransfer = aEvent.dataTransfer; - var urls = []; - - // Convert every dropped item into a url - for (var i = 0; i < dataTransfer.mozItemCount; i++) { - var url = dataTransfer.mozGetDataAt("text/uri-list", i); - if (url) { - urls.push(url); - continue; - } - - url = dataTransfer.mozGetDataAt("text/x-moz-url", i); - if (url) { - urls.push(url.split("\n")[0]); - continue; - } - - var file = dataTransfer.mozGetDataAt("application/x-moz-file", i); - if (file) { - urls.push(Services.io.newFileURI(file).spec); - continue; - } - } - - var pos = 0; - var installs = []; - - function buildNextInstall() { - if (pos == urls.length) { - if (installs.length > 0) { - // Display the normal install confirmation for the installs - let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"]. - getService(Ci.amIWebInstallListener); - webInstaller.onWebInstallRequested(getBrowserElement(), - document.documentURIObject, - installs); - } - return; - } - - AddonManager.getInstallForURL(urls[pos++], function(aInstall) { - installs.push(aInstall); - buildNextInstall(); - }, "application/x-xpinstall"); - } - - buildNextInstall(); - - aEvent.preventDefault(); - } -}; diff --git a/toolkit/mozapps/webextensions/content/extensions.xml b/toolkit/mozapps/webextensions/content/extensions.xml deleted file mode 100644 index b49645cf0..000000000 --- a/toolkit/mozapps/webextensions/content/extensions.xml +++ /dev/null @@ -1,2008 +0,0 @@ -<?xml version="1.0"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - -<!DOCTYPE page [ -<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd"> -%extensionsDTD; -]> - -<!-- import-globals-from extensions.js --> - -<bindings id="addonBindings" - xmlns="http://www.mozilla.org/xbl" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:xbl="http://www.mozilla.org/xbl"> - - - <!-- Rating - displays current/average rating, allows setting user rating --> - <binding id="rating"> - <content> - <xul:image class="star" - onmouseover="document.getBindingParent(this)._hover(1);" - onclick="document.getBindingParent(this).userRating = 1;"/> - <xul:image class="star" - onmouseover="document.getBindingParent(this)._hover(2);" - onclick="document.getBindingParent(this).userRating = 2;"/> - <xul:image class="star" - onmouseover="document.getBindingParent(this)._hover(3);" - onclick="document.getBindingParent(this).userRating = 3;"/> - <xul:image class="star" - onmouseover="document.getBindingParent(this)._hover(4);" - onclick="document.getBindingParent(this).userRating = 4;"/> - <xul:image class="star" - onmouseover="document.getBindingParent(this)._hover(5);" - onclick="document.getBindingParent(this).userRating = 5;"/> - </content> - - <implementation> - <constructor><![CDATA[ - this._updateStars(); - ]]></constructor> - - <property name="stars" readonly="true"> - <getter><![CDATA[ - return document.getAnonymousNodes(this); - ]]></getter> - </property> - - <property name="averageRating"> - <getter><![CDATA[ - if (this.hasAttribute("averagerating")) - return this.getAttribute("averagerating"); - return -1; - ]]></getter> - <setter><![CDATA[ - this.setAttribute("averagerating", val); - if (this.showRating == "average") - this._updateStars(); - ]]></setter> - </property> - - <property name="userRating"> - <getter><![CDATA[ - if (this.hasAttribute("userrating")) - return this.getAttribute("userrating"); - return -1; - ]]></getter> - <setter><![CDATA[ - if (this.showRating != "user") - return; - this.setAttribute("userrating", val); - if (this.showRating == "user") - this._updateStars(); - ]]></setter> - </property> - - <property name="showRating"> - <getter><![CDATA[ - if (this.hasAttribute("showrating")) - return this.getAttribute("showrating"); - return "average"; - ]]></getter> - <setter><![CDATA[ - if (val != "average" || val != "user") - throw Components.Exception("Invalid value", Components.results.NS_ERROR_ILLEGAL_VALUE); - this.setAttribute("showrating", val); - this._updateStars(); - ]]></setter> - </property> - - <method name="_updateStars"> - <body><![CDATA[ - var stars = this.stars; - var rating = this[this.showRating + "Rating"]; - // average ratings can be non-whole numbers, round them so they - // match to their closest star - rating = Math.round(rating); - for (let i = 0; i < stars.length; i++) - stars[i].setAttribute("on", rating > i); - ]]></body> - </method> - - <method name="_hover"> - <parameter name="aScore"/> - <body><![CDATA[ - if (this.showRating != "user") - return; - var stars = this.stars; - for (let i = 0; i < stars.length; i++) - stars[i].setAttribute("on", i <= (aScore -1)); - ]]></body> - </method> - - </implementation> - - <handlers> - <handler event="mouseout"> - this._updateStars(); - </handler> - </handlers> - </binding> - - <!-- Download progress - shows graphical progress of download and any - related status message. --> - <binding id="download-progress"> - <content> - <xul:stack flex="1"> - <xul:hbox flex="1"> - <xul:hbox class="start-cap"/> - <xul:progressmeter anonid="progress" class="progress" flex="1" - min="0" max="100"/> - <xul:hbox class="end-cap"/> - </xul:hbox> - <xul:hbox class="status-container"> - <xul:spacer flex="1"/> - <xul:label anonid="status" class="status"/> - <xul:spacer flex="1"/> - <xul:button anonid="cancel-btn" class="cancel" - tooltiptext="&progress.cancel.tooltip;" - oncommand="document.getBindingParent(this).cancel();"/> - </xul:hbox> - </xul:stack> - </content> - - <implementation> - <constructor><![CDATA[ - var progress = 0; - if (this.hasAttribute("progress")) - progress = parseInt(this.getAttribute("progress")); - this.progress = progress; - ]]></constructor> - - <field name="_progress"> - document.getAnonymousElementByAttribute(this, "anonid", "progress"); - </field> - <field name="_cancel"> - document.getAnonymousElementByAttribute(this, "anonid", "cancel-btn"); - </field> - <field name="_status"> - document.getAnonymousElementByAttribute(this, "anonid", "status"); - </field> - - <property name="progress"> - <getter><![CDATA[ - return this._progress.value; - ]]></getter> - <setter><![CDATA[ - this._progress.value = val; - if (val == this._progress.max) - this.setAttribute("complete", true); - else - this.removeAttribute("complete"); - ]]></setter> - </property> - - <property name="maxProgress"> - <getter><![CDATA[ - return this._progress.max; - ]]></getter> - <setter><![CDATA[ - if (val == -1) { - this._progress.mode = "undetermined"; - } else { - this._progress.mode = "determined"; - this._progress.max = val; - } - this.setAttribute("mode", this._progress.mode); - ]]></setter> - </property> - - <property name="status"> - <getter><![CDATA[ - return this._status.value; - ]]></getter> - <setter><![CDATA[ - this._status.value = val; - ]]></setter> - </property> - - <method name="cancel"> - <body><![CDATA[ - this.mInstall.cancel(); - ]]></body> - </method> - </implementation> - </binding> - - - <!-- Sorters - displays and controls the sort state of a list. --> - <binding id="sorters"> - <content orient="horizontal"> - <xul:button anonid="name-btn" class="sorter" - label="&sort.name.label;" tooltiptext="&sort.name.tooltip;" - oncommand="this.parentNode._handleChange('name');"/> - <xul:button anonid="date-btn" class="sorter" - label="&sort.dateUpdated.label;" - tooltiptext="&sort.dateUpdated.tooltip;" - oncommand="this.parentNode._handleChange('updateDate');"/> - <xul:button anonid="price-btn" class="sorter" hidden="true" - label="&sort.price.label;" - tooltiptext="&sort.price.tooltip;" - oncommand="this.parentNode._handleChange('purchaseAmount');"/> - <xul:button anonid="relevance-btn" class="sorter" hidden="true" - label="&sort.relevance.label;" - tooltiptext="&sort.relevance.tooltip;" - oncommand="this.parentNode._handleChange('relevancescore');"/> - </content> - - <implementation> - <constructor><![CDATA[ - if (!this.hasAttribute("sortby")) - this.setAttribute("sortby", "name"); - - if (this.getAttribute("showrelevance") == "true") - this._btnRelevance.hidden = false; - - if (this.getAttribute("showprice") == "true") - this._btnPrice.hidden = false; - - this._refreshState(); - ]]></constructor> - - <field name="handler">null</field> - <field name="_btnName"> - document.getAnonymousElementByAttribute(this, "anonid", "name-btn"); - </field> - <field name="_btnDate"> - document.getAnonymousElementByAttribute(this, "anonid", "date-btn"); - </field> - <field name="_btnPrice"> - document.getAnonymousElementByAttribute(this, "anonid", "price-btn"); - </field> - <field name="_btnRelevance"> - document.getAnonymousElementByAttribute(this, "anonid", "relevance-btn"); - </field> - - <property name="sortBy"> - <getter><![CDATA[ - return this.getAttribute("sortby"); - ]]></getter> - <setter><![CDATA[ - if (val != this.sortBy) { - this.setAttribute("sortBy", val); - this._refreshState(); - } - ]]></setter> - </property> - - <property name="ascending"> - <getter><![CDATA[ - return (this.getAttribute("ascending") == "true"); - ]]></getter> - <setter><![CDATA[ - val = !!val; - if (val != this.ascending) { - this.setAttribute("ascending", val); - this._refreshState(); - } - ]]></setter> - </property> - - <property name="showrelevance"> - <getter><![CDATA[ - return (this.getAttribute("showrelevance") == "true"); - ]]></getter> - <setter><![CDATA[ - val = !!val; - this.setAttribute("showrelevance", val); - this._btnRelevance.hidden = !val; - ]]></setter> - </property> - - <property name="showprice"> - <getter><![CDATA[ - return (this.getAttribute("showprice") == "true"); - ]]></getter> - <setter><![CDATA[ - val = !!val; - this.setAttribute("showprice", val); - this._btnPrice.hidden = !val; - ]]></setter> - </property> - - <method name="setSort"> - <parameter name="aSort"/> - <parameter name="aAscending"/> - <body><![CDATA[ - var sortChanged = false; - if (aSort != this.sortBy) { - this.setAttribute("sortby", aSort); - sortChanged = true; - } - - aAscending = !!aAscending; - if (this.ascending != aAscending) { - this.setAttribute("ascending", aAscending); - sortChanged = true; - } - - if (sortChanged) - this._refreshState(); - ]]></body> - </method> - - <method name="_handleChange"> - <parameter name="aSort"/> - <body><![CDATA[ - const ASCENDING_SORT_FIELDS = ["name", "purchaseAmount"]; - - // Toggle ascending if sort by is not changing, otherwise - // name sorting defaults to ascending, others to descending - if (aSort == this.sortBy) - this.ascending = !this.ascending; - else - this.setSort(aSort, ASCENDING_SORT_FIELDS.indexOf(aSort) >= 0); - ]]></body> - </method> - - <method name="_refreshState"> - <body><![CDATA[ - var sortBy = this.sortBy; - var checkState = this.ascending ? 2 : 1; - - if (sortBy == "name") { - this._btnName.checkState = checkState; - this._btnName.checked = true; - } else { - this._btnName.checkState = 0; - this._btnName.checked = false; - } - - if (sortBy == "updateDate") { - this._btnDate.checkState = checkState; - this._btnDate.checked = true; - } else { - this._btnDate.checkState = 0; - this._btnDate.checked = false; - } - - if (sortBy == "purchaseAmount") { - this._btnPrice.checkState = checkState; - this._btnPrice.checked = true; - } else { - this._btnPrice.checkState = 0; - this._btnPrice.checked = false; - } - - if (sortBy == "relevancescore") { - this._btnRelevance.checkState = checkState; - this._btnRelevance.checked = true; - } else { - this._btnRelevance.checkState = 0; - this._btnRelevance.checked = false; - } - - if (this.handler && "onSortChanged" in this.handler) - this.handler.onSortChanged(sortBy, this.ascending); - ]]></body> - </method> - </implementation> - </binding> - - - <!-- Categories list - displays the list of categories on the left pane. --> - <binding id="categories-list" - extends="chrome://global/content/bindings/richlistbox.xml#richlistbox"> - <implementation> - <!-- This needs to be overridden to allow the fancy animation while not - allowing that item to be selected when hiding. --> - <method name="_canUserSelect"> - <parameter name="aItem"/> - <body> - <![CDATA[ - if (aItem.hasAttribute("disabled") && - aItem.getAttribute("disabled") == "true") - return false; - var style = document.defaultView.getComputedStyle(aItem, ""); - return style.display != "none" && style.visibility == "visible"; - ]]> - </body> - </method> - </implementation> - </binding> - - - <!-- Category item - an item in the category list. --> - <binding id="category" - extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> - <content align="center"> - <xul:image anonid="icon" class="category-icon"/> - <xul:label anonid="name" class="category-name" flex="1" xbl:inherits="value=name"/> - <xul:label anonid="badge" class="category-badge" xbl:inherits="value=count"/> - </content> - - <implementation> - <constructor><![CDATA[ - if (!this.hasAttribute("count")) - this.setAttribute("count", 0); - ]]></constructor> - - <property name="badgeCount"> - <getter><![CDATA[ - return this.getAttribute("count"); - ]]></getter> - <setter><![CDATA[ - if (this.getAttribute("count") == val) - return; - - this.setAttribute("count", val); - var event = document.createEvent("Events"); - event.initEvent("CategoryBadgeUpdated", true, true); - this.dispatchEvent(event); - ]]></setter> - </property> - </implementation> - </binding> - - - <!-- Creator link - Name of a user/developer, providing a link if relevant. --> - <binding id="creator-link"> - <content> - <xul:label anonid="label" value="&addon.createdBy.label;"/> - <xul:label anonid="creator-link" class="creator-link text-link"/> - <xul:label anonid="creator-name" class="creator-name"/> - </content> - - <implementation> - <constructor><![CDATA[ - if (this.hasAttribute("nameonly") && - this.getAttribute("nameonly") == "true") { - this._label.hidden = true; - } - ]]></constructor> - - <field name="_label"> - document.getAnonymousElementByAttribute(this, "anonid", "label"); - </field> - <field name="_creatorLink"> - document.getAnonymousElementByAttribute(this, "anonid", "creator-link"); - </field> - <field name="_creatorName"> - document.getAnonymousElementByAttribute(this, "anonid", "creator-name"); - </field> - - <method name="setCreator"> - <parameter name="aCreator"/> - <parameter name="aHomepageURL"/> - <body><![CDATA[ - if (!aCreator) { - this.collapsed = true; - return; - } - this.collapsed = false; - var url = aCreator.url || aHomepageURL; - var showLink = !!url; - if (showLink) { - this._creatorLink.value = aCreator.name; - this._creatorLink.href = url; - } else { - this._creatorName.value = aCreator.name; - } - this._creatorLink.hidden = !showLink; - this._creatorName.hidden = showLink; - ]]></body> - </method> - </implementation> - </binding> - - - <!-- Install status - Displays the status of an install/upgrade. --> - <binding id="install-status"> - <content> - <xul:label anonid="message"/> - <xul:progressmeter anonid="progress" class="download-progress"/> - <xul:button anonid="purchase-remote-btn" hidden="true" - class="addon-control" - oncommand="document.getBindingParent(this).purchaseRemote();"/> - <xul:button anonid="install-remote-btn" hidden="true" - class="addon-control install" label="&addon.install.label;" - tooltiptext="&addon.install.tooltip;" - oncommand="document.getBindingParent(this).installRemote();"/> - </content> - - <implementation> - <constructor><![CDATA[ - if (this.mInstall) - this.initWithInstall(this.mInstall); - else if (this.mControl.mAddon.install) - this.initWithInstall(this.mControl.mAddon.install); - else - this.refreshState(); - ]]></constructor> - - <destructor><![CDATA[ - if (this.mInstall) - this.mInstall.removeListener(this); - ]]></destructor> - - <field name="_message"> - document.getAnonymousElementByAttribute(this, "anonid", "message"); - </field> - <field name="_progress"> - document.getAnonymousElementByAttribute(this, "anonid", "progress"); - </field> - <field name="_purchaseRemote"> - document.getAnonymousElementByAttribute(this, "anonid", - "purchase-remote-btn"); - </field> - <field name="_installRemote"> - document.getAnonymousElementByAttribute(this, "anonid", - "install-remote-btn"); - </field> - <field name="_restartNeeded"> - document.getAnonymousElementByAttribute(this, "anonid", - "restart-needed"); - </field> - <field name="_undo"> - document.getAnonymousElementByAttribute(this, "anonid", - "undo-btn"); - </field> - - <method name="initWithInstall"> - <parameter name="aInstall"/> - <body><![CDATA[ - if (this.mInstall) { - this.mInstall.removeListener(this); - this.mInstall = null; - } - this.mInstall = aInstall; - this._progress.mInstall = aInstall; - this.refreshState(); - this.mInstall.addListener(this); - ]]></body> - </method> - - <method name="refreshState"> - <body><![CDATA[ - var showInstallRemote = false; - var showPurchase = false; - - if (this.mInstall) { - - switch (this.mInstall.state) { - case AddonManager.STATE_AVAILABLE: - if (this.mControl.getAttribute("remote") != "true") - break; - - this._progress.hidden = true; - showInstallRemote = true; - break; - case AddonManager.STATE_DOWNLOADING: - this.showMessage("installDownloading"); - break; - case AddonManager.STATE_CHECKING: - this.showMessage("installVerifying"); - break; - case AddonManager.STATE_DOWNLOADED: - this.showMessage("installDownloaded"); - break; - case AddonManager.STATE_DOWNLOAD_FAILED: - // XXXunf expose what error occured (bug 553487) - this.showMessage("installDownloadFailed", true); - break; - case AddonManager.STATE_INSTALLING: - this.showMessage("installInstalling"); - break; - case AddonManager.STATE_INSTALL_FAILED: - // XXXunf expose what error occured (bug 553487) - this.showMessage("installFailed", true); - break; - case AddonManager.STATE_CANCELLED: - this.showMessage("installCancelled", true); - break; - } - - } else if (this.mControl.mAddon.purchaseURL) { - this._progress.hidden = true; - showPurchase = true; - this._purchaseRemote.label = - gStrings.ext.formatStringFromName("addon.purchase.label", - [this.mControl.mAddon.purchaseDisplayAmount], 1); - this._purchaseRemote.tooltiptext = - gStrings.ext.GetStringFromName("addon.purchase.tooltip"); - } - - this._purchaseRemote.hidden = !showPurchase; - this._installRemote.hidden = !showInstallRemote; - - if ("refreshInfo" in this.mControl) - this.mControl.refreshInfo(); - ]]></body> - </method> - - <method name="showMessage"> - <parameter name="aMsgId"/> - <parameter name="aHideProgress"/> - <body><![CDATA[ - this._message.setAttribute("hidden", !aHideProgress); - this._progress.setAttribute("hidden", !!aHideProgress); - - var msg = gStrings.ext.GetStringFromName(aMsgId); - if (aHideProgress) - this._message.value = msg; - else - this._progress.status = msg; - ]]></body> - </method> - - <method name="purchaseRemote"> - <body><![CDATA[ - openURL(this.mControl.mAddon.purchaseURL); - ]]></body> - </method> - - <method name="installRemote"> - <body><![CDATA[ - if (this.mControl.getAttribute("remote") != "true") - return; - - if (this.mControl.mAddon.eula) { - var data = { - addon: this.mControl.mAddon, - accepted: false - }; - window.openDialog("chrome://mozapps/content/extensions/eula.xul", "_blank", - "chrome,dialog,modal,centerscreen,resizable=no", data); - if (!data.accepted) - return; - } - - delete this.mControl.mAddon; - this.mControl.mInstall = this.mInstall; - this.mControl.setAttribute("status", "installing"); - this.mInstall.install(); - ]]></body> - </method> - - <method name="undoAction"> - <body><![CDATA[ - if (!this.mAddon) - return; - var pending = this.mAddon.pendingOperations; - if (pending & AddonManager.PENDING_ENABLE) - this.mAddon.userDisabled = true; - else if (pending & AddonManager.PENDING_DISABLE) - this.mAddon.userDisabled = false; - this.refreshState(); - ]]></body> - </method> - - <method name="onDownloadStarted"> - <body><![CDATA[ - this.refreshState(); - ]]></body> - </method> - - <method name="onDownloadEnded"> - <body><![CDATA[ - this.refreshState(); - ]]></body> - </method> - - <method name="onDownloadFailed"> - <body><![CDATA[ - this.refreshState(); - ]]></body> - </method> - - <method name="onDownloadProgress"> - <body><![CDATA[ - this._progress.maxProgress = this.mInstall.maxProgress; - this._progress.progress = this.mInstall.progress; - ]]></body> - </method> - - <method name="onInstallStarted"> - <body><![CDATA[ - this._progress.progress = 0; - this.refreshState(); - ]]></body> - </method> - - <method name="onInstallEnded"> - <body><![CDATA[ - this.refreshState(); - if ("onInstallCompleted" in this.mControl) - this.mControl.onInstallCompleted(); - ]]></body> - </method> - - <method name="onInstallFailed"> - <body><![CDATA[ - this.refreshState(); - ]]></body> - </method> - </implementation> - </binding> - - - <!-- Addon - base - parent binding of any item representing an addon. --> - <binding id="addon-base" - extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> - <implementation> - <method name="hasPermission"> - <parameter name="aPerm"/> - <body><![CDATA[ - var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()]; - return !!(this.mAddon.permissions & perm); - ]]></body> - </method> - - <method name="opRequiresRestart"> - <parameter name="aOperation"/> - <body><![CDATA[ - var operation = AddonManager["OP_NEEDS_RESTART_" + aOperation.toUpperCase()]; - return !!(this.mAddon.operationsRequiringRestart & operation); - ]]></body> - </method> - - <method name="isPending"> - <parameter name="aAction"/> - <body><![CDATA[ - var action = AddonManager["PENDING_" + aAction.toUpperCase()]; - return !!(this.mAddon.pendingOperations & action); - ]]></body> - </method> - - <method name="typeHasFlag"> - <parameter name="aFlag"/> - <body><![CDATA[ - let flag = AddonManager["TYPE_" + aFlag]; - let type = AddonManager.addonTypes[this.mAddon.type]; - - return !!(type.flags & flag); - ]]></body> - </method> - - <method name="onUninstalled"> - <body><![CDATA[ - this.parentNode.removeChild(this); - ]]></body> - </method> - </implementation> - </binding> - - - <!-- Addon - generic - A normal addon item, or an update to one --> - <binding id="addon-generic" - extends="chrome://mozapps/content/extensions/extensions.xml#addon-base"> - <content> - <xul:hbox anonid="warning-container" - class="warning"> - <xul:image class="warning-icon"/> - <xul:label anonid="warning" flex="1"/> - <xul:label anonid="warning-link" class="text-link"/> - <xul:button anonid="warning-btn" class="button-link" hidden="true"/> - <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </xul:hbox> - <xul:hbox anonid="error-container" - class="error"> - <xul:image class="error-icon"/> - <xul:label anonid="error" flex="1"/> - <xul:label anonid="error-link" class="text-link" hidden="true"/> - <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </xul:hbox> - <xul:hbox anonid="pending-container" - class="pending"> - <xul:image class="pending-icon"/> - <xul:label anonid="pending" flex="1"/> - <xul:button anonid="restart-btn" class="button-link" - label="&addon.restartNow.label;" - oncommand="document.getBindingParent(this).restart();"/> - <xul:button anonid="undo-btn" class="button-link" - label="&addon.undoAction.label;" - tooltipText="&addon.undoAction.tooltip;" - oncommand="document.getBindingParent(this).undo();"/> - <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </xul:hbox> - - <xul:hbox class="content-container" align="center"> - <xul:vbox class="icon-container"> - <xul:image anonid="icon" class="icon"/> - </xul:vbox> - <xul:vbox class="content-inner-container" flex="1"> - <xul:hbox class="basicinfo-container"> - <xul:hbox class="name-container"> - <xul:label anonid="name" class="name" crop="end" flex="1" - tooltip="addonitem-tooltip" xbl:inherits="value=name"/> - <xul:label class="disabled-postfix" value="&addon.disabled.postfix;"/> - <xul:label class="update-postfix" value="&addon.update.postfix;"/> - <xul:spacer flex="5000"/> <!-- Necessary to make the name crop --> - </xul:hbox> - <xul:label anonid="date-updated" class="date-updated" - unknown="&addon.unknownDate;"/> - </xul:hbox> - <xul:hbox class="experiment-container"> - <svg width="6" height="6" viewBox="0 0 6 6" version="1.1" - xmlns="http://www.w3.org/2000/svg" - class="experiment-bullet-container"> - <circle cx="3" cy="3" r="3" class="experiment-bullet"/> - </svg> - <xul:label anonid="experiment-state" class="experiment-state"/> - <xul:label anonid="experiment-time" class="experiment-time"/> - </xul:hbox> - - <xul:hbox class="advancedinfo-container" flex="1"> - <xul:vbox class="description-outer-container" flex="1"> - <xul:hbox class="description-container"> - <xul:label anonid="description" class="description" crop="end" flex="1"/> - <xul:button anonid="details-btn" class="details button-link" - label="&addon.details.label;" - tooltiptext="&addon.details.tooltip;" - oncommand="document.getBindingParent(this).showInDetailView();"/> - <xul:spacer flex="5000"/> <!-- Necessary to make the description crop --> - </xul:hbox> - <xul:vbox anonid="relnotes-container" class="relnotes-container"> - <xul:label class="relnotes-header" value="&addon.releaseNotes.label;"/> - <xul:label anonid="relnotes-loading" value="&addon.loadingReleaseNotes.label;"/> - <xul:label anonid="relnotes-error" hidden="true" - value="&addon.errorLoadingReleaseNotes.label;"/> - <xul:vbox anonid="relnotes" class="relnotes"/> - </xul:vbox> - <xul:hbox class="relnotes-toggle-container"> - <xul:button anonid="relnotes-toggle-btn" class="relnotes-toggle" - hidden="true" label="&cmd.showReleaseNotes.label;" - tooltiptext="&cmd.showReleaseNotes.tooltip;" - showlabel="&cmd.showReleaseNotes.label;" - showtooltip="&cmd.showReleaseNotes.tooltip;" - hidelabel="&cmd.hideReleaseNotes.label;" - hidetooltip="&cmd.hideReleaseNotes.tooltip;" - oncommand="document.getBindingParent(this).toggleReleaseNotes();"/> - </xul:hbox> - </xul:vbox> - </xul:hbox> - </xul:vbox> - <xul:vbox class="status-control-wrapper"> - <xul:hbox class="status-container"> - <xul:hbox anonid="checking-update" hidden="true"> - <xul:image class="spinner"/> - <xul:label value="&addon.checkingForUpdates.label;"/> - </xul:hbox> - <xul:vbox anonid="update-available" class="update-available" - hidden="true"> - <xul:checkbox anonid="include-update" class="include-update" - label="&addon.includeUpdate.label;" checked="true" - oncommand="document.getBindingParent(this).onIncludeUpdateChanged();"/> - <xul:hbox class="update-info-container"> - <xul:label class="update-available-notice" - value="&addon.updateAvailable.label;"/> - <xul:button anonid="update-btn" class="addon-control update" - label="&addon.updateNow.label;" - tooltiptext="&addon.updateNow.tooltip;" - oncommand="document.getBindingParent(this).upgrade();"/> - </xul:hbox> - </xul:vbox> - <xul:hbox anonid="install-status" class="install-status" - hidden="true"/> - </xul:hbox> - <xul:hbox anonid="control-container" class="control-container"> - <xul:button anonid="preferences-btn" - class="addon-control preferences" -#ifdef XP_WIN - label="&cmd.showPreferencesWin.label;" - tooltiptext="&cmd.showPreferencesWin.tooltip;" -#else - label="&cmd.showPreferencesUnix.label;" - tooltiptext="&cmd.showPreferencesUnix.tooltip;" -#endif - oncommand="document.getBindingParent(this).showPreferences();"/> - <xul:button anonid="enable-btn" class="addon-control enable" - label="&cmd.enableAddon.label;" - oncommand="document.getBindingParent(this).userDisabled = false;"/> - <xul:button anonid="disable-btn" class="addon-control disable" - label="&cmd.disableAddon.label;" - oncommand="document.getBindingParent(this).userDisabled = true;"/> - <xul:button anonid="remove-btn" class="addon-control remove" - label="&cmd.uninstallAddon.label;" - oncommand="document.getBindingParent(this).uninstall();"/> - <xul:menulist anonid="state-menulist" - class="addon-control state" - tooltiptext="&cmd.stateMenu.tooltip;"> - <xul:menupopup> - <xul:menuitem anonid="ask-to-activate-menuitem" - class="addon-control" - label="&cmd.askToActivate.label;" - tooltiptext="&cmd.askToActivate.tooltip;" - oncommand="document.getBindingParent(this).userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;"/> - <xul:menuitem anonid="always-activate-menuitem" - class="addon-control" - label="&cmd.alwaysActivate.label;" - tooltiptext="&cmd.alwaysActivate.tooltip;" - oncommand="document.getBindingParent(this).userDisabled = false;"/> - <xul:menuitem anonid="never-activate-menuitem" - class="addon-control" - label="&cmd.neverActivate.label;" - tooltiptext="&cmd.neverActivate.tooltip;" - oncommand="document.getBindingParent(this).userDisabled = true;"/> - </xul:menupopup> - </xul:menulist> - </xul:hbox> - </xul:vbox> - </xul:hbox> - </content> - - <implementation> - <constructor><![CDATA[ - this._installStatus = document.getAnonymousElementByAttribute(this, "anonid", "install-status"); - this._installStatus.mControl = this; - - this.setAttribute("contextmenu", "addonitem-popup"); - - this._showStatus("none"); - - this._initWithAddon(this.mAddon); - - gEventManager.registerAddonListener(this, this.mAddon.id); - ]]></constructor> - - <destructor><![CDATA[ - gEventManager.unregisterAddonListener(this, this.mAddon.id); - ]]></destructor> - - <field name="_warningContainer"> - document.getAnonymousElementByAttribute(this, "anonid", - "warning-container"); - </field> - <field name="_warning"> - document.getAnonymousElementByAttribute(this, "anonid", - "warning"); - </field> - <field name="_warningLink"> - document.getAnonymousElementByAttribute(this, "anonid", - "warning-link"); - </field> - <field name="_warningBtn"> - document.getAnonymousElementByAttribute(this, "anonid", - "warning-btn"); - </field> - <field name="_errorContainer"> - document.getAnonymousElementByAttribute(this, "anonid", - "error-container"); - </field> - <field name="_error"> - document.getAnonymousElementByAttribute(this, "anonid", - "error"); - </field> - <field name="_errorLink"> - document.getAnonymousElementByAttribute(this, "anonid", - "error-link"); - </field> - <field name="_pendingContainer"> - document.getAnonymousElementByAttribute(this, "anonid", - "pending-container"); - </field> - <field name="_pending"> - document.getAnonymousElementByAttribute(this, "anonid", - "pending"); - </field> - <field name="_infoContainer"> - document.getAnonymousElementByAttribute(this, "anonid", - "info-container"); - </field> - <field name="_info"> - document.getAnonymousElementByAttribute(this, "anonid", - "info"); - </field> - <field name="_experimentState"> - document.getAnonymousElementByAttribute(this, "anonid", "experiment-state"); - </field> - <field name="_experimentTime"> - document.getAnonymousElementByAttribute(this, "anonid", "experiment-time"); - </field> - <field name="_icon"> - document.getAnonymousElementByAttribute(this, "anonid", "icon"); - </field> - <field name="_dateUpdated"> - document.getAnonymousElementByAttribute(this, "anonid", - "date-updated"); - </field> - <field name="_description"> - document.getAnonymousElementByAttribute(this, "anonid", - "description"); - </field> - <field name="_stateMenulist"> - document.getAnonymousElementByAttribute(this, "anonid", - "state-menulist"); - </field> - <field name="_askToActivateMenuitem"> - document.getAnonymousElementByAttribute(this, "anonid", - "ask-to-activate-menuitem"); - </field> - <field name="_alwaysActivateMenuitem"> - document.getAnonymousElementByAttribute(this, "anonid", - "always-activate-menuitem"); - </field> - <field name="_neverActivateMenuitem"> - document.getAnonymousElementByAttribute(this, "anonid", - "never-activate-menuitem"); - </field> - <field name="_preferencesBtn"> - document.getAnonymousElementByAttribute(this, "anonid", - "preferences-btn"); - </field> - <field name="_enableBtn"> - document.getAnonymousElementByAttribute(this, "anonid", - "enable-btn"); - </field> - <field name="_disableBtn"> - document.getAnonymousElementByAttribute(this, "anonid", - "disable-btn"); - </field> - <field name="_removeBtn"> - document.getAnonymousElementByAttribute(this, "anonid", - "remove-btn"); - </field> - <field name="_updateBtn"> - document.getAnonymousElementByAttribute(this, "anonid", - "update-btn"); - </field> - <field name="_controlContainer"> - document.getAnonymousElementByAttribute(this, "anonid", - "control-container"); - </field> - <field name="_installStatus"> - document.getAnonymousElementByAttribute(this, "anonid", - "install-status"); - </field> - <field name="_checkingUpdate"> - document.getAnonymousElementByAttribute(this, "anonid", - "checking-update"); - </field> - <field name="_updateAvailable"> - document.getAnonymousElementByAttribute(this, "anonid", - "update-available"); - </field> - <field name="_includeUpdate"> - document.getAnonymousElementByAttribute(this, "anonid", - "include-update"); - </field> - <field name="_relNotesLoaded">false</field> - <field name="_relNotesToggle"> - document.getAnonymousElementByAttribute(this, "anonid", - "relnotes-toggle-btn"); - </field> - <field name="_relNotesLoading"> - document.getAnonymousElementByAttribute(this, "anonid", - "relnotes-loading"); - </field> - <field name="_relNotesError"> - document.getAnonymousElementByAttribute(this, "anonid", - "relnotes-error"); - </field> - <field name="_relNotesContainer"> - document.getAnonymousElementByAttribute(this, "anonid", - "relnotes-container"); - </field> - <field name="_relNotes"> - document.getAnonymousElementByAttribute(this, "anonid", - "relnotes"); - </field> - - <property name="userDisabled"> - <getter><![CDATA[ - return this.mAddon.userDisabled; - ]]></getter> - <setter><![CDATA[ - this.mAddon.userDisabled = val; - ]]></setter> - </property> - - <property name="includeUpdate"> - <getter><![CDATA[ - return this._includeUpdate.checked && !!this.mManualUpdate; - ]]></getter> - <setter><![CDATA[ - // XXXunf Eventually, we'll want to persist this for individual - // updates - see bug 594619. - this._includeUpdate.checked = !!val; - ]]></setter> - </property> - - <method name="_initWithAddon"> - <parameter name="aAddon"/> - <body><![CDATA[ - this.mAddon = aAddon; - - this._installStatus.mAddon = this.mAddon; - this._updateDates(); - this._updateState(); - - this.setAttribute("name", aAddon.name); - - var iconURL = AddonManager.getPreferredIconURL(aAddon, 48, window); - if (iconURL) - this._icon.src = iconURL; - else - this._icon.src = ""; - - if (this.mAddon.description) - this._description.value = this.mAddon.description; - else - this._description.hidden = true; - - if (!("applyBackgroundUpdates" in this.mAddon) || - (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE || - (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT && - !AddonManager.autoUpdateDefault))) { - AddonManager.getAllInstalls(aInstallsList => { - // This can return after the binding has been destroyed, - // so try to detect that and return early - if (!("onNewInstall" in this)) - return; - for (let install of aInstallsList) { - if (install.existingAddon && - install.existingAddon.id == this.mAddon.id && - install.state == AddonManager.STATE_AVAILABLE) { - this.onNewInstall(install); - this.onIncludeUpdateChanged(); - } - } - }); - } - ]]></body> - </method> - - <method name="_showStatus"> - <parameter name="aType"/> - <body><![CDATA[ - this._controlContainer.hidden = aType != "none" && - !(aType == "update-available" && !this.hasAttribute("upgrade")); - - this._installStatus.hidden = aType != "progress"; - if (aType == "progress") - this._installStatus.refreshState(); - this._checkingUpdate.hidden = aType != "checking-update"; - this._updateAvailable.hidden = aType != "update-available"; - this._relNotesToggle.hidden = !(this.mManualUpdate ? - this.mManualUpdate.releaseNotesURI : - this.mAddon.releaseNotesURI); - ]]></body> - </method> - - <method name="_updateDates"> - <body><![CDATA[ - function formatDate(aDate) { - const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"] - .getService(Components.interfaces.nsIXULChromeRegistry) - .getSelectedLocale("global", true); - const dtOptions = { year: 'numeric', month: 'long', day: 'numeric' }; - return aDate.toLocaleDateString(locale, dtOptions); - } - - if (this.mAddon.updateDate) - this._dateUpdated.value = formatDate(this.mAddon.updateDate); - else - this._dateUpdated.value = this._dateUpdated.getAttribute("unknown"); - ]]></body> - </method> - - <method name="_updateState"> - <body><![CDATA[ - if (this.parentNode.selectedItem == this) - gViewController.updateCommands(); - - var pending = this.mAddon.pendingOperations; - if (pending != AddonManager.PENDING_NONE) { - this.removeAttribute("notification"); - - pending = null; - const PENDING_OPERATIONS = ["enable", "disable", "install", - "uninstall", "upgrade"]; - for (let op of PENDING_OPERATIONS) { - if (this.isPending(op)) - pending = op; - } - - this.setAttribute("pending", pending); - this._pending.textContent = gStrings.ext.formatStringFromName( - "notification." + pending, - [this.mAddon.name, gStrings.brandShortName], 2 - ); - } else { - this.removeAttribute("pending"); - - var isUpgrade = this.hasAttribute("upgrade"); - var install = this._installStatus.mInstall; - - if (install && install.state == AddonManager.STATE_DOWNLOAD_FAILED) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.downloadError", - [this.mAddon.name], 1 - ); - this._warningBtn.label = gStrings.ext.GetStringFromName("notification.downloadError.retry"); - this._warningBtn.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip"); - this._warningBtn.setAttribute("oncommand", "document.getBindingParent(this).retryInstall();"); - this._warningBtn.hidden = false; - this._warningLink.hidden = true; - } else if (install && install.state == AddonManager.STATE_INSTALL_FAILED) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.installError", - [this.mAddon.name], 1 - ); - this._warningBtn.label = gStrings.ext.GetStringFromName("notification.installError.retry"); - this._warningBtn.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip"); - this._warningBtn.setAttribute("oncommand", "document.getBindingParent(this).retryInstall();"); - this._warningBtn.hidden = false; - this._warningLink.hidden = true; - } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { - this.setAttribute("notification", "error"); - this._error.textContent = gStrings.ext.formatStringFromName( - "notification.blocked", - [this.mAddon.name], 1 - ); - this._errorLink.value = gStrings.ext.GetStringFromName("notification.blocked.link"); - this._errorLink.href = this.mAddon.blocklistURL; - this._errorLink.hidden = false; - } else if (!isUpgrade && !isCorrectlySigned(this.mAddon) && SIGNING_REQUIRED) { - this.setAttribute("notification", "error"); - this._error.textContent = gStrings.ext.formatStringFromName( - "notification.unsignedAndDisabled", [this.mAddon.name, gStrings.brandShortName], 2 - ); - this._errorLink.value = gStrings.ext.GetStringFromName("notification.unsigned.link"); - this._errorLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; - this._errorLink.hidden = false; - } else if ((!isUpgrade && !this.mAddon.isCompatible) && (AddonManager.checkCompatibility - || (this.mAddon.blocklistState != Ci.nsIBlocklistService.STATE_SOFTBLOCKED))) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.incompatible", - [this.mAddon.name, gStrings.brandShortName, gStrings.appVersion], 3 - ); - this._warningLink.hidden = true; - this._warningBtn.hidden = true; - } else if (!isUpgrade && !isCorrectlySigned(this.mAddon)) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.unsigned", [this.mAddon.name, gStrings.brandShortName], 2 - ); - this._warningLink.value = gStrings.ext.GetStringFromName("notification.unsigned.link"); - this._warningLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; - this._warningLink.hidden = false; - } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.softblocked", - [this.mAddon.name], 1 - ); - this._warningLink.value = gStrings.ext.GetStringFromName("notification.softblocked.link"); - this._warningLink.href = this.mAddon.blocklistURL; - this._warningLink.hidden = false; - this._warningBtn.hidden = true; - } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.outdated", - [this.mAddon.name], 1 - ); - this._warningLink.value = gStrings.ext.GetStringFromName("notification.outdated.link"); - this._warningLink.href = this.mAddon.blocklistURL; - this._warningLink.hidden = false; - this._warningBtn.hidden = true; - } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) { - this.setAttribute("notification", "error"); - this._error.textContent = gStrings.ext.formatStringFromName( - "notification.vulnerableUpdatable", - [this.mAddon.name], 1 - ); - this._errorLink.value = gStrings.ext.GetStringFromName("notification.vulnerableUpdatable.link"); - this._errorLink.href = this.mAddon.blocklistURL; - this._errorLink.hidden = false; - } else if (!isUpgrade && this.mAddon.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) { - this.setAttribute("notification", "error"); - this._error.textContent = gStrings.ext.formatStringFromName( - "notification.vulnerableNoUpdate", - [this.mAddon.name], 1 - ); - this._errorLink.value = gStrings.ext.GetStringFromName("notification.vulnerableNoUpdate.link"); - this._errorLink.href = this.mAddon.blocklistURL; - this._errorLink.hidden = false; - } else if (this.mAddon.isGMPlugin && !this.mAddon.isInstalled && - this.mAddon.isActive) { - this.setAttribute("notification", "warning"); - this._warning.textContent = - gStrings.ext.formatStringFromName("notification.gmpPending", - [this.mAddon.name], 1); - } else { - this.removeAttribute("notification"); - } - } - - this._preferencesBtn.hidden = (!this.mAddon.optionsURL) || - this.mAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO; - - if (this.typeHasFlag("SUPPORTS_ASK_TO_ACTIVATE")) { - this._enableBtn.disabled = true; - this._disableBtn.disabled = true; - this._askToActivateMenuitem.disabled = !this.hasPermission("ask_to_activate"); - this._alwaysActivateMenuitem.disabled = !this.hasPermission("enable"); - this._neverActivateMenuitem.disabled = !this.hasPermission("disable"); - if (!this.mAddon.isActive) { - this._stateMenulist.selectedItem = this._neverActivateMenuitem; - } else if (this.mAddon.userDisabled == AddonManager.STATE_ASK_TO_ACTIVATE) { - this._stateMenulist.selectedItem = this._askToActivateMenuitem; - } else { - this._stateMenulist.selectedItem = this._alwaysActivateMenuitem; - } - let hasActivatePermission = - ["ask_to_activate", "enable", "disable"].some(perm => this.hasPermission(perm)); - this._stateMenulist.disabled = !hasActivatePermission; - this._stateMenulist.hidden = false; - this._stateMenulist.classList.add('no-auto-hide'); - } else { - this._stateMenulist.hidden = true; - - let enableTooltip = gViewController.commands["cmd_enableItem"] - .getTooltip(this.mAddon); - this._enableBtn.setAttribute("tooltiptext", enableTooltip); - if (this.hasPermission("enable")) { - this._enableBtn.hidden = false; - } else { - this._enableBtn.hidden = true; - } - - let disableTooltip = gViewController.commands["cmd_disableItem"] - .getTooltip(this.mAddon); - this._disableBtn.setAttribute("tooltiptext", disableTooltip); - if (this.hasPermission("disable")) { - this._disableBtn.hidden = false; - } else { - this._disableBtn.hidden = true; - } - } - - let uninstallTooltip = gViewController.commands["cmd_uninstallItem"] - .getTooltip(this.mAddon); - this._removeBtn.setAttribute("tooltiptext", uninstallTooltip); - if (this.hasPermission("uninstall")) { - this._removeBtn.hidden = false; - } else { - this._removeBtn.hidden = true; - } - - this.setAttribute("active", this.mAddon.isActive); - - var showProgress = this.mAddon.purchaseURL || (this.mAddon.install && - this.mAddon.install.state != AddonManager.STATE_INSTALLED); - this._showStatus(showProgress ? "progress" : "none"); - - if (this.mAddon.type == "experiment") { - this.removeAttribute("notification"); - let prefix = "experiment."; - let active = this.mAddon.isActive; - - if (!showProgress) { - let stateKey = prefix + "state." + (active ? "active" : "complete"); - this._experimentState.value = gStrings.ext.GetStringFromName(stateKey); - - let now = Date.now(); - let end = this.endDate; - let days = Math.abs(end - now) / (24 * 60 * 60 * 1000); - - let timeKey = prefix + "time."; - let timeMessage; - - if (days < 1) { - timeKey += (active ? "endsToday" : "endedToday"); - timeMessage = gStrings.ext.GetStringFromName(timeKey); - } else { - timeKey += (active ? "daysRemaining" : "daysPassed"); - days = Math.round(days); - let timeString = gStrings.ext.GetStringFromName(timeKey); - timeMessage = PluralForm.get(days, timeString) - .replace("#1", days); - } - - this._experimentTime.value = timeMessage; - } - } - ]]></body> - </method> - - <method name="_fetchReleaseNotes"> - <parameter name="aURI"/> - <body><![CDATA[ - if (!aURI || this._relNotesLoaded) { - sendToggleEvent(); - return; - } - - var relNotesData = null, transformData = null; - - this._relNotesLoaded = true; - this._relNotesLoading.hidden = false; - this._relNotesError.hidden = true; - - let sendToggleEvent = () => { - var event = document.createEvent("Events"); - event.initEvent("RelNotesToggle", true, true); - this.dispatchEvent(event); - } - - let showRelNotes = () => { - if (!relNotesData || !transformData) - return; - - this._relNotesLoading.hidden = true; - - var processor = Components.classes["@mozilla.org/document-transformer;1?type=xslt"] - .createInstance(Components.interfaces.nsIXSLTProcessor); - processor.flags |= Components.interfaces.nsIXSLTProcessorPrivate.DISABLE_ALL_LOADS; - - processor.importStylesheet(transformData); - var fragment = processor.transformToFragment(relNotesData, document); - this._relNotes.appendChild(fragment); - if (this.hasAttribute("show-relnotes")) { - var container = this._relNotesContainer; - container.style.height = container.scrollHeight + "px"; - } - sendToggleEvent(); - } - - let handleError = () => { - dataReq.abort(); - styleReq.abort(); - this._relNotesLoading.hidden = true; - this._relNotesError.hidden = false; - this._relNotesLoaded = false; // allow loading to be re-tried - sendToggleEvent(); - } - - function handleResponse(aEvent) { - var req = aEvent.target; - var ct = req.getResponseHeader("content-type"); - if ((!ct || ct.indexOf("text/html") < 0) && - req.responseXML && - req.responseXML.documentElement.namespaceURI != XMLURI_PARSE_ERROR) { - if (req == dataReq) - relNotesData = req.responseXML; - else - transformData = req.responseXML; - showRelNotes(); - } else { - handleError(); - } - } - - var dataReq = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] - .createInstance(Components.interfaces.nsIXMLHttpRequest); - dataReq.open("GET", aURI.spec, true); - dataReq.responseType = "document"; - dataReq.addEventListener("load", handleResponse, false); - dataReq.addEventListener("error", handleError, false); - dataReq.send(null); - - var styleReq = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] - .createInstance(Components.interfaces.nsIXMLHttpRequest); - styleReq.open("GET", UPDATES_RELEASENOTES_TRANSFORMFILE, true); - styleReq.responseType = "document"; - styleReq.addEventListener("load", handleResponse, false); - styleReq.addEventListener("error", handleError, false); - styleReq.send(null); - ]]></body> - </method> - - <method name="toggleReleaseNotes"> - <body><![CDATA[ - if (this.hasAttribute("show-relnotes")) { - this._relNotesContainer.style.height = "0px"; - this.removeAttribute("show-relnotes"); - this._relNotesToggle.setAttribute( - "label", - this._relNotesToggle.getAttribute("showlabel") - ); - this._relNotesToggle.setAttribute( - "tooltiptext", - this._relNotesToggle.getAttribute("showtooltip") - ); - var event = document.createEvent("Events"); - event.initEvent("RelNotesToggle", true, true); - this.dispatchEvent(event); - } else { - this._relNotesContainer.style.height = this._relNotesContainer.scrollHeight + - "px"; - this.setAttribute("show-relnotes", true); - this._relNotesToggle.setAttribute( - "label", - this._relNotesToggle.getAttribute("hidelabel") - ); - this._relNotesToggle.setAttribute( - "tooltiptext", - this._relNotesToggle.getAttribute("hidetooltip") - ); - var uri = this.mManualUpdate ? - this.mManualUpdate.releaseNotesURI : - this.mAddon.releaseNotesURI; - this._fetchReleaseNotes(uri); - } - ]]></body> - </method> - - <method name="restart"> - <body><![CDATA[ - gViewController.commands["cmd_restartApp"].doCommand(); - ]]></body> - </method> - - <method name="undo"> - <body><![CDATA[ - gViewController.commands["cmd_cancelOperation"].doCommand(this.mAddon); - ]]></body> - </method> - - <method name="uninstall"> - <body><![CDATA[ - // If uninstalling does not require a restart and the type doesn't - // support undoing of restartless uninstalls, then we fake it by - // just disabling it it, and doing the real uninstall later. - if (!this.opRequiresRestart("uninstall") && - !this.typeHasFlag("SUPPORTS_UNDO_RESTARTLESS_UNINSTALL")) { - this.setAttribute("wasDisabled", this.mAddon.userDisabled); - - // We must set userDisabled to true first, this will call - // _updateState which will clear any pending attribute set. - this.mAddon.userDisabled = true; - - // This won't update any other add-on manager views (bug 582002) - this.setAttribute("pending", "uninstall"); - } else { - this.mAddon.uninstall(true); - } - ]]></body> - </method> - - <method name="showPreferences"> - <body><![CDATA[ - gViewController.doCommand("cmd_showItemPreferences", this.mAddon); - ]]></body> - </method> - - <method name="upgrade"> - <body><![CDATA[ - var install = this.mManualUpdate; - delete this.mManualUpdate; - install.install(); - ]]></body> - </method> - - <method name="retryInstall"> - <body><![CDATA[ - var install = this._installStatus.mInstall; - if (!install) - return; - if (install.state != AddonManager.STATE_DOWNLOAD_FAILED && - install.state != AddonManager.STATE_INSTALL_FAILED) - return; - install.install(); - ]]></body> - </method> - - <method name="showInDetailView"> - <body><![CDATA[ - gViewController.loadView("addons://detail/" + - encodeURIComponent(this.mAddon.id)); - ]]></body> - </method> - - <method name="onIncludeUpdateChanged"> - <body><![CDATA[ - var event = document.createEvent("Events"); - event.initEvent("IncludeUpdateChanged", true, true); - this.dispatchEvent(event); - ]]></body> - </method> - - <method name="onEnabling"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onEnabled"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onDisabling"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onDisabled"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onUninstalling"> - <parameter name="aRestartRequired"/> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onOperationCancelled"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onPropertyChanged"> - <parameter name="aProperties"/> - <body><![CDATA[ - if (aProperties.indexOf("appDisabled") != -1 || - aProperties.indexOf("signedState") != -1 || - aProperties.indexOf("userDisabled") != -1) - this._updateState(); - ]]></body> - </method> - - <method name="onNoUpdateAvailable"> - <body><![CDATA[ - this._showStatus("none"); - ]]></body> - </method> - - <method name="onCheckingUpdate"> - <body><![CDATA[ - this._showStatus("checking-update"); - ]]></body> - </method> - - <method name="onCompatibilityUpdateAvailable"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onExternalInstall"> - <parameter name="aAddon"/> - <parameter name="aExistingAddon"/> - <parameter name="aNeedsRestart"/> - <body><![CDATA[ - if (aExistingAddon.id != this.mAddon.id) - return; - - // If the install completed without needing a restart then switch to - // using the new Addon - if (!aNeedsRestart) - this._initWithAddon(aAddon); - else - this._updateState(); - ]]></body> - </method> - - <method name="onNewInstall"> - <parameter name="aInstall"/> - <body><![CDATA[ - if (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_ENABLE) - return; - if (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT && - AddonManager.autoUpdateDefault) - return; - - this.mManualUpdate = aInstall; - this._showStatus("update-available"); - ]]></body> - </method> - - <method name="onDownloadStarted"> - <parameter name="aInstall"/> - <body><![CDATA[ - this._updateState(); - this._showStatus("progress"); - this._installStatus.initWithInstall(aInstall); - ]]></body> - </method> - - <method name="onInstallStarted"> - <parameter name="aInstall"/> - <body><![CDATA[ - this._updateState(); - this._showStatus("progress"); - this._installStatus.initWithInstall(aInstall); - ]]></body> - </method> - - <method name="onInstallEnded"> - <parameter name="aInstall"/> - <parameter name="aAddon"/> - <body><![CDATA[ - // If the install completed without needing a restart then switch to - // using the new Addon - if (!(aAddon.pendingOperations & AddonManager.PENDING_INSTALL)) - this._initWithAddon(aAddon); - else - this._updateState(); - ]]></body> - </method> - - <method name="onDownloadFailed"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onInstallFailed"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - - <method name="onInstallCancelled"> - <body><![CDATA[ - this._updateState(); - ]]></body> - </method> - </implementation> - - <handlers> - <handler event="click" button="0"><![CDATA[ - switch (event.detail) { - case 1: - // Prevent double-click where the UI changes on the first click - this._lastClickTarget = event.originalTarget; - break; - case 2: - if (event.originalTarget.localName != 'button' && - !event.originalTarget.classList.contains('text-link') && - event.originalTarget == this._lastClickTarget) { - this.showInDetailView(); - } - break; - } - ]]></handler> - </handlers> - </binding> - - - <!-- Addon - uninstalled - An uninstalled addon that can be re-installed. --> - <binding id="addon-uninstalled" - extends="chrome://mozapps/content/extensions/extensions.xml#addon-base"> - <content> - <xul:hbox class="pending"> - <xul:image class="pending-icon"/> - <xul:label anonid="notice" flex="1"/> - <xul:button anonid="restart-btn" class="button-link" - label="&addon.restartNow.label;" - command="cmd_restartApp"/> - <xul:button anonid="undo-btn" class="button-link" - label="&addon.undoRemove.label;" - tooltiptext="&addon.undoRemove.tooltip;" - oncommand="document.getBindingParent(this).cancelUninstall();"/> - <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </xul:hbox> - </content> - - <implementation> - <constructor><![CDATA[ - this._notice.textContent = gStrings.ext.formatStringFromName("uninstallNotice", - [this.mAddon.name], - 1); - - if (!this.opRequiresRestart("uninstall")) - this._restartBtn.setAttribute("hidden", true); - - gEventManager.registerAddonListener(this, this.mAddon.id); - ]]></constructor> - - <destructor><![CDATA[ - gEventManager.unregisterAddonListener(this, this.mAddon.id); - ]]></destructor> - - <field name="_notice" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "notice"); - </field> - <field name="_restartBtn" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "restart-btn"); - </field> - - <method name="cancelUninstall"> - <body><![CDATA[ - // This assumes that disabling does not require a restart when - // uninstalling doesn't. Things will still work if not, the add-on - // will just still be active until finally getting uninstalled. - - if (this.isPending("uninstall")) - this.mAddon.cancelUninstall(); - else if (this.getAttribute("wasDisabled") != "true") - this.mAddon.userDisabled = false; - - this.removeAttribute("pending"); - ]]></body> - </method> - - <method name="onOperationCancelled"> - <body><![CDATA[ - if (!this.isPending("uninstall")) - this.removeAttribute("pending"); - ]]></body> - </method> - - <method name="onExternalInstall"> - <parameter name="aAddon"/> - <parameter name="aExistingAddon"/> - <parameter name="aNeedsRestart"/> - <body><![CDATA[ - if (aExistingAddon.id != this.mAddon.id) - return; - - // Make sure any newly installed add-on has the correct disabled state - if (this.hasAttribute("wasDisabled")) - aAddon.userDisabled = this.getAttribute("wasDisabled") == "true"; - - // If the install completed without needing a restart then switch to - // using the new Addon - if (!aNeedsRestart) - this.mAddon = aAddon; - - this.removeAttribute("pending"); - ]]></body> - </method> - - <method name="onInstallStarted"> - <parameter name="aInstall"/> - <body><![CDATA[ - // Make sure any newly installed add-on has the correct disabled state - if (this.hasAttribute("wasDisabled")) - aInstall.addon.userDisabled = this.getAttribute("wasDisabled") == "true"; - ]]></body> - </method> - - <method name="onInstallEnded"> - <parameter name="aInstall"/> - <parameter name="aAddon"/> - <body><![CDATA[ - // If the install completed without needing a restart then switch to - // using the new Addon - if (!(aAddon.pendingOperations & AddonManager.PENDING_INSTALL)) - this.mAddon = aAddon; - - this.removeAttribute("pending"); - ]]></body> - </method> - </implementation> - </binding> - - - <!-- Addon - installing - an addon item that is currently being installed --> - <binding id="addon-installing" - extends="chrome://mozapps/content/extensions/extensions.xml#addon-base"> - <content> - <xul:hbox anonid="warning-container" class="warning"> - <xul:image class="warning-icon"/> - <xul:label anonid="warning" flex="1"/> - <xul:button anonid="warning-link" class="button-link" - oncommand="document.getBindingParent(this).retryInstall();"/> - <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </xul:hbox> - <xul:hbox class="content-container"> - <xul:vbox class="icon-outer-container"> - <xul:vbox class="icon-container"> - <xul:image anonid="icon" class="icon"/> - </xul:vbox> - </xul:vbox> - <xul:vbox class="fade name-outer-container" flex="1"> - <xul:hbox class="name-container"> - <xul:label anonid="name" class="name" crop="end" tooltip="addonitem-tooltip"/> - </xul:hbox> - </xul:vbox> - <xul:vbox class="install-status-container"> - <xul:hbox anonid="install-status" class="install-status"/> - </xul:vbox> - </xul:hbox> - </content> - - <implementation> - <constructor><![CDATA[ - this._installStatus.mControl = this; - this._installStatus.mInstall = this.mInstall; - this.refreshInfo(); - ]]></constructor> - - <field name="_icon"> - document.getAnonymousElementByAttribute(this, "anonid", "icon"); - </field> - <field name="_name"> - document.getAnonymousElementByAttribute(this, "anonid", "name"); - </field> - <field name="_warning"> - document.getAnonymousElementByAttribute(this, "anonid", "warning"); - </field> - <field name="_warningLink"> - document.getAnonymousElementByAttribute(this, "anonid", "warning-link"); - </field> - <field name="_installStatus"> - document.getAnonymousElementByAttribute(this, "anonid", - "install-status"); - </field> - - <method name="onInstallCompleted"> - <body><![CDATA[ - this.mAddon = this.mInstall.addon; - this.setAttribute("name", this.mAddon.name); - this.setAttribute("value", this.mAddon.id); - this.setAttribute("status", "installed"); - ]]></body> - </method> - - <method name="refreshInfo"> - <body><![CDATA[ - this.mAddon = this.mAddon || this.mInstall.addon; - if (this.mAddon) { - this._icon.src = this.mAddon.iconURL || - (this.mInstall ? this.mInstall.iconURL : ""); - this._name.value = this.mAddon.name; - } else { - this._icon.src = this.mInstall.iconURL; - // AddonInstall.name isn't always available - fallback to filename - if (this.mInstall.name) { - this._name.value = this.mInstall.name; - } else if (this.mInstall.sourceURI) { - var url = Components.classes["@mozilla.org/network/standard-url;1"] - .createInstance(Components.interfaces.nsIStandardURL); - url.init(url.URLTYPE_STANDARD, 80, this.mInstall.sourceURI.spec, - null, null); - url.QueryInterface(Components.interfaces.nsIURL); - this._name.value = url.fileName; - } - } - - if (this.mInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.downloadError", - [this._name.value], 1 - ); - this._warningLink.label = gStrings.ext.GetStringFromName("notification.downloadError.retry"); - this._warningLink.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip"); - } else if (this.mInstall.state == AddonManager.STATE_INSTALL_FAILED) { - this.setAttribute("notification", "warning"); - this._warning.textContent = gStrings.ext.formatStringFromName( - "notification.installError", - [this._name.value], 1 - ); - this._warningLink.label = gStrings.ext.GetStringFromName("notification.installError.retry"); - this._warningLink.tooltipText = gStrings.ext.GetStringFromName("notification.downloadError.retry.tooltip"); - } else { - this.removeAttribute("notification"); - } - ]]></body> - </method> - - <method name="retryInstall"> - <body><![CDATA[ - this.mInstall.install(); - ]]></body> - </method> - </implementation> - </binding> - - <binding id="detail-row"> - <content> - <xul:label class="detail-row-label" xbl:inherits="value=label"/> - <xul:label class="detail-row-value" xbl:inherits="value"/> - </content> - - <implementation> - <property name="value"> - <getter><![CDATA[ - return this.getAttribute("value"); - ]]></getter> - <setter><![CDATA[ - if (!val) - this.removeAttribute("value"); - else - this.setAttribute("value", val); - ]]></setter> - </property> - </implementation> - </binding> - -</bindings> diff --git a/toolkit/mozapps/webextensions/content/extensions.xul b/toolkit/mozapps/webextensions/content/extensions.xul deleted file mode 100644 index 70939d024..000000000 --- a/toolkit/mozapps/webextensions/content/extensions.xul +++ /dev/null @@ -1,715 +0,0 @@ -<?xml version="1.0"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> -<?xml-stylesheet href="chrome://mozapps/content/extensions/extensions.css"?> -<?xml-stylesheet href="chrome://mozapps/skin/extensions/extensions.css"?> - -<!DOCTYPE page [ -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > -%brandDTD; -<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd"> -%extensionsDTD; -]> - -<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:xhtml="http://www.w3.org/1999/xhtml" - id="addons-page" title="&addons.windowTitle;" - role="application" windowtype="Addons:Manager" - disablefastfind="true"> - - <xhtml:link rel="shortcut icon" - href="chrome://mozapps/skin/extensions/extensionGeneric-16.png"/> - - <script type="application/javascript" - src="chrome://mozapps/content/extensions/extensions.js"/> - <script type="application/javascript" - src="chrome://global/content/contentAreaUtils.js"/> - - <popupset> - <!-- menu for an addon item --> - <menupopup id="addonitem-popup"> - <menuitem id="menuitem_showDetails" command="cmd_showItemDetails" - default="true" label="&cmd.showDetails.label;" - accesskey="&cmd.showDetails.accesskey;"/> - <menuitem id="menuitem_enableItem" command="cmd_enableItem" - label="&cmd.enableAddon.label;" - accesskey="&cmd.enableAddon.accesskey;"/> - <menuitem id="menuitem_disableItem" command="cmd_disableItem" - label="&cmd.disableAddon.label;" - accesskey="&cmd.disableAddon.accesskey;"/> - <menuitem id="menuitem_enableTheme" command="cmd_enableItem" - label="&cmd.enableTheme.label;" - accesskey="&cmd.enableTheme.accesskey;"/> - <menuitem id="menuitem_disableTheme" command="cmd_disableItem" - label="&cmd.disableTheme.label;" - accesskey="&cmd.disableTheme.accesskey;"/> - <menuitem id="menuitem_installItem" command="cmd_installItem" - label="&cmd.installAddon.label;" - accesskey="&cmd.installAddon.accesskey;"/> - <menuitem id="menuitem_uninstallItem" command="cmd_uninstallItem" - label="&cmd.uninstallAddon.label;" - accesskey="&cmd.uninstallAddon.accesskey;"/> - <menuseparator id="addonitem-menuseparator" /> - <menuitem id="menuitem_preferences" command="cmd_showItemPreferences" -#ifdef XP_WIN - label="&cmd.preferencesWin.label;" - accesskey="&cmd.preferencesWin.accesskey;"/> -#else - label="&cmd.preferencesUnix.label;" - accesskey="&cmd.preferencesUnix.accesskey;"/> -#endif - <menuitem id="menuitem_findUpdates" command="cmd_findItemUpdates" - label="&cmd.findUpdates.label;" - accesskey="&cmd.findUpdates.accesskey;"/> - <menuitem id="menuitem_about" command="cmd_showItemAbout" - label="&cmd.about.label;" - accesskey="&cmd.about.accesskey;"/> - </menupopup> - - <tooltip id="addonitem-tooltip"/> - </popupset> - - <!-- global commands - these act on all addons, or affect the addons manager - in some other way --> - <commandset id="globalCommandSet"> - <!-- XXXsw remove useless oncommand attribute once bug 371900 is fixed --> - <command id="cmd_focusSearch" oncommand=";"/> - <command id="cmd_findAllUpdates"/> - <command id="cmd_restartApp"/> - <command id="cmd_goToDiscoverPane"/> - <command id="cmd_goToRecentUpdates"/> - <command id="cmd_goToAvailableUpdates"/> - <command id="cmd_installFromFile"/> - <command id="cmd_debugAddons"/> - <command id="cmd_back"/> - <command id="cmd_forward"/> - <command id="cmd_enableCheckCompatibility"/> - <command id="cmd_enableUpdateSecurity"/> - <command id="cmd_toggleAutoUpdateDefault"/> - <command id="cmd_resetAddonAutoUpdate"/> - <command id="cmd_experimentsLearnMore"/> - <command id="cmd_experimentsOpenTelemetryPreferences"/> - <command id="cmd_showUnsignedExtensions"/> - <command id="cmd_showAllExtensions"/> - </commandset> - - <!-- view commands - these act on the selected addon --> - <commandset id="viewCommandSet" - events="richlistbox-select" commandupdater="true"> - <command id="cmd_showItemDetails"/> - <command id="cmd_findItemUpdates"/> - <command id="cmd_showItemPreferences"/> - <command id="cmd_showItemAbout"/> - <command id="cmd_enableItem"/> - <command id="cmd_disableItem"/> - <command id="cmd_installItem"/> - <command id="cmd_purchaseItem"/> - <command id="cmd_uninstallItem"/> - <command id="cmd_cancelUninstallItem"/> - <command id="cmd_cancelOperation"/> - <command id="cmd_contribute"/> - <command id="cmd_askToActivateItem"/> - <command id="cmd_alwaysActivateItem"/> - <command id="cmd_neverActivateItem"/> - </commandset> - - <keyset> - <key id="focusSearch" key="&search.commandkey;" modifiers="accel" - command="cmd_focusSearch"/> - </keyset> - <hbox flex="1"> - <vbox> - <hbox id="nav-header" - align="center" - pack="center"> - <toolbarbutton id="back-btn" - class="nav-button header-button" - command="cmd_back" - tooltiptext="&cmd.back.tooltip;" - hidden="true" - disabled="true"/> - <toolbarbutton id="forward-btn" - class="nav-button header-button" - command="cmd_forward" - tooltiptext="&cmd.forward.tooltip;" - hidden="true" - disabled="true"/> - </hbox> - <!-- category list --> - <richlistbox id="categories" flex="1"> - <richlistitem id="category-search" value="addons://search/" - class="category" - name="&view.search.label;" priority="0" - tooltiptext="&view.search.label;" disabled="true"/> - <richlistitem id="category-discover" value="addons://discover/" - class="category" - name="&view.discover.label;" priority="1000" - tooltiptext="&view.discover.label;"/> - <richlistitem id="category-availableUpdates" value="addons://updates/available" - class="category" - name="&view.availableUpdates.label;" priority="100000" - tooltiptext="&view.availableUpdates.label;" - disabled="true"/> - <richlistitem id="category-recentUpdates" value="addons://updates/recent" - class="category" - name="&view.recentUpdates.label;" priority="101000" - tooltiptext="&view.recentUpdates.label;" disabled="true"/> - </richlistbox> - </vbox> - <vbox class="main-content" flex="1"> - <!-- view port --> - <deck id="view-port" flex="1" selectedIndex="0"> - <!-- discover view --> - <deck id="discover-view" flex="1" class="view-pane" selectedIndex="0" tabindex="0"> - <vbox id="discover-loading" align="center" pack="stretch" flex="1" class="alert-container"> - <spacer class="alert-spacer-before"/> - <hbox class="alert loading" align="center"> - <image/> - <label value="&loading.label;"/> - </hbox> - <spacer class="alert-spacer-after"/> - </vbox> - <vbox id="discover-error" align="center" pack="stretch" flex="1" class="alert-container"> - <spacer class="alert-spacer-before"/> - <hbox> - <spacer class="discover-spacer-before"/> - <hbox class="alert" align="center"> - <image class="discover-logo"/> - <vbox flex="1" align="stretch"> - <label class="discover-title">&discover.title;</label> - <description class="discover-description">&discover.description2;</description> - <description class="discover-footer">&discover.footer;</description> - </vbox> - </hbox> - <spacer class="discover-spacer-after"/> - </hbox> - <spacer class="alert-spacer-after"/> - </vbox> - <browser id="discover-browser" type="content" flex="1" - disablehistory="true" homepage="about:blank"/> - </deck> - - <!-- container for views with the search/tools header --> - <vbox id="headered-views" flex="1"> - <!-- main header --> - <hbox id="header" align="center"> - <button id="show-all-extensions" hidden="true" - label="&showAllExtensions.button.label;" - command="cmd_showAllExtensions"/> - <spacer flex="1"/> - <hbox id="updates-container" align="center"> - <image class="spinner"/> - <label id="updates-noneFound" hidden="true" - value="&updates.noneFound.label;"/> - <button id="updates-manualUpdatesFound-btn" class="button-link" - hidden="true" label="&updates.manualUpdatesFound.label;" - command="cmd_goToAvailableUpdates"/> - <label id="updates-progress" hidden="true" - value="&updates.updating.label;"/> - <label id="updates-installed" hidden="true" - value="&updates.installed.label;"/> - <label id="updates-downloaded" hidden="true" - value="&updates.downloaded.label;"/> - <button id="updates-restart-btn" class="button-link" hidden="true" - label="&updates.restart.label;" - command="cmd_restartApp"/> - </hbox> - <button id="show-disabled-unsigned-extensions" hidden="true" - class="warning" - label="&showUnsignedExtensions.button.label;" - command="cmd_showUnsignedExtensions"/> - <toolbarbutton id="header-utils-btn" class="header-button" type="menu" - tooltiptext="&toolsMenu.tooltip;"> - <menupopup id="utils-menu"> - <menuitem id="utils-updateNow" - label="&updates.checkForUpdates.label;" - accesskey="&updates.checkForUpdates.accesskey;" - command="cmd_findAllUpdates"/> - <menuitem id="utils-viewUpdates" - label="&updates.viewUpdates.label;" - accesskey="&updates.viewUpdates.accesskey;" - command="cmd_goToRecentUpdates"/> - <menuseparator id="utils-installFromFile-separator"/> - <menuitem id="utils-installFromFile" - label="&installAddonFromFile.label;" - accesskey="&installAddonFromFile.accesskey;" - command="cmd_installFromFile"/> - <menuitem id="utils-debugAddons" - label="&debugAddons.label;" - accesskey="&debugAddons.accesskey;" - command="cmd_debugAddons"/> - <menuseparator/> - <menuitem id="utils-autoUpdateDefault" - label="&updates.updateAddonsAutomatically.label;" - accesskey="&updates.updateAddonsAutomatically.accesskey;" - type="checkbox" autocheck="false" - command="cmd_toggleAutoUpdateDefault"/> - <menuitem id="utils-resetAddonUpdatesToAutomatic" - label="&updates.resetUpdatesToAutomatic.label;" - accesskey="&updates.resetUpdatesToAutomatic.accesskey;" - command="cmd_resetAddonAutoUpdate"/> - <menuitem id="utils-resetAddonUpdatesToManual" - label="&updates.resetUpdatesToManual.label;" - accesskey="&updates.resetUpdatesToManual.accesskey;" - command="cmd_resetAddonAutoUpdate"/> - </menupopup> - </toolbarbutton> - <textbox id="header-search" type="search" searchbutton="true" - searchbuttonlabel="&search.buttonlabel;" - placeholder="&search.placeholder;"/> - </hbox> - - <deck id="headered-views-content" flex="1" selectedIndex="0"> - <!-- search view --> - <vbox id="search-view" flex="1" class="view-pane" tabindex="0"> - <hbox class="view-header global-warning-container" align="center"> - <!-- global warnings --> - <hbox class="global-warning" flex="1"> - <hbox class="global-warning-safemode" flex="1" align="center" - tooltiptext="&warning.safemode.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.safemode.label;"/> - </hbox> - <hbox class="global-warning-checkcompatibility" flex="1" align="center" - tooltiptext="&warning.checkcompatibility.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.checkcompatibility.label;"/> - </hbox> - <button class="button-link global-warning-checkcompatibility" - label="&warning.checkcompatibility.enable.label;" - tooltiptext="&warning.checkcompatibility.enable.tooltip;" - command="cmd_enableCheckCompatibility"/> - <hbox class="global-warning-updatesecurity" flex="1" align="center" - tooltiptext="&warning.updatesecurity.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.updatesecurity.label;"/> - </hbox> - <button class="button-link global-warning-updatesecurity" - label="&warning.updatesecurity.enable.label;" - tooltiptext="&warning.updatesecurity.enable.tooltip;" - command="cmd_enableUpdateSecurity"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </hbox> - <spacer flex="1"/> - <hbox id="search-sorters" class="sort-controls" - showrelevance="true" sortby="relevancescore" ascending="false"/> - </hbox> - <hbox id="search-filter" align="center"> - <label id="search-filter-label" value="&search.filter2.label;"/> - <radiogroup id="search-filter-radiogroup" orient="horizontal" - align="center" persist="value" value="remote"> - <radio id="search-filter-local" class="search-filter-radio" - label="&search.filter2.installed.label;" value="local" - tooltiptext="&search.filter2.installed.tooltip;"/> - <radio id="search-filter-remote" class="search-filter-radio" - label="&search.filter2.available.label;" value="remote" - tooltiptext="&search.filter2.available.tooltip;"/> - </radiogroup> - </hbox> - <vbox id="search-loading" class="alert-container" - flex="1" hidden="true"> - <spacer class="alert-spacer-before"/> - <hbox class="alert loading" align="center"> - <image/> - <label value="&loading.label;"/> - </hbox> - <spacer class="alert-spacer-after"/> - </vbox> - <vbox id="search-list-empty" class="alert-container" - flex="1" hidden="true"> - <spacer class="alert-spacer-before"/> - <vbox class="alert"> - <label value="&listEmpty.search.label;"/> - <button class="discover-button" - id="discover-button-search" - label="&listEmpty.button.label;" - command="cmd_goToDiscoverPane"/> - </vbox> - <spacer class="alert-spacer-after"/> - </vbox> - <richlistbox id="search-list" class="list" flex="1"> - <hbox pack="center"> - <label id="search-allresults-link" class="text-link"/> - </hbox> - </richlistbox> - </vbox> - - <!-- list view --> - <vbox id="list-view" flex="1" class="view-pane" align="stretch" tabindex="0"> - <!-- info UI for add-ons that have been disabled for being unsigned --> - <vbox id="disabled-unsigned-addons-info" hidden="true"> - <label id="disabled-unsigned-addons-heading" value="&disabledUnsigned.heading;"/> - <description> - &disabledUnsigned.description.start;<label class="text-link plain" id="find-alternative-addons">&disabledUnsigned.description.findAddonsLink;</label>&disabledUnsigned.description.end; - </description> - <hbox pack="start"><label class="text-link" id="signing-learn-more">&disabledUnsigned.learnMore;</label></hbox> - <description id="signing-dev-info"> - &disabledUnsigned.devInfo.start;<label class="text-link plain" id="signing-dev-manual-link">&disabledUnsigned.devInfo.linkToManual;</label>&disabledUnsigned.devInfo.end; - </description> - </vbox> - <vbox id="plugindeprecation-notice" class="alert-container"> - <hbox class="alert"> - <description>&pluginDeprecation.description;   - <label class="text-link plain" id="plugindeprecation-learnmore-link">&pluginDeprecation.learnMore;</label> - </description> - </hbox> - </vbox> - <hbox class="view-header global-warning-container"> - <!-- global warnings --> - <hbox class="global-warning" flex="1"> - <hbox class="global-warning-safemode" flex="1" align="center" - tooltiptext="&warning.safemode.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.safemode.label;"/> - </hbox> - <hbox class="global-warning-checkcompatibility" flex="1" align="center" - tooltiptext="&warning.checkcompatibility.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.checkcompatibility.label;"/> - </hbox> - <button class="button-link global-warning-checkcompatibility" - label="&warning.checkcompatibility.enable.label;" - tooltiptext="&warning.checkcompatibility.enable.tooltip;" - command="cmd_enableCheckCompatibility"/> - <hbox class="global-warning-updatesecurity" flex="1" align="center" - tooltiptext="&warning.updatesecurity.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.updatesecurity.label;"/> - </hbox> - <button class="button-link global-warning-updatesecurity" - label="&warning.updatesecurity.enable.label;" - tooltiptext="&warning.updatesecurity.enable.tooltip;" - command="cmd_enableUpdateSecurity"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </hbox> - </hbox> - <hbox class="view-header global-info-container experiment-info-container"> - <hbox class="global-info" flex="1" align="center"> - <label value="&experiment.info.label;"/> - <button id="experiments-learn-more" - label="&experiment.info.learnmore;" - tooltiptext="&experiment.info.learnmore;" - accesskey="&experiment.info.learnmore.accesskey;" - command="cmd_experimentsLearnMore"/> - <button id="experiments-change-telemetry" - label="&experiment.info.changetelemetry;" - tooltiptext="&experiment.info.changetelemetry;" - accesskey="&experiment.info.changetelemetry.accesskey;" - command="cmd_experimentsOpenTelemetryPreferences"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap. --> - </hbox> - </hbox> - <vbox id="addon-list-empty" class="alert-container" - flex="1" hidden="true"> - <spacer class="alert-spacer-before"/> - <vbox class="alert"> - <label value="&listEmpty.installed.label;"/> - <button class="discover-button" - id="discover-button-install" - label="&listEmpty.button.label;" - command="cmd_goToDiscoverPane"/> - </vbox> - <spacer class="alert-spacer-after"/> - </vbox> - <richlistbox id="addon-list" class="list" flex="1"/> - </vbox> - <!-- updates view --> - <vbox id="updates-view" flex="1" class="view-pane" tabindex="0"> - <hbox class="view-header global-warning-container" align="center"> - <!-- global warnings --> - <hbox class="global-warning" flex="1"> - <hbox class="global-warning-safemode" flex="1" align="center" - tooltiptext="&warning.safemode.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.safemode.label;"/> - </hbox> - <hbox class="global-warning-checkcompatibility" flex="1" align="center" - tooltiptext="&warning.checkcompatibility.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.checkcompatibility.label;"/> - </hbox> - <button class="button-link global-warning-checkcompatibility" - label="&warning.checkcompatibility.enable.label;" - tooltiptext="&warning.checkcompatibility.enable.tooltip;" - command="cmd_enableCheckCompatibility"/> - <hbox class="global-warning-updatesecurity" flex="1" align="center" - tooltiptext="&warning.updatesecurity.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.updatesecurity.label;"/> - </hbox> - <button class="button-link global-warning-updatesecurity" - label="&warning.updatesecurity.enable.label;" - tooltiptext="&warning.updatesecurity.enable.tooltip;" - command="cmd_enableUpdateSecurity"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </hbox> - <spacer flex="1"/> - <hbox id="updates-sorters" class="sort-controls" sortby="updateDate" - ascending="false"/> - </hbox> - <vbox id="updates-list-empty" class="alert-container" - flex="1" hidden="true"> - <spacer class="alert-spacer-before"/> - <vbox class="alert"> - <label id="empty-availableUpdates-msg" value="&listEmpty.availableUpdates.label;"/> - <label id="empty-recentUpdates-msg" value="&listEmpty.recentUpdates.label;"/> - <button label="&listEmpty.findUpdates.label;" - command="cmd_findAllUpdates"/> - </vbox> - <spacer class="alert-spacer-after"/> - </vbox> - <hbox id="update-actions" pack="center"> - <button id="update-selected-btn" hidden="true" - label="&updates.updateSelected.label;" - tooltiptext="&updates.updateSelected.tooltip;"/> - </hbox> - <richlistbox id="updates-list" class="list" flex="1"/> - </vbox> - - <!-- detail view --> - <scrollbox id="detail-view" flex="1" class="view-pane addon-view" orient="vertical" tabindex="0" - role="document"> - <!-- global warnings --> - <hbox class="global-warning-container global-warning"> - <hbox class="global-warning-safemode" flex="1" align="center" - tooltiptext="&warning.safemode.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.safemode.label;"/> - </hbox> - <hbox class="global-warning-checkcompatibility" flex="1" align="center" - tooltiptext="&warning.checkcompatibility.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.checkcompatibility.label;"/> - </hbox> - <button class="button-link global-warning-checkcompatibility" - label="&warning.checkcompatibility.enable.label;" - tooltiptext="&warning.checkcompatibility.enable.tooltip;" - command="cmd_enableCheckCompatibility"/> - <hbox class="global-warning-updatesecurity" flex="1" align="center" - tooltiptext="&warning.updatesecurity.label;"> - <image class="warning-icon"/> - <label class="global-warning-text" flex="1" crop="end" - value="&warning.updatesecurity.label;"/> - </hbox> - <button class="button-link global-warning-updatesecurity" - label="&warning.updatesecurity.enable.label;" - tooltiptext="&warning.updatesecurity.enable.tooltip;" - command="cmd_enableUpdateSecurity"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </hbox> - <hbox flex="1"> - <spacer flex="1"/> - <!-- "loading" splash screen --> - <vbox class="alert-container"> - <spacer class="alert-spacer-before"/> - <hbox class="alert loading"> - <image/> - <label value="&loading.label;"/> - </hbox> - <spacer class="alert-spacer-after"/> - </vbox> - <!-- actual detail view --> - <vbox class="detail-view-container" flex="3" contextmenu="addonitem-popup"> - <vbox id="detail-notifications"> - <hbox id="warning-container" align="center" class="warning"> - <image class="warning-icon"/> - <label id="detail-warning" flex="1"/> - <label id="detail-warning-link" class="text-link"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </hbox> - <hbox id="error-container" align="center" class="error"> - <image class="error-icon"/> - <label id="detail-error" flex="1"/> - <label id="detail-error-link" class="text-link"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </hbox> - <hbox id="pending-container" align="center" class="pending"> - <image class="pending-icon"/> - <label id="detail-pending" flex="1"/> - <button id="detail-restart-btn" class="button-link" - label="&addon.restartNow.label;" - command="cmd_restartApp"/> - <button id="detail-undo-btn" class="button-link" - label="&addon.undoAction.label;" - tooltipText="&addon.undoAction.tooltip;" - command="cmd_cancelOperation"/> - <spacer flex="5000"/> <!-- Necessary to allow the message to wrap --> - </hbox> - </vbox> - <hbox align="start"> - <vbox id="detail-icon-container" align="end"> - <image id="detail-icon" class="icon"/> - </vbox> - <vbox flex="1"> - <vbox id="detail-summary"> - <hbox id="detail-name-container" class="name-container" - align="start"> - <label id="detail-name" flex="1"/> - <label id="detail-version"/> - <label class="disabled-postfix" value="&addon.disabled.postfix;"/> - <label class="update-postfix" value="&addon.update.postfix;"/> - <spacer flex="5000"/> <!-- Necessary to allow the name to wrap --> - </hbox> - <label id="detail-creator" class="creator"/> - </vbox> - <hbox id="detail-experiment-container"> - <svg width="8" height="8" viewBox="0 0 8 8" version="1.1" - xmlns="http://www.w3.org/2000/svg" - id="detail-experiment-bullet-container"> - <circle cx="4" cy="4" r="4" id="detail-experiment-bullet"/> - </svg> - <label id="detail-experiment-state"/> - <label id="detail-experiment-time"/> - </hbox> - <hbox id="detail-desc-container" align="start"> - <vbox id="detail-screenshot-box" pack="center" hidden="true"> <!-- Necessary to work around bug 394738 --> - <image id="detail-screenshot"/> - </vbox> - <vbox flex="1"> - <description id="detail-desc"/> - <description id="detail-fulldesc"/> - </vbox> - </hbox> - <vbox id="detail-contributions"> - <description id="detail-contrib-description"> - &detail.contributions.description; - </description> - <hbox align="center"> - <label id="detail-contrib-suggested"/> - <spacer flex="1"/> - <button id="detail-contrib-btn" - label="&cmd.contribute.label;" - accesskey="&cmd.contribute.accesskey;" - tooltiptext="&cmd.contribute.tooltip;" - command="cmd_contribute"/> - </hbox> - </vbox> - <grid id="detail-grid"> - <columns> - <column flex="1"/> - <column flex="2"/> - </columns> - <rows id="detail-rows"> - <row class="detail-row-complex" id="detail-updates-row"> - <label class="detail-row-label" value="&detail.updateType;"/> - <hbox align="center"> - <radiogroup id="detail-autoUpdate" orient="horizontal"> - <!-- The values here need to match the values of - AddonManager.AUTOUPDATE_* --> - <radio label="&detail.updateDefault.label;" - tooltiptext="&detail.updateDefault.tooltip;" - value="1"/> - <radio label="&detail.updateAutomatic.label;" - tooltiptext="&detail.updateAutomatic.tooltip;" - value="2"/> - <radio label="&detail.updateManual.label;" - tooltiptext="&detail.updateManual.tooltip;" - value="0"/> - </radiogroup> - <button id="detail-findUpdates-btn" class="button-link" - label="&detail.checkForUpdates.label;" - accesskey="&detail.checkForUpdates.accesskey;" - tooltiptext="&detail.checkForUpdates.tooltip;" - command="cmd_findItemUpdates"/> - </hbox> - </row> - <row class="detail-row" id="detail-dateUpdated" label="&detail.lastupdated.label;"/> - <row class="detail-row-complex" id="detail-homepage-row" label="&detail.home;"> - <label class="detail-row-label" value="&detail.home;"/> - <label id="detail-homepage" class="detail-row-value text-link" crop="end"/> - </row> - <row class="detail-row-complex" id="detail-repository-row" label="&detail.repository;"> - <label class="detail-row-label" value="&detail.repository;"/> - <label id="detail-repository" class="detail-row-value text-link"/> - </row> - <row class="detail-row" id="detail-size" label="&detail.size;"/> - <row class="detail-row-complex" id="detail-rating-row"> - <label class="detail-row-label" value="&rating2.label;"/> - <hbox> - <label id="detail-rating" class="meta-value meta-rating" - showrating="average"/> - <label id="detail-reviews" class="text-link"/> - </hbox> - </row> - <row class="detail-row" id="detail-downloads" label="&detail.numberOfDownloads.label;"/> - </rows> - </grid> - <hbox id="detail-controls"> - <button id="detail-prefs-btn" class="addon-control preferences" -#ifdef XP_WIN - label="&detail.showPreferencesWin.label;" - accesskey="&detail.showPreferencesWin.accesskey;" - tooltiptext="&detail.showPreferencesWin.tooltip;" -#else - label="&detail.showPreferencesUnix.label;" - accesskey="&detail.showPreferencesUnix.accesskey;" - tooltiptext="&detail.showPreferencesUnix.tooltip;" -#endif - command="cmd_showItemPreferences"/> - <spacer flex="1"/> - <button id="detail-enable-btn" class="addon-control enable" - label="&cmd.enableAddon.label;" - accesskey="&cmd.enableAddon.accesskey;" - command="cmd_enableItem"/> - <button id="detail-disable-btn" class="addon-control disable" - label="&cmd.disableAddon.label;" - accesskey="&cmd.disableAddon.accesskey;" - command="cmd_disableItem"/> - <button id="detail-uninstall-btn" class="addon-control remove" - label="&cmd.uninstallAddon.label;" - accesskey="&cmd.uninstallAddon.accesskey;" - command="cmd_uninstallItem"/> - <button id="detail-purchase-btn" class="addon-control purchase" - command="cmd_purchaseItem"/> - <button id="detail-install-btn" class="addon-control install" - label="&cmd.installAddon.label;" - accesskey="&cmd.installAddon.accesskey;" - command="cmd_installItem"/> - <menulist id="detail-state-menulist" - crop="none" sizetopopup="always" - tooltiptext="&cmd.stateMenu.tooltip;"> - <menupopup> - <menuitem id="detail-ask-to-activate-menuitem" - class="addon-control" - label="&cmd.askToActivate.label;" - tooltiptext="&cmd.askToActivate.tooltip;" - command="cmd_askToActivateItem"/> - <menuitem id="detail-always-activate-menuitem" - class="addon-control" - label="&cmd.alwaysActivate.label;" - tooltiptext="&cmd.alwaysActivate.tooltip;" - command="cmd_alwaysActivateItem"/> - <menuitem id="detail-never-activate-menuitem" - class="addon-control" - label="&cmd.neverActivate.label;" - tooltiptext="&cmd.neverActivate.tooltip;" - command="cmd_neverActivateItem"/> - </menupopup> - </menulist> - </hbox> - </vbox> - </hbox> - </vbox> - <spacer flex="1"/> - </hbox> - </scrollbox> - </deck> - </vbox> - </deck> - </vbox> - </hbox> -</page> diff --git a/toolkit/mozapps/webextensions/content/newaddon.xul b/toolkit/mozapps/webextensions/content/newaddon.xul deleted file mode 100644 index 1d8545249..000000000 --- a/toolkit/mozapps/webextensions/content/newaddon.xul +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> -<?xml-stylesheet href="chrome://mozapps/skin/extensions/newaddon.css"?> - -<!DOCTYPE page [ -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > -%brandDTD; -<!ENTITY % newaddonDTD SYSTEM "chrome://mozapps/locale/extensions/newaddon.dtd"> -%newaddonDTD; -]> - -<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:xhtml="http://www.w3.org/1999/xhtml" title="&title;" - disablefastfind="true" id="addon-page" onload="initialize()" - onunload="unload()" role="application" align="stretch" pack="stretch"> - - <xhtml:link rel="shortcut icon" style="display: none" - href="chrome://mozapps/skin/extensions/extensionGeneric-16.png"/> - - <script type="application/javascript" - src="chrome://mozapps/content/extensions/newaddon.js"/> - - <scrollbox id="addon-scrollbox" align="center"> - <spacer id="spacer-start"/> - - <vbox id="addon-container" class="main-content"> - <description>&intro;</description> - - <hbox id="addon-info"> - <image id="icon"/> - <vbox flex="1"> - <label id="name"/> - <label id="author"/> - <label id="location" crop="end"/> - </vbox> - </hbox> - - <hbox id="warning"> - <image id="warning-icon"/> - <description flex="1">&warning;</description> - </hbox> - - <checkbox id="allow" label="&allow;"/> - <description id="later">&later;</description> - - <deck id="buttonDeck"> - <hbox id="continuePanel"> - <button id="continue-button" label="&continue;" - oncommand="continueClicked()"/> - </hbox> - <vbox id="restartPanel"> - <description id="restartMessage">&restartMessage;</description> - <hbox id="restartPanelButtons"> - <button id="restart-button" label="&restartButton;" oncommand="restartClicked()"/> - <button id="cancel-button" label="&cancelButton;" oncommand="cancelClicked()"/> - </hbox> - </vbox> - </deck> - </vbox> - - <spacer id="spacer-end"/> - </scrollbox> -</page> diff --git a/toolkit/mozapps/webextensions/content/setting.xml b/toolkit/mozapps/webextensions/content/setting.xml deleted file mode 100644 index 2b70eb0d0..000000000 --- a/toolkit/mozapps/webextensions/content/setting.xml +++ /dev/null @@ -1,486 +0,0 @@ -<?xml version="1.0"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<!DOCTYPE page [ -<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd"> -%extensionsDTD; -]> - -<!-- import-globals-from extensions.js --> - -<bindings xmlns="http://www.mozilla.org/xbl" - xmlns:xbl="http://www.mozilla.org/xbl" - xmlns:html="http://www.w3.org/1999/xhtml" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - - <binding id="setting-base"> - <implementation> - <constructor><![CDATA[ - this.preferenceChanged(); - - this.addEventListener("keypress", function(event) { - event.stopPropagation(); - }, false); - - if (this.usePref) - Services.prefs.addObserver(this.pref, this._observer, true); - ]]></constructor> - - <field name="_observer"><![CDATA[({ - _self: this, - - QueryInterface: function(aIID) { - const Ci = Components.interfaces; - if (aIID.equals(Ci.nsIObserver) || - aIID.equals(Ci.nsISupportsWeakReference) || - aIID.equals(Ci.nsISupports)) - return this; - - throw Components.Exception("No interface", Components.results.NS_ERROR_NO_INTERFACE); - }, - - observe: function(aSubject, aTopic, aPrefName) { - if (aTopic != "nsPref:changed") - return; - - if (this._self.pref == aPrefName) - this._self.preferenceChanged(); - } - })]]> - </field> - - <method name="fireEvent"> - <parameter name="eventName"/> - <parameter name="funcStr"/> - <body> - <![CDATA[ - let body = funcStr || this.getAttribute(eventName); - if (!body) - return; - - try { - let event = document.createEvent("Events"); - event.initEvent(eventName, true, true); - let f = new Function("event", body); - f.call(this, event); - } - catch (e) { - Cu.reportError(e); - } - ]]> - </body> - </method> - - <method name="valueFromPreference"> - <body> - <![CDATA[ - // Should be code to set the from the preference input.value - throw Components.Exception("No valueFromPreference implementation", - Components.results.NS_ERROR_NOT_IMPLEMENTED); - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - // Should be code to set the input.value from the preference - throw Components.Exception("No valueToPreference implementation", - Components.results.NS_ERROR_NOT_IMPLEMENTED); - ]]> - </body> - </method> - - <method name="inputChanged"> - <body> - <![CDATA[ - if (this.usePref && !this._updatingInput) { - this.valueToPreference(); - this.fireEvent("oninputchanged"); - } - ]]> - </body> - </method> - - <method name="preferenceChanged"> - <body> - <![CDATA[ - if (this.usePref) { - this._updatingInput = true; - try { - this.valueFromPreference(); - this.fireEvent("onpreferencechanged"); - } catch (e) {} - this._updatingInput = false; - } - ]]> - </body> - </method> - - <property name="usePref" readonly="true" onget="return this.hasAttribute('pref');"/> - <property name="pref" readonly="true" onget="return this.getAttribute('pref');"/> - <property name="type" readonly="true" onget="return this.getAttribute('type');"/> - <property name="value" onget="return this.input.value;" onset="return this.input.value = val;"/> - - <field name="_updatingInput">false</field> - <field name="input">document.getAnonymousElementByAttribute(this, "anonid", "input");</field> - <field name="settings"> - this.parentNode.localName == "settings" ? this.parentNode : null; - </field> - </implementation> - </binding> - - <binding id="setting-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-base"> - <content> - <xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/> - </xul:hbox> - <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/> - <xul:label class="preferences-learnmore text-link" - onclick="document.getBindingParent(this).openLearnMore()">&setting.learnmore;</xul:label> - </xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:checkbox anonid="input" xbl:inherits="disabled,onlabel,offlabel,label=checkboxlabel" oncommand="inputChanged();"/> - </xul:hbox> - </content> - - <implementation> - <method name="valueFromPreference"> - <body> - <![CDATA[ - let val = Services.prefs.getBoolPref(this.pref); - this.value = this.inverted ? !val : val; - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - let val = this.value; - Services.prefs.setBoolPref(this.pref, this.inverted ? !val : val); - ]]> - </body> - </method> - - <property name="value" onget="return this.input.checked;" onset="return this.input.setChecked(val);"/> - <property name="inverted" readonly="true" onget="return this.getAttribute('inverted');"/> - - <method name="openLearnMore"> - <body> - <![CDATA[ - window.open(this.getAttribute("learnmore"), "_blank"); - ]]> - </body> - </method> - </implementation> - </binding> - - <binding id="setting-boolint" extends="chrome://mozapps/content/extensions/setting.xml#setting-bool"> - <implementation> - <method name="valueFromPreference"> - <body> - <![CDATA[ - let val = Services.prefs.getIntPref(this.pref); - this.value = (val == this.getAttribute("on")); - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - Services.prefs.setIntPref(this.pref, this.getAttribute(this.value ? "on" : "off")); - ]]> - </body> - </method> - </implementation> - </binding> - - <binding id="setting-localized-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-bool"> - <implementation> - <method name="valueFromPreference"> - <body> - <![CDATA[ - let val = Services.prefs.getComplexValue(this.pref, Components.interfaces.nsIPrefLocalizedString).data; - if (this.inverted) val = !val; - this.value = (val == "true"); - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - let val = this.value; - if (this.inverted) val = !val; - let pref = Components.classes["@mozilla.org/pref-localizedstring;1"].createInstance(Components.interfaces.nsIPrefLocalizedString); - pref.data = this.inverted ? (!val).toString() : val.toString(); - Services.prefs.setComplexValue(this.pref, Components.interfaces.nsIPrefLocalizedString, pref); - ]]> - </body> - </method> - </implementation> - </binding> - - <binding id="setting-integer" extends="chrome://mozapps/content/extensions/setting.xml#setting-base"> - <content> - <xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/> - </xul:hbox> - <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/> - </xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:textbox type="number" anonid="input" oninput="inputChanged();" onchange="inputChanged();" - xbl:inherits="disabled,emptytext,min,max,increment,hidespinbuttons,wraparound,size"/> - </xul:hbox> - </content> - - <implementation> - <method name="valueFromPreference"> - <body> - <![CDATA[ - let val = Services.prefs.getIntPref(this.pref); - this.value = val; - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - Services.prefs.setIntPref(this.pref, this.value); - ]]> - </body> - </method> - </implementation> - </binding> - - <binding id="setting-control" extends="chrome://mozapps/content/extensions/setting.xml#setting-base"> - <content> - <xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/> - </xul:hbox> - <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/> - </xul:vbox> - <xul:hbox class="preferences-alignment"> - <children/> - </xul:hbox> - </content> - </binding> - - <binding id="setting-string" extends="chrome://mozapps/content/extensions/setting.xml#setting-base"> - <content> - <xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/> - </xul:hbox> - <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/> - </xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:textbox anonid="input" flex="1" oninput="inputChanged();" - xbl:inherits="disabled,emptytext,type=inputtype,min,max,increment,hidespinbuttons,decimalplaces,wraparound"/> - </xul:hbox> - </content> - - <implementation> - <method name="valueFromPreference"> - <body> - <![CDATA[ - this.value = Preferences.get(this.pref, ""); - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - Preferences.set(this.pref, this.value); - ]]> - </body> - </method> - </implementation> - </binding> - - <binding id="setting-color" extends="chrome://mozapps/content/extensions/setting.xml#setting-base"> - <content> - <xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/> - </xul:hbox> - <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/> - </xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:colorpicker type="button" anonid="input" xbl:inherits="disabled" onchange="document.getBindingParent(this).inputChanged();"/> - </xul:hbox> - </content> - - <implementation> - <method name="valueFromPreference"> - <body> - <![CDATA[ - // We must wait for the colorpicker's binding to be applied before setting the value - if (!this.input.color) - this.input.initialize(); - this.value = Services.prefs.getCharPref(this.pref); - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - Services.prefs.setCharPref(this.pref, this.value); - ]]> - </body> - </method> - - <property name="value" onget="return this.input.color;" onset="return this.input.color = val;"/> - </implementation> - </binding> - - <binding id="setting-path" extends="chrome://mozapps/content/extensions/setting.xml#setting-base"> - <content> - <xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/> - </xul:hbox> - <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/> - </xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:button type="button" anonid="button" label="&settings.path.button.label;" xbl:inherits="disabled" oncommand="showPicker();"/> - <xul:label anonid="input" flex="1" crop="center" xbl:inherits="disabled"/> - </xul:hbox> - </content> - - <implementation> - <method name="showPicker"> - <body> - <![CDATA[ - var filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - filePicker.init(window, this.getAttribute("title"), - this.type == "file" ? Ci.nsIFilePicker.modeOpen : Ci.nsIFilePicker.modeGetFolder); - if (this.value) { - try { - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.initWithPath(this.value); - filePicker.displayDirectory = this.type == "file" ? file.parent : file; - if (this.type == "file") { - filePicker.defaultString = file.leafName; - } - } catch (e) {} - } - if (filePicker.show() != Ci.nsIFilePicker.returnCancel) { - this.value = filePicker.file.path; - this.inputChanged(); - } - ]]> - </body> - </method> - - <method name="valueFromPreference"> - <body> - <![CDATA[ - this.value = Preferences.get(this.pref, ""); - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - Preferences.set(this.pref, this.value); - ]]> - </body> - </method> - - <field name="_value"></field> - - <property name="value"> - <getter> - <![CDATA[ - return this._value; - ]]> - </getter> - <setter> - <![CDATA[ - this._value = val; - let label = ""; - if (val) { - try { - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.initWithPath(val); - label = this.hasAttribute("fullpath") ? file.path : file.leafName; - } catch (e) {} - } - this.input.tooltipText = val; - return this.input.value = label; - ]]> - </setter> - </property> - </implementation> - </binding> - - <binding id="setting-multi" extends="chrome://mozapps/content/extensions/setting.xml#setting-base"> - <content> - <xul:vbox> - <xul:hbox class="preferences-alignment"> - <xul:label class="preferences-title" flex="1" xbl:inherits="xbl:text=title"/> - </xul:hbox> - <xul:description class="preferences-description" flex="1" xbl:inherits="xbl:text=desc"/> - </xul:vbox> - <xul:hbox class="preferences-alignment"> - <children includes="radiogroup|menulist"/> - </xul:hbox> - </content> - - <implementation> - <constructor> - <![CDATA[ - this.control.addEventListener("command", this.inputChanged.bind(this), false); - ]]> - </constructor> - - <method name="valueFromPreference"> - <body> - <![CDATA[ - let val = Preferences.get(this.pref, "").toString(); - - if ("itemCount" in this.control) { - for (let i = 0; i < this.control.itemCount; i++) { - if (this.control.getItemAtIndex(i).value == val) { - this.control.selectedIndex = i; - break; - } - } - } else { - this.control.setAttribute("value", val); - } - ]]> - </body> - </method> - - <method name="valueToPreference"> - <body> - <![CDATA[ - // We might not have a pref already set, so we guess the type from the value attribute - let val = this.control.selectedItem.value; - if (val == "true" || val == "false") { - val = val == "true"; - } else if (/^-?\d+$/.test(val)) { - val = parseInt(val, 10); - } - Preferences.set(this.pref, val); - ]]> - </body> - </method> - - <field name="control">this.getElementsByTagName(this.getAttribute("type") == "radio" ? "radiogroup" : "menulist")[0];</field> - </implementation> - </binding> -</bindings> diff --git a/toolkit/mozapps/webextensions/content/update.js b/toolkit/mozapps/webextensions/content/update.js deleted file mode 100644 index 80d0fa688..000000000 --- a/toolkit/mozapps/webextensions/content/update.js +++ /dev/null @@ -1,663 +0,0 @@ -// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- - -/* 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 UI is only opened from the Extension Manager when the app is upgraded. - -"use strict"; - -const PREF_UPDATE_EXTENSIONS_ENABLED = "extensions.update.enabled"; -const PREF_XPINSTALL_ENABLED = "xpinstall.enabled"; - -// timeout (in milliseconds) to wait for response to the metadata ping -const METADATA_TIMEOUT = 30000; - -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modules/addons/AddonRepository.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/Log.jsm"); -var logger = null; - -var gUpdateWizard = { - // When synchronizing app compatibility info this contains all installed - // add-ons. When checking for compatible versions this contains only - // incompatible add-ons. - addons: [], - // Contains a Set of IDs for add-on that were disabled by the application update. - affectedAddonIDs: null, - // The add-ons that we found updates available for - addonsToUpdate: [], - shouldSuggestAutoChecking: false, - shouldAutoCheck: false, - xpinstallEnabled: true, - xpinstallLocked: false, - // cached AddonInstall entries for add-ons we might want to update, - // keyed by add-on ID - addonInstalls: new Map(), - shuttingDown: false, - // Count the add-ons disabled by this update, enabled/disabled by - // metadata checks, and upgraded. - disabled: 0, - metadataEnabled: 0, - metadataDisabled: 0, - upgraded: 0, - upgradeFailed: 0, - upgradeDeclined: 0, - - init: function() - { - logger = Log.repository.getLogger("addons.update-dialog"); - // XXX could we pass the addons themselves rather than the IDs? - this.affectedAddonIDs = new Set(window.arguments[0]); - - try { - this.shouldSuggestAutoChecking = - !Services.prefs.getBoolPref(PREF_UPDATE_EXTENSIONS_ENABLED); - } - catch (e) { - } - - try { - this.xpinstallEnabled = Services.prefs.getBoolPref(PREF_XPINSTALL_ENABLED); - this.xpinstallLocked = Services.prefs.prefIsLocked(PREF_XPINSTALL_ENABLED); - } - catch (e) { - } - - if (Services.io.offline) - document.documentElement.currentPage = document.getElementById("offline"); - else - document.documentElement.currentPage = document.getElementById("versioninfo"); - }, - - onWizardFinish: function gUpdateWizard_onWizardFinish () - { - if (this.shouldSuggestAutoChecking) - Services.prefs.setBoolPref(PREF_UPDATE_EXTENSIONS_ENABLED, this.shouldAutoCheck); - }, - - _setUpButton: function(aButtonID, aButtonKey, aDisabled) - { - var strings = document.getElementById("updateStrings"); - var button = document.documentElement.getButton(aButtonID); - if (aButtonKey) { - button.label = strings.getString(aButtonKey); - try { - button.setAttribute("accesskey", strings.getString(aButtonKey + "Accesskey")); - } - catch (e) { - } - } - button.disabled = aDisabled; - }, - - setButtonLabels: function(aBackButton, aBackButtonIsDisabled, - aNextButton, aNextButtonIsDisabled, - aCancelButton, aCancelButtonIsDisabled) - { - this._setUpButton("back", aBackButton, aBackButtonIsDisabled); - this._setUpButton("next", aNextButton, aNextButtonIsDisabled); - this._setUpButton("cancel", aCancelButton, aCancelButtonIsDisabled); - }, - - // Update Errors - errorItems: [], - - checkForErrors: function(aElementIDToShow) - { - if (this.errorItems.length > 0) - document.getElementById(aElementIDToShow).hidden = false; - }, - - onWizardClose: function(aEvent) - { - return this.onWizardCancel(); - }, - - onWizardCancel: function() - { - gUpdateWizard.shuttingDown = true; - // Allow add-ons to continue downloading and installing - // in the background, though some may require a later restart - // Pages that are waiting for user input go into the background - // on cancel - if (gMismatchPage.waiting) { - logger.info("Dialog closed in mismatch page"); - if (gUpdateWizard.addonInstalls.size > 0) { - gInstallingPage.startInstalls( - Array.from(gUpdateWizard.addonInstalls.values())); - } - return true; - } - - // Pages that do asynchronous things will just keep running and check - // gUpdateWizard.shuttingDown to trigger background behaviour - if (!gInstallingPage.installing) { - logger.info("Dialog closed while waiting for updated compatibility information"); - } - else { - logger.info("Dialog closed while downloading and installing updates"); - } - return true; - } -}; - -var gOfflinePage = { - onPageAdvanced: function() - { - Services.io.offline = false; - return true; - }, - - toggleOffline: function() - { - var nextbtn = document.documentElement.getButton("next"); - nextbtn.disabled = !nextbtn.disabled; - } -} - -// Addon listener to count addons enabled/disabled by metadata checks -var listener = { - onDisabled: function(aAddon) { - gUpdateWizard.affectedAddonIDs.add(aAddon.id); - gUpdateWizard.metadataDisabled++; - }, - onEnabled: function(aAddon) { - gUpdateWizard.affectedAddonIDs.delete(aAddon.id); - gUpdateWizard.metadataEnabled++; - } -}; - -var gVersionInfoPage = { - _completeCount: 0, - _totalCount: 0, - _versionInfoDone: false, - onPageShow: Task.async(function*() { - gUpdateWizard.setButtonLabels(null, true, - "nextButtonText", true, - "cancelButtonText", false); - - gUpdateWizard.disabled = gUpdateWizard.affectedAddonIDs.size; - - // Ensure compatibility overrides are up to date before checking for - // individual addon updates. - AddonManager.addAddonListener(listener); - if (AddonRepository.isMetadataStale()) { - // Do the metadata ping, listening for any newly enabled/disabled add-ons. - yield AddonRepository.repopulateCache(METADATA_TIMEOUT); - if (gUpdateWizard.shuttingDown) { - logger.debug("repopulateCache completed after dialog closed"); - } - } - // Fetch the add-ons that are still affected by this update, - // excluding the hotfix add-on. - let idlist = Array.from(gUpdateWizard.affectedAddonIDs).filter( - a => a.id != AddonManager.hotfixID); - if (idlist.length < 1) { - gVersionInfoPage.onAllUpdatesFinished(); - return; - } - - logger.debug("Fetching affected addons " + idlist.toSource()); - let fetchedAddons = yield new Promise((resolve, reject) => - AddonManager.getAddonsByIDs(idlist, resolve)); - // We shouldn't get nulls here, but let's be paranoid... - gUpdateWizard.addons = fetchedAddons.filter(a => a); - if (gUpdateWizard.addons.length < 1) { - gVersionInfoPage.onAllUpdatesFinished(); - return; - } - - gVersionInfoPage._totalCount = gUpdateWizard.addons.length; - - for (let addon of gUpdateWizard.addons) { - logger.debug("VersionInfo Finding updates for ${id}", addon); - addon.findUpdates(gVersionInfoPage, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED); - } - }), - - onAllUpdatesFinished: function() { - AddonManager.removeAddonListener(listener); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_disabled", - gUpdateWizard.disabled); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_metadata_enabled", - gUpdateWizard.metadataEnabled); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_metadata_disabled", - gUpdateWizard.metadataDisabled); - // Record 0 for these here in case we exit early; values will be replaced - // later if we actually upgrade any. - AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgraded", 0); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeFailed", 0); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeDeclined", 0); - // Filter out any add-ons that are now enabled. - let addonList = gUpdateWizard.addons.map(a => a.id + ":" + a.appDisabled); - logger.debug("VersionInfo updates finished: found " + addonList.toSource()); - let filteredAddons = []; - for (let a of gUpdateWizard.addons) { - if (a.appDisabled) { - logger.debug("Continuing with add-on " + a.id); - filteredAddons.push(a); - } - else if (gUpdateWizard.addonInstalls.has(a.id)) { - gUpdateWizard.addonInstalls.get(a.id).cancel(); - gUpdateWizard.addonInstalls.delete(a.id); - } - } - gUpdateWizard.addons = filteredAddons; - - if (gUpdateWizard.shuttingDown) { - // jump directly to updating auto-update add-ons in the background - if (gUpdateWizard.addonInstalls.size > 0) { - let installs = Array.from(gUpdateWizard.addonInstalls.values()); - gInstallingPage.startInstalls(installs); - } - return; - } - - if (filteredAddons.length > 0) { - if (!gUpdateWizard.xpinstallEnabled && gUpdateWizard.xpinstallLocked) { - document.documentElement.currentPage = document.getElementById("adminDisabled"); - return; - } - document.documentElement.currentPage = document.getElementById("mismatch"); - } - else { - logger.info("VersionInfo: No updates require further action"); - // VersionInfo compatibility updates resolved all compatibility problems, - // close this window and continue starting the application... - // XXX Bug 314754 - We need to use setTimeout to close the window due to - // the EM using xmlHttpRequest when checking for updates. - setTimeout(close, 0); - } - }, - - // UpdateListener - onUpdateFinished: function(aAddon, status) { - ++this._completeCount; - - if (status != AddonManager.UPDATE_STATUS_NO_ERROR) { - logger.debug("VersionInfo update " + this._completeCount + " of " + this._totalCount + - " failed for " + aAddon.id + ": " + status); - gUpdateWizard.errorItems.push(aAddon); - } - else { - logger.debug("VersionInfo update " + this._completeCount + " of " + this._totalCount + - " finished for " + aAddon.id); - } - - // If we're not in the background, just make a list of add-ons that have - // updates available - if (!gUpdateWizard.shuttingDown) { - // If we're still in the update check window and the add-on is now active - // then it won't have been disabled by startup - if (aAddon.active) { - AddonManagerPrivate.removeStartupChange(AddonManager.STARTUP_CHANGE_DISABLED, aAddon.id); - gUpdateWizard.metadataEnabled++; - } - - // Update the status text and progress bar - var updateStrings = document.getElementById("updateStrings"); - var statusElt = document.getElementById("versioninfo.status"); - var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]); - statusElt.setAttribute("value", statusString); - - // Update the status text and progress bar - var progress = document.getElementById("versioninfo.progress"); - progress.mode = "normal"; - progress.value = Math.ceil((this._completeCount / this._totalCount) * 100); - } - - if (this._completeCount == this._totalCount) - this.onAllUpdatesFinished(); - }, - - onUpdateAvailable: function(aAddon, aInstall) { - logger.debug("VersionInfo got an install for " + aAddon.id + ": " + aAddon.version); - gUpdateWizard.addonInstalls.set(aAddon.id, aInstall); - }, -}; - -var gMismatchPage = { - waiting: false, - - onPageShow: function() - { - gMismatchPage.waiting = true; - gUpdateWizard.setButtonLabels(null, true, - "mismatchCheckNow", false, - "mismatchDontCheck", false); - document.documentElement.getButton("next").focus(); - - var incompatible = document.getElementById("mismatch.incompatible"); - for (let addon of gUpdateWizard.addons) { - var listitem = document.createElement("listitem"); - listitem.setAttribute("label", addon.name + " " + addon.version); - incompatible.appendChild(listitem); - } - } -}; - -var gUpdatePage = { - _totalCount: 0, - _completeCount: 0, - onPageShow: function() - { - gMismatchPage.waiting = false; - gUpdateWizard.setButtonLabels(null, true, - "nextButtonText", true, - "cancelButtonText", false); - document.documentElement.getButton("next").focus(); - - gUpdateWizard.errorItems = []; - - this._totalCount = gUpdateWizard.addons.length; - for (let addon of gUpdateWizard.addons) { - logger.debug("UpdatePage requesting update for " + addon.id); - // Redundant call to find updates again here when we already got them - // in the VersionInfo page: https://bugzilla.mozilla.org/show_bug.cgi?id=960597 - addon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED); - } - }, - - onAllUpdatesFinished: function() { - if (gUpdateWizard.shuttingDown) - return; - - var nextPage = document.getElementById("noupdates"); - if (gUpdateWizard.addonsToUpdate.length > 0) - nextPage = document.getElementById("found"); - document.documentElement.currentPage = nextPage; - }, - - // UpdateListener - onUpdateAvailable: function(aAddon, aInstall) { - logger.debug("UpdatePage got an update for " + aAddon.id + ": " + aAddon.version); - gUpdateWizard.addonsToUpdate.push(aInstall); - }, - - onUpdateFinished: function(aAddon, status) { - if (status != AddonManager.UPDATE_STATUS_NO_ERROR) - gUpdateWizard.errorItems.push(aAddon); - - ++this._completeCount; - - if (!gUpdateWizard.shuttingDown) { - // Update the status text and progress bar - var updateStrings = document.getElementById("updateStrings"); - var statusElt = document.getElementById("checking.status"); - var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]); - statusElt.setAttribute("value", statusString); - - var progress = document.getElementById("checking.progress"); - progress.value = Math.ceil((this._completeCount / this._totalCount) * 100); - } - - if (this._completeCount == this._totalCount) - this.onAllUpdatesFinished() - }, -}; - -var gFoundPage = { - onPageShow: function() - { - gUpdateWizard.setButtonLabels(null, true, - "installButtonText", false, - null, false); - - var foundUpdates = document.getElementById("found.updates"); - var itemCount = gUpdateWizard.addonsToUpdate.length; - for (let install of gUpdateWizard.addonsToUpdate) { - let listItem = foundUpdates.appendItem(install.name + " " + install.version); - listItem.setAttribute("type", "checkbox"); - listItem.setAttribute("checked", "true"); - listItem.install = install; - } - - if (!gUpdateWizard.xpinstallEnabled) { - document.getElementById("xpinstallDisabledAlert").hidden = false; - document.getElementById("enableXPInstall").focus(); - document.documentElement.getButton("next").disabled = true; - } - else { - document.documentElement.getButton("next").focus(); - document.documentElement.getButton("next").disabled = false; - } - }, - - toggleXPInstallEnable: function(aEvent) - { - var enabled = aEvent.target.checked; - gUpdateWizard.xpinstallEnabled = enabled; - var pref = Components.classes["@mozilla.org/preferences-service;1"] - .getService(Components.interfaces.nsIPrefBranch); - pref.setBoolPref(PREF_XPINSTALL_ENABLED, enabled); - this.updateNextButton(); - }, - - updateNextButton: function() - { - if (!gUpdateWizard.xpinstallEnabled) { - document.documentElement.getButton("next").disabled = true; - return; - } - - var oneChecked = false; - var foundUpdates = document.getElementById("found.updates"); - var updates = foundUpdates.getElementsByTagName("listitem"); - for (let update of updates) { - if (!update.checked) - continue; - oneChecked = true; - break; - } - - gUpdateWizard.setButtonLabels(null, true, - "installButtonText", true, - null, false); - document.getElementById("found").setAttribute("next", "installing"); - document.documentElement.getButton("next").disabled = !oneChecked; - } -}; - -var gInstallingPage = { - _installs : [], - _errors : [], - _strings : null, - _currentInstall : -1, - _installing : false, - - // Initialize fields we need for installing and tracking progress, - // and start iterating through the installations - startInstalls: function(aInstallList) { - if (!gUpdateWizard.xpinstallEnabled) { - return; - } - - let installs = Array.from(aInstallList).map(a => a.existingAddon.id); - logger.debug("Start installs for " + installs.toSource()); - this._errors = []; - this._installs = aInstallList; - this._installing = true; - this.startNextInstall(); - }, - - onPageShow: function() - { - gUpdateWizard.setButtonLabels(null, true, - "nextButtonText", true, - null, true); - - var foundUpdates = document.getElementById("found.updates"); - var updates = foundUpdates.getElementsByTagName("listitem"); - let toInstall = []; - for (let update of updates) { - if (!update.checked) { - logger.info("User chose to cancel update of " + update.label); - gUpdateWizard.upgradeDeclined++; - update.install.cancel(); - continue; - } - toInstall.push(update.install); - } - this._strings = document.getElementById("updateStrings"); - - this.startInstalls(toInstall); - }, - - startNextInstall: function() { - if (this._currentInstall >= 0) { - this._installs[this._currentInstall].removeListener(this); - } - - this._currentInstall++; - - if (this._installs.length == this._currentInstall) { - Services.obs.notifyObservers(null, "TEST:all-updates-done", null); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgraded", - gUpdateWizard.upgraded); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeFailed", - gUpdateWizard.upgradeFailed); - AddonManagerPrivate.recordSimpleMeasure("appUpdate_upgradeDeclined", - gUpdateWizard.upgradeDeclined); - this._installing = false; - if (gUpdateWizard.shuttingDown) { - return; - } - var nextPage = this._errors.length > 0 ? "installerrors" : "finished"; - document.getElementById("installing").setAttribute("next", nextPage); - document.documentElement.advance(); - return; - } - - let install = this._installs[this._currentInstall]; - - if (gUpdateWizard.shuttingDown && !AddonManager.shouldAutoUpdate(install.existingAddon)) { - logger.debug("Don't update " + install.existingAddon.id + " in background"); - gUpdateWizard.upgradeDeclined++; - install.cancel(); - this.startNextInstall(); - return; - } - install.addListener(this); - install.install(); - }, - - // InstallListener - onDownloadStarted: function(aInstall) { - if (gUpdateWizard.shuttingDown) { - return; - } - var strings = document.getElementById("updateStrings"); - var label = strings.getFormattedString("downloadingPrefix", [aInstall.name]); - var actionItem = document.getElementById("actionItem"); - actionItem.value = label; - }, - - onDownloadProgress: function(aInstall) { - if (gUpdateWizard.shuttingDown) { - return; - } - var downloadProgress = document.getElementById("downloadProgress"); - downloadProgress.value = Math.ceil(100 * aInstall.progress / aInstall.maxProgress); - }, - - onDownloadEnded: function(aInstall) { - }, - - onDownloadFailed: function(aInstall) { - this._errors.push(aInstall); - - gUpdateWizard.upgradeFailed++; - this.startNextInstall(); - }, - - onInstallStarted: function(aInstall) { - if (gUpdateWizard.shuttingDown) { - return; - } - var strings = document.getElementById("updateStrings"); - var label = strings.getFormattedString("installingPrefix", [aInstall.name]); - var actionItem = document.getElementById("actionItem"); - actionItem.value = label; - }, - - onInstallEnded: function(aInstall, aAddon) { - if (!gUpdateWizard.shuttingDown) { - // Remember that this add-on was updated during startup - AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, - aAddon.id); - } - - gUpdateWizard.upgraded++; - this.startNextInstall(); - }, - - onInstallFailed: function(aInstall) { - this._errors.push(aInstall); - - gUpdateWizard.upgradeFailed++; - this.startNextInstall(); - } -}; - -var gInstallErrorsPage = { - onPageShow: function() - { - gUpdateWizard.setButtonLabels(null, true, null, true, null, true); - document.documentElement.getButton("finish").focus(); - }, -}; - -// Displayed when there are incompatible add-ons and the xpinstall.enabled -// pref is false and locked. -var gAdminDisabledPage = { - onPageShow: function() - { - gUpdateWizard.setButtonLabels(null, true, null, true, - "cancelButtonText", true); - document.documentElement.getButton("finish").focus(); - } -}; - -// Displayed when selected add-on updates have been installed without error. -// There can still be add-ons that are not compatible and don't have an update. -var gFinishedPage = { - onPageShow: function() - { - gUpdateWizard.setButtonLabels(null, true, null, true, null, true); - document.documentElement.getButton("finish").focus(); - - if (gUpdateWizard.shouldSuggestAutoChecking) { - document.getElementById("finishedCheckDisabled").hidden = false; - gUpdateWizard.shouldAutoCheck = true; - } - else - document.getElementById("finishedCheckEnabled").hidden = false; - - document.documentElement.getButton("finish").focus(); - } -}; - -// Displayed when there are incompatible add-ons and there are no available -// updates. -var gNoUpdatesPage = { - onPageShow: function(aEvent) - { - gUpdateWizard.setButtonLabels(null, true, null, true, null, true); - if (gUpdateWizard.shouldSuggestAutoChecking) { - document.getElementById("noupdatesCheckDisabled").hidden = false; - gUpdateWizard.shouldAutoCheck = true; - } - else - document.getElementById("noupdatesCheckEnabled").hidden = false; - - gUpdateWizard.checkForErrors("updateCheckErrorNotFound"); - document.documentElement.getButton("finish").focus(); - } -}; diff --git a/toolkit/mozapps/webextensions/content/update.xul b/toolkit/mozapps/webextensions/content/update.xul deleted file mode 100644 index 745983814..000000000 --- a/toolkit/mozapps/webextensions/content/update.xul +++ /dev/null @@ -1,194 +0,0 @@ -<?xml version="1.0"?> - -# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> -<?xml-stylesheet href="chrome://mozapps/skin/extensions/update.css" type="text/css"?> - -<!DOCTYPE wizard [ -<!ENTITY % updateDTD SYSTEM "chrome://mozapps/locale/extensions/update.dtd"> -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> -%updateDTD; -%brandDTD; -]> - -<wizard id="updateWizard" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - title="&updateWizard.title;" - windowtype="Addons:Compatibility" - branded="true" - onload="gUpdateWizard.init();" - onwizardfinish="gUpdateWizard.onWizardFinish();" - onwizardcancel="return gUpdateWizard.onWizardCancel();" - onclose="return gUpdateWizard.onWizardClose(event);" - buttons="accept,cancel"> - - <script type="application/javascript" src="chrome://mozapps/content/extensions/update.js"/> - - <stringbundleset id="updateSet"> - <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/> - <stringbundle id="updateStrings" src="chrome://mozapps/locale/extensions/update.properties"/> - </stringbundleset> - - <wizardpage id="dummy" pageid="dummy"/> - - <wizardpage id="offline" pageid="offline" next="versioninfo" - label="&offline.title;" - onpageadvanced="return gOfflinePage.onPageAdvanced();"> - <description>&offline.description;</description> - <checkbox id="toggleOffline" - checked="true" - label="&offline.toggleOffline.label;" - accesskey="&offline.toggleOffline.accesskey;" - oncommand="gOfflinePage.toggleOffline();"/> - </wizardpage> - - <wizardpage id="versioninfo" pageid="versioninfo" next="mismatch" - label="&versioninfo.wizard.title;" - onpageshow="gVersionInfoPage.onPageShow();"> - <label>&versioninfo.top.label;</label> - <separator class="thin"/> - <progressmeter id="versioninfo.progress" mode="undetermined"/> - <hbox align="center"> - <image id="versioninfo.throbber" class="throbber"/> - <label flex="1" id="versioninfo.status" crop="right">&versioninfo.waiting;</label> - </hbox> - <separator/> - </wizardpage> - - <wizardpage id="mismatch" pageid="mismatch" next="checking" - label="&mismatch.win.title;" - onpageshow="gMismatchPage.onPageShow();"> - <label>&mismatch.top.label;</label> - <separator class="thin"/> - <listbox id="mismatch.incompatible" flex="1"/> - <separator class="thin"/> - <label>&mismatch.bottom.label;</label> - </wizardpage> - - <wizardpage id="checking" pageid="checking" next="noupdates" - label="&checking.wizard.title;" - onpageshow="gUpdatePage.onPageShow();"> - <label>&checking.top.label;</label> - <separator class="thin"/> - <progressmeter id="checking.progress"/> - <hbox align="center"> - <image id="checking.throbber" class="throbber"/> - <label id="checking.status" flex="1" crop="right">&checking.status;</label> - </hbox> - </wizardpage> - - <wizardpage id="noupdates" pageid="noupdates" - label="&noupdates.wizard.title;" - onpageshow="gNoUpdatesPage.onPageShow();"> - <description>&noupdates.intro.desc;</description> - <separator class="thin"/> - <hbox id="updateCheckErrorNotFound" class="alertBox" hidden="true" align="top"> - <description flex="1">&noupdates.error.desc;</description> - </hbox> - <separator class="thin"/> - <description id="noupdatesCheckEnabled" hidden="true"> - &noupdates.checkEnabled.desc; - </description> - <vbox id="noupdatesCheckDisabled" hidden="true"> - <description>&finished.checkDisabled.desc;</description> - <checkbox label="&enableChecking.label;" checked="true" - oncommand="gUpdateWizard.shouldAutoCheck = this.checked;"/> - </vbox> - <separator flex="1"/> -#ifndef XP_MACOSX - <label>&clickFinish.label;</label> -#else - <label>&clickFinish.labelMac;</label> -#endif - <separator class="thin"/> - </wizardpage> - - <wizardpage id="found" pageid="found" next="installing" - label="&found.wizard.title;" - onpageshow="gFoundPage.onPageShow();"> - <label>&found.top.label;</label> - <separator class="thin"/> - <listbox id="found.updates" flex="1" seltype="multiple" - onclick="gFoundPage.updateNextButton();"/> - <separator class="thin"/> - <vbox align="left" id="xpinstallDisabledAlert" hidden="true"> - <description>&found.disabledXPinstall.label;</description> - <checkbox label="&found.enableXPInstall.label;" - id="enableXPInstall" - accesskey="&found.enableXPInstall.accesskey;" - oncommand="gFoundPage.toggleXPInstallEnable(event);"/> - </vbox> - </wizardpage> - - <wizardpage id="installing" pageid="installing" next="finished" - label="&installing.wizard.title;" - onpageshow="gInstallingPage.onPageShow();"> - <label>&installing.top.label;</label> - <progressmeter id="downloadProgress"/> - <hbox align="center"> - <image id="installing.throbber" class="throbber"/> - <label id="actionItem" flex="1" crop="right"/> - </hbox> - <separator/> - </wizardpage> - - <wizardpage id="installerrors" pageid="installerrors" - label="&installerrors.wizard.title;" - onpageshow="gInstallErrorsPage.onPageShow();"> - <hbox align="top" class="alertBox"> - <description flex="1">&installerrors.intro.label;</description> - </hbox> - <separator flex="1"/> -#ifndef XP_MACOSX - <label>&clickFinish.label;</label> -#else - <label>&clickFinish.labelMac;</label> -#endif - <separator class="thin"/> - </wizardpage> - - <wizardpage id="adminDisabled" pageid="adminDisabled" - label="&adminDisabled.wizard.title;" - onpageshow="gAdminDisabledPage.onPageShow();"> - <separator/> - <hbox class="alertBox" align="top"> - <description flex="1">&adminDisabled.warning.label;</description> - </hbox> - <separator flex="1"/> -#ifndef XP_MACOSX - <label>&clickFinish.label;</label> -#else - <label>&clickFinish.labelMac;</label> -#endif - <separator class="thin"/> - </wizardpage> - - <wizardpage id="finished" pageid="finished" - label="&finished.wizard.title;" - onpageshow="gFinishedPage.onPageShow();"> - - <label>&finished.top.label;</label> - <separator/> - <description id="finishedCheckEnabled" hidden="true"> - &finished.checkEnabled.desc; - </description> - <vbox id="finishedCheckDisabled" hidden="true"> - <description>&finished.checkDisabled.desc;</description> - <checkbox label="&enableChecking.label;" checked="true" - oncommand="gUpdateWizard.shouldAutoCheck = this.checked;"/> - </vbox> - <separator flex="1"/> -#ifndef XP_MACOSX - <label>&clickFinish.label;</label> -#else - <label>&clickFinish.labelMac;</label> -#endif - <separator class="thin"/> - </wizardpage> - -</wizard> - diff --git a/toolkit/mozapps/webextensions/extensions.manifest b/toolkit/mozapps/webextensions/extensions.manifest deleted file mode 100644 index 2129012ab..000000000 --- a/toolkit/mozapps/webextensions/extensions.manifest +++ /dev/null @@ -1,18 +0,0 @@ -component {4399533d-08d1-458c-a87a-235f74451cfa} addonManager.js -contract @mozilla.org/addons/integration;1 {4399533d-08d1-458c-a87a-235f74451cfa} -#ifndef MOZ_WIDGET_ANDROID -category update-timer addonManager @mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400 -#endif -component {7beb3ba8-6ec3-41b4-b67c-da89b8518922} amContentHandler.js -contract @mozilla.org/uriloader/content-handler;1?type=application/x-xpinstall {7beb3ba8-6ec3-41b4-b67c-da89b8518922} -component {0f38e086-89a3-40a5-8ffc-9b694de1d04a} amWebInstallListener.js -contract @mozilla.org/addons/web-install-listener;1 {0f38e086-89a3-40a5-8ffc-9b694de1d04a} -component {9df8ef2b-94da-45c9-ab9f-132eb55fddf1} amInstallTrigger.js -contract @mozilla.org/addons/installtrigger;1 {9df8ef2b-94da-45c9-ab9f-132eb55fddf1} -category JavaScript-global-property InstallTrigger @mozilla.org/addons/installtrigger;1 -#ifndef MOZ_WIDGET_ANDROID -category addon-provider-module PluginProvider resource://gre/modules/addons/PluginProvider.jsm -#endif -category addon-provider-module GMPProvider resource://gre/modules/addons/GMPProvider.jsm -component {8866d8e3-4ea5-48b7-a891-13ba0ac15235} amWebAPI.js -contract @mozilla.org/addon-web-api/manager;1 {8866d8e3-4ea5-48b7-a891-13ba0ac15235} diff --git a/toolkit/mozapps/webextensions/internal/APIExtensionBootstrap.js b/toolkit/mozapps/webextensions/internal/APIExtensionBootstrap.js deleted file mode 100644 index 0eae2475c..000000000 --- a/toolkit/mozapps/webextensions/internal/APIExtensionBootstrap.js +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -Components.utils.import("resource://gre/modules/ExtensionManagement.jsm"); -Components.utils.import("resource://gre/modules/Services.jsm"); - -var namespace; -var resource; -var resProto; - -function install(data, reason) { -} - -function startup(data, reason) { - namespace = data.id.replace(/@.*/, ""); - resource = `extension-${namespace}-api`; - - resProto = Services.io.getProtocolHandler("resource") - .QueryInterface(Components.interfaces.nsIResProtocolHandler); - - resProto.setSubstitution(resource, data.resourceURI); - - ExtensionManagement.registerAPI( - namespace, - `resource://${resource}/schema.json`, - `resource://${resource}/api.js`); -} - -function shutdown(data, reason) { - resProto.setSubstitution(resource, null); - - ExtensionManagement.unregisterAPI(namespace); -} - -function uninstall(data, reason) { -} diff --git a/toolkit/mozapps/webextensions/internal/AddonConstants.jsm b/toolkit/mozapps/webextensions/internal/AddonConstants.jsm deleted file mode 100644 index 22d91fdf5..000000000 --- a/toolkit/mozapps/webextensions/internal/AddonConstants.jsm +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = [ "ADDON_SIGNING", "REQUIRE_SIGNING" ]; - -// Make these non-changable properties so they can't be manipulated from other -// code in the app. -Object.defineProperty(this, "ADDON_SIGNING", { - configurable: false, - enumerable: false, - writable: false, -#ifdef MOZ_ADDON_SIGNING - value: true, -#else - value: false, -#endif -}); - -Object.defineProperty(this, "REQUIRE_SIGNING", { - configurable: false, - enumerable: false, - writable: false, -#ifdef MOZ_REQUIRE_SIGNING - value: true, -#else - value: false, -#endif -}); diff --git a/toolkit/mozapps/webextensions/internal/AddonRepository.jsm b/toolkit/mozapps/webextensions/internal/AddonRepository.jsm deleted file mode 100644 index 7f88d44ad..000000000 --- a/toolkit/mozapps/webextensions/internal/AddonRepository.jsm +++ /dev/null @@ -1,1988 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; -const Cr = Components.results; - -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/AddonManager.jsm"); -/* globals AddonManagerPrivate*/ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave", - "resource://gre/modules/DeferredSave.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator", - "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest", - "resource://gre/modules/ServiceRequest.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); - - -this.EXPORTED_SYMBOLS = [ "AddonRepository" ]; - -const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; -const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types"; -const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled" -const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons"; -const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url"; -const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url"; -const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL"; -const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url"; -const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL"; -const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url"; -const PREF_GETADDONS_DB_SCHEMA = "extensions.getAddons.databaseSchema" - -const PREF_METADATA_LASTUPDATE = "extensions.getAddons.cache.lastUpdate"; -const PREF_METADATA_UPDATETHRESHOLD_SEC = "extensions.getAddons.cache.updateThreshold"; -const DEFAULT_METADATA_UPDATETHRESHOLD_SEC = 172800; // two days - -const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"; - -const API_VERSION = "1.5"; -const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary"; - -const KEY_PROFILEDIR = "ProfD"; -const FILE_DATABASE = "addons.json"; -const DB_SCHEMA = 5; -const DB_MIN_JSON_SCHEMA = 5; -const DB_BATCH_TIMEOUT_MS = 50; - -const BLANK_DB = function() { - return { - addons: new Map(), - schema: DB_SCHEMA - }; -} - -const TOOLKIT_ID = "toolkit@mozilla.org"; - -Cu.import("resource://gre/modules/Log.jsm"); -const LOGGER_ID = "addons.repository"; - -// Create a new logger for use by the Addons Repository -// (Requires AddonManager.jsm) -var logger = Log.repository.getLogger(LOGGER_ID); - -// A map between XML keys to AddonSearchResult keys for string values -// that require no extra parsing from XML -const STRING_KEY_MAP = { - name: "name", - version: "version", - homepage: "homepageURL", - support: "supportURL" -}; - -// A map between XML keys to AddonSearchResult keys for string values -// that require parsing from HTML -const HTML_KEY_MAP = { - summary: "description", - description: "fullDescription", - developer_comments: "developerComments", - eula: "eula" -}; - -// A map between XML keys to AddonSearchResult keys for integer values -// that require no extra parsing from XML -const INTEGER_KEY_MAP = { - total_downloads: "totalDownloads", - weekly_downloads: "weeklyDownloads", - daily_users: "dailyUsers" -}; - -function convertHTMLToPlainText(html) { - if (!html) - return html; - var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"]. - createInstance(Ci.nsIFormatConverter); - - var input = Cc["@mozilla.org/supports-string;1"]. - createInstance(Ci.nsISupportsString); - input.data = html.replace(/\n/g, "<br>"); - - var output = {}; - converter.convert("text/html", input, input.data.length, "text/unicode", - output, {}); - - if (output.value instanceof Ci.nsISupportsString) - return output.value.data.replace(/\r\n/g, "\n"); - return html; -} - -function getAddonsToCache(aIds, aCallback) { - try { - var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES); - } - catch (e) { } - if (!types) - types = DEFAULT_CACHE_TYPES; - - types = types.split(","); - - AddonManager.getAddonsByIDs(aIds, function(aAddons) { - let enabledIds = []; - for (var i = 0; i < aIds.length; i++) { - var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]); - try { - if (!Services.prefs.getBoolPref(preference)) - continue; - } catch (e) { - // If the preference doesn't exist caching is enabled by default - } - - // The add-ons manager may not know about this ID yet if it is a pending - // install. In that case we'll just cache it regardless - if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1)) - continue; - - enabledIds.push(aIds[i]); - } - - aCallback(enabledIds); - }); -} - -function AddonSearchResult(aId) { - this.id = aId; - this.icons = {}; - this._unsupportedProperties = {}; -} - -AddonSearchResult.prototype = { - /** - * The ID of the add-on - */ - id: null, - - /** - * The add-on type (e.g. "extension" or "theme") - */ - type: null, - - /** - * The name of the add-on - */ - name: null, - - /** - * The version of the add-on - */ - version: null, - - /** - * The creator of the add-on - */ - creator: null, - - /** - * The developers of the add-on - */ - developers: null, - - /** - * A short description of the add-on - */ - description: null, - - /** - * The full description of the add-on - */ - fullDescription: null, - - /** - * The developer comments for the add-on. This includes any information - * that may be helpful to end users that isn't necessarily applicable to - * the add-on description (e.g. known major bugs) - */ - developerComments: null, - - /** - * The end-user licensing agreement (EULA) of the add-on - */ - eula: null, - - /** - * The url of the add-on's icon - */ - get iconURL() { - return this.icons && this.icons[32]; - }, - - /** - * The URLs of the add-on's icons, as an object with icon size as key - */ - icons: null, - - /** - * An array of screenshot urls for the add-on - */ - screenshots: null, - - /** - * The homepage for the add-on - */ - homepageURL: null, - - /** - * The homepage for the add-on - */ - learnmoreURL: null, - - /** - * The support URL for the add-on - */ - supportURL: null, - - /** - * The contribution url of the add-on - */ - contributionURL: null, - - /** - * The suggested contribution amount - */ - contributionAmount: null, - - /** - * The URL to visit in order to purchase the add-on - */ - purchaseURL: null, - - /** - * The numerical cost of the add-on in some currency, for sorting purposes - * only - */ - purchaseAmount: null, - - /** - * The display cost of the add-on, for display purposes only - */ - purchaseDisplayAmount: null, - - /** - * The rating of the add-on, 0-5 - */ - averageRating: null, - - /** - * The number of reviews for this add-on - */ - reviewCount: null, - - /** - * The URL to the list of reviews for this add-on - */ - reviewURL: null, - - /** - * The total number of times the add-on was downloaded - */ - totalDownloads: null, - - /** - * The number of times the add-on was downloaded the current week - */ - weeklyDownloads: null, - - /** - * The number of daily users for the add-on - */ - dailyUsers: null, - - /** - * AddonInstall object generated from the add-on XPI url - */ - install: null, - - /** - * nsIURI storing where this add-on was installed from - */ - sourceURI: null, - - /** - * The status of the add-on in the repository (e.g. 4 = "Public") - */ - repositoryStatus: null, - - /** - * The size of the add-on's files in bytes. For an add-on that have not yet - * been downloaded this may be an estimated value. - */ - size: null, - - /** - * The Date that the add-on was most recently updated - */ - updateDate: null, - - /** - * True or false depending on whether the add-on is compatible with the - * current version of the application - */ - isCompatible: true, - - /** - * True or false depending on whether the add-on is compatible with the - * current platform - */ - isPlatformCompatible: true, - - /** - * Array of AddonCompatibilityOverride objects, that describe overrides for - * compatibility with an application versions. - **/ - compatibilityOverrides: null, - - /** - * True if the add-on has a secure means of updating - */ - providesUpdatesSecurely: true, - - /** - * The current blocklist state of the add-on - */ - blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED, - - /** - * True if this add-on cannot be used in the application based on version - * compatibility, dependencies and blocklisting - */ - appDisabled: false, - - /** - * True if the user wants this add-on to be disabled - */ - userDisabled: false, - - /** - * Indicates what scope the add-on is installed in, per profile, user, - * system or application - */ - scope: AddonManager.SCOPE_PROFILE, - - /** - * True if the add-on is currently functional - */ - isActive: true, - - /** - * A bitfield holding all of the current operations that are waiting to be - * performed for this add-on - */ - pendingOperations: AddonManager.PENDING_NONE, - - /** - * A bitfield holding all the the operations that can be performed on - * this add-on - */ - permissions: 0, - - /** - * Tests whether this add-on is known to be compatible with a - * particular application and platform version. - * - * @param appVersion - * An application version to test against - * @param platformVersion - * A platform version to test against - * @return Boolean representing if the add-on is compatible - */ - isCompatibleWith: function(aAppVersion, aPlatformVersion) { - return true; - }, - - /** - * Starts an update check for this add-on. This will perform - * asynchronously and deliver results to the given listener. - * - * @param aListener - * An UpdateListener for the update process - * @param aReason - * A reason code for performing the update - * @param aAppVersion - * An application version to check for updates for - * @param aPlatformVersion - * A platform version to check for updates for - */ - findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) { - if ("onNoCompatibilityUpdateAvailable" in aListener) - aListener.onNoCompatibilityUpdateAvailable(this); - if ("onNoUpdateAvailable" in aListener) - aListener.onNoUpdateAvailable(this); - if ("onUpdateFinished" in aListener) - aListener.onUpdateFinished(this); - }, - - toJSON: function() { - let json = {}; - - for (let property of Object.keys(this)) { - let value = this[property]; - if (property.startsWith("_") || - typeof(value) === "function") - continue; - - try { - switch (property) { - case "sourceURI": - json.sourceURI = value ? value.spec : ""; - break; - - case "updateDate": - json.updateDate = value ? value.getTime() : ""; - break; - - default: - json[property] = value; - } - } catch (ex) { - logger.warn("Error writing property value for " + property); - } - } - - for (let property of Object.keys(this._unsupportedProperties)) { - let value = this._unsupportedProperties[property]; - if (!property.startsWith("_")) - json[property] = value; - } - - return json; - } -} - -/** - * The add-on repository is a source of add-ons that can be installed. It can - * be searched in three ways. The first takes a list of IDs and returns a - * list of the corresponding add-ons. The second returns a list of add-ons that - * come highly recommended. This list should change frequently. The third is to - * search for specific search terms entered by the user. Searches are - * asynchronous and results should be passed to the provided callback object - * when complete. The results passed to the callback should only include add-ons - * that are compatible with the current application and are not already - * installed. - */ -this.AddonRepository = { - /** - * Whether caching is currently enabled - */ - get cacheEnabled() { - let preference = PREF_GETADDONS_CACHE_ENABLED; - let enabled = false; - try { - enabled = Services.prefs.getBoolPref(preference); - } catch (e) { - logger.warn("cacheEnabled: Couldn't get pref: " + preference); - } - - return enabled; - }, - - // A cache of the add-ons stored in the database - _addons: null, - - // Whether a search is currently in progress - _searching: false, - - // XHR associated with the current request - _request: null, - - /* - * Addon search results callback object that contains two functions - * - * searchSucceeded - Called when a search has suceeded. - * - * @param aAddons - * An array of the add-on results. In the case of searching for - * specific terms the ordering of results may be determined by - * the search provider. - * @param aAddonCount - * The length of aAddons - * @param aTotalResults - * The total results actually available in the repository - * - * - * searchFailed - Called when an error occurred when performing a search. - */ - _callback: null, - - // Maximum number of results to return - _maxResults: null, - - /** - * Shut down AddonRepository - * return: promise{integer} resolves with the result of flushing - * the AddonRepository database - */ - shutdown: function() { - this.cancelSearch(); - - this._addons = null; - return AddonDatabase.shutdown(false); - }, - - metadataAge: function() { - let now = Math.round(Date.now() / 1000); - - let lastUpdate = 0; - try { - lastUpdate = Services.prefs.getIntPref(PREF_METADATA_LASTUPDATE); - } catch (e) {} - - // Handle clock jumps - if (now < lastUpdate) { - return now; - } - return now - lastUpdate; - }, - - isMetadataStale: function() { - let threshold = DEFAULT_METADATA_UPDATETHRESHOLD_SEC; - try { - threshold = Services.prefs.getIntPref(PREF_METADATA_UPDATETHRESHOLD_SEC); - } catch (e) {} - return (this.metadataAge() > threshold); - }, - - /** - * Asynchronously get a cached add-on by id. The add-on (or null if the - * add-on is not found) is passed to the specified callback. If caching is - * disabled, null is passed to the specified callback. - * - * @param aId - * The id of the add-on to get - * @param aCallback - * The callback to pass the result back to - */ - getCachedAddonByID: Task.async(function*(aId, aCallback) { - if (!aId || !this.cacheEnabled) { - aCallback(null); - return; - } - - function getAddon(aAddons) { - aCallback(aAddons.get(aId) || null); - } - - if (this._addons == null) { - AddonDatabase.retrieveStoredData().then(aAddons => { - this._addons = aAddons; - getAddon(aAddons); - }); - - return; - } - - getAddon(this._addons); - }), - - /** - * Asynchronously repopulate cache so it only contains the add-ons - * corresponding to the specified ids. If caching is disabled, - * the cache is completely removed. - * - * @param aTimeout - * (Optional) timeout in milliseconds to abandon the XHR request - * if we have not received a response from the server. - * @return Promise{null} - * Resolves when the metadata ping is complete - */ - repopulateCache: function(aTimeout) { - return this._repopulateCacheInternal(false, aTimeout); - }, - - /* - * Clear and delete the AddonRepository database - * @return Promise{null} resolves when the database is deleted - */ - _clearCache: function() { - this._addons = null; - return AddonDatabase.delete().then(() => - new Promise((resolve, reject) => - AddonManagerPrivate.updateAddonRepositoryData(resolve)) - ); - }, - - _repopulateCacheInternal: Task.async(function*(aSendPerformance, aTimeout) { - let allAddons = yield new Promise((resolve, reject) => - AddonManager.getAllAddons(resolve)); - - // Filter the hotfix out of our list of add-ons - allAddons = allAddons.filter(a => a.id != AddonManager.hotfixID); - - // Completely remove cache if caching is not enabled - if (!this.cacheEnabled) { - logger.debug("Clearing cache because it is disabled"); - yield this._clearCache(); - return; - } - - let ids = allAddons.map(a => a.id); - logger.debug("Repopulate add-on cache with " + ids.toSource()); - - let addonsToCache = yield new Promise((resolve, reject) => - getAddonsToCache(ids, resolve)); - - // Completely remove cache if there are no add-ons to cache - if (addonsToCache.length == 0) { - logger.debug("Clearing cache because 0 add-ons were requested"); - yield this._clearCache(); - return; - } - - yield new Promise((resolve, reject) => - this._beginGetAddons(addonsToCache, { - searchSucceeded: aAddons => { - this._addons = new Map(); - for (let addon of aAddons) { - this._addons.set(addon.id, addon); - } - AddonDatabase.repopulate(aAddons, resolve); - }, - searchFailed: () => { - logger.warn("Search failed when repopulating cache"); - resolve(); - } - }, aSendPerformance, aTimeout)); - - // Always call AddonManager updateAddonRepositoryData after we refill the cache - yield new Promise((resolve, reject) => - AddonManagerPrivate.updateAddonRepositoryData(resolve)); - }), - - /** - * Asynchronously add add-ons to the cache corresponding to the specified - * ids. If caching is disabled, the cache is unchanged and the callback is - * immediately called if it is defined. - * - * @param aIds - * The array of add-on ids to add to the cache - * @param aCallback - * The optional callback to call once complete - */ - cacheAddons: function(aIds, aCallback) { - logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource()); - if (!this.cacheEnabled) { - if (aCallback) - aCallback(); - return; - } - - getAddonsToCache(aIds, aAddons => { - // If there are no add-ons to cache, act as if caching is disabled - if (aAddons.length == 0) { - if (aCallback) - aCallback(); - return; - } - - this.getAddonsByIDs(aAddons, { - searchSucceeded: aAddons => { - for (let addon of aAddons) { - this._addons.set(addon.id, addon); - } - AddonDatabase.insertAddons(aAddons, aCallback); - }, - searchFailed: () => { - logger.warn("Search failed when adding add-ons to cache"); - if (aCallback) - aCallback(); - } - }); - }); - }, - - /** - * The homepage for visiting this repository. If the corresponding preference - * is not defined, defaults to about:blank. - */ - get homepageURL() { - let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {}); - return (url != null) ? url : "about:blank"; - }, - - /** - * Returns whether this instance is currently performing a search. New - * searches will not be performed while this is the case. - */ - get isSearching() { - return this._searching; - }, - - /** - * The url that can be visited to see recommended add-ons in this repository. - * If the corresponding preference is not defined, defaults to about:blank. - */ - getRecommendedURL: function() { - let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {}); - return (url != null) ? url : "about:blank"; - }, - - /** - * Retrieves the url that can be visited to see search results for the given - * terms. If the corresponding preference is not defined, defaults to - * about:blank. - * - * @param aSearchTerms - * Search terms used to search the repository - */ - getSearchURL: function(aSearchTerms) { - let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, { - TERMS : encodeURIComponent(aSearchTerms) - }); - return (url != null) ? url : "about:blank"; - }, - - /** - * Cancels the search in progress. If there is no search in progress this - * does nothing. - */ - cancelSearch: function() { - this._searching = false; - if (this._request) { - this._request.abort(); - this._request = null; - } - this._callback = null; - }, - - /** - * Begins a search for add-ons in this repository by ID. Results will be - * passed to the given callback. - * - * @param aIDs - * The array of ids to search for - * @param aCallback - * The callback to pass results to - */ - getAddonsByIDs: function(aIDs, aCallback) { - return this._beginGetAddons(aIDs, aCallback, false); - }, - - /** - * Begins a search of add-ons, potentially sending performance data. - * - * @param aIDs - * Array of ids to search for. - * @param aCallback - * Function to pass results to. - * @param aSendPerformance - * Boolean indicating whether to send performance data with the - * request. - * @param aTimeout - * (Optional) timeout in milliseconds to abandon the XHR request - * if we have not received a response from the server. - */ - _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) { - let ids = aIDs.slice(0); - - let params = { - API_VERSION : API_VERSION, - IDS : ids.map(encodeURIComponent).join(',') - }; - - let pref = PREF_GETADDONS_BYIDS; - - if (aSendPerformance) { - let type = Services.prefs.getPrefType(PREF_GETADDONS_BYIDS_PERFORMANCE); - if (type == Services.prefs.PREF_STRING) { - pref = PREF_GETADDONS_BYIDS_PERFORMANCE; - - let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"]. - getService(Ci.nsIAppStartup). - getStartupInfo(); - - params.TIME_MAIN = ""; - params.TIME_FIRST_PAINT = ""; - params.TIME_SESSION_RESTORED = ""; - if (startupInfo.process) { - if (startupInfo.main) { - params.TIME_MAIN = startupInfo.main - startupInfo.process; - } - if (startupInfo.firstPaint) { - params.TIME_FIRST_PAINT = startupInfo.firstPaint - - startupInfo.process; - } - if (startupInfo.sessionRestored) { - params.TIME_SESSION_RESTORED = startupInfo.sessionRestored - - startupInfo.process; - } - } - } - } - - let url = this._formatURLPref(pref, params); - - let handleResults = (aElements, aTotalResults, aCompatData) => { - // Don't use this._parseAddons() so that, for example, - // incompatible add-ons are not filtered out - let results = []; - for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) { - let result = this._parseAddon(aElements[i], null, aCompatData); - if (result == null) - continue; - - // Ignore add-on if it wasn't actually requested - let idIndex = ids.indexOf(result.addon.id); - if (idIndex == -1) - continue; - - // Ignore add-on if the add-on manager doesn't know about its type: - if (!(result.addon.type in AddonManager.addonTypes)) { - continue; - } - - results.push(result); - // Ignore this add-on from now on - ids.splice(idIndex, 1); - } - - // Include any compatibility overrides for addons not hosted by the - // remote repository. - for (let id in aCompatData) { - let addonCompat = aCompatData[id]; - if (addonCompat.hosted) - continue; - - let addon = new AddonSearchResult(addonCompat.id); - // Compatibility overrides can only be for extensions. - addon.type = "extension"; - addon.compatibilityOverrides = addonCompat.compatRanges; - let result = { - addon: addon, - xpiURL: null, - xpiHash: null - }; - results.push(result); - } - - // aTotalResults irrelevant - this._reportSuccess(results, -1); - } - - this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout); - }, - - /** - * Performs the daily background update check. - * - * This API both searches for the add-on IDs specified and sends performance - * data. It is meant to be called as part of the daily update ping. It should - * not be used for any other purpose. Use repopulateCache instead. - * - * @return Promise{null} Resolves when the metadata update is complete. - */ - backgroundUpdateCheck: function() { - return this._repopulateCacheInternal(true); - }, - - /** - * Begins a search for recommended add-ons in this repository. Results will - * be passed to the given callback. - * - * @param aMaxResults - * The maximum number of results to return - * @param aCallback - * The callback to pass results to - */ - retrieveRecommendedAddons: function(aMaxResults, aCallback) { - let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, { - API_VERSION : API_VERSION, - - // Get twice as many results to account for potential filtering - MAX_RESULTS : 2 * aMaxResults - }); - - let handleResults = (aElements, aTotalResults) => { - this._getLocalAddonIds(aLocalAddonIds => { - // aTotalResults irrelevant - this._parseAddons(aElements, -1, aLocalAddonIds); - }); - } - - this._beginSearch(url, aMaxResults, aCallback, handleResults); - }, - - /** - * Begins a search for add-ons in this repository. Results will be passed to - * the given callback. - * - * @param aSearchTerms - * The terms to search for - * @param aMaxResults - * The maximum number of results to return - * @param aCallback - * The callback to pass results to - */ - searchAddons: function(aSearchTerms, aMaxResults, aCallback) { - let compatMode = "normal"; - if (!AddonManager.checkCompatibility) - compatMode = "ignore"; - else if (AddonManager.strictCompatibility) - compatMode = "strict"; - - let substitutions = { - API_VERSION : API_VERSION, - TERMS : encodeURIComponent(aSearchTerms), - // Get twice as many results to account for potential filtering - MAX_RESULTS : 2 * aMaxResults, - COMPATIBILITY_MODE : compatMode, - }; - - let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions); - - let handleResults = (aElements, aTotalResults) => { - this._getLocalAddonIds(aLocalAddonIds => { - this._parseAddons(aElements, aTotalResults, aLocalAddonIds); - }); - } - - this._beginSearch(url, aMaxResults, aCallback, handleResults); - }, - - // Posts results to the callback - _reportSuccess: function(aResults, aTotalResults) { - this._searching = false; - this._request = null; - // The callback may want to trigger a new search so clear references early - let addons = aResults.map(result => result.addon); - let callback = this._callback; - this._callback = null; - callback.searchSucceeded(addons, addons.length, aTotalResults); - }, - - // Notifies the callback of a failure - _reportFailure: function() { - this._searching = false; - this._request = null; - // The callback may want to trigger a new search so clear references early - let callback = this._callback; - this._callback = null; - callback.searchFailed(); - }, - - // Get descendant by unique tag name. Returns null if not unique tag name. - _getUniqueDescendant: function(aElement, aTagName) { - let elementsList = aElement.getElementsByTagName(aTagName); - return (elementsList.length == 1) ? elementsList[0] : null; - }, - - // Get direct descendant by unique tag name. - // Returns null if not unique tag name. - _getUniqueDirectDescendant: function(aElement, aTagName) { - let elementsList = Array.filter(aElement.children, - aChild => aChild.tagName == aTagName); - return (elementsList.length == 1) ? elementsList[0] : null; - }, - - // Parse out trimmed text content. Returns null if text content empty. - _getTextContent: function(aElement) { - let textContent = aElement.textContent.trim(); - return (textContent.length > 0) ? textContent : null; - }, - - // Parse out trimmed text content of a descendant with the specified tag name - // Returns null if the parsing unsuccessful. - _getDescendantTextContent: function(aElement, aTagName) { - let descendant = this._getUniqueDescendant(aElement, aTagName); - return (descendant != null) ? this._getTextContent(descendant) : null; - }, - - // Parse out trimmed text content of a direct descendant with the specified - // tag name. - // Returns null if the parsing unsuccessful. - _getDirectDescendantTextContent: function(aElement, aTagName) { - let descendant = this._getUniqueDirectDescendant(aElement, aTagName); - return (descendant != null) ? this._getTextContent(descendant) : null; - }, - - /* - * Creates an AddonSearchResult by parsing an <addon> element - * - * @param aElement - * The <addon> element to parse - * @param aSkip - * Object containing ids and sourceURIs of add-ons to skip. - * @param aCompatData - * Array of parsed addon_compatibility elements to accosiate with the - * resulting AddonSearchResult. Optional. - * @return Result object containing the parsed AddonSearchResult, xpiURL and - * xpiHash if the parsing was successful. Otherwise returns null. - */ - _parseAddon: function(aElement, aSkip, aCompatData) { - let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : []; - let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : []; - - let guid = this._getDescendantTextContent(aElement, "guid"); - if (guid == null || skipIDs.indexOf(guid) != -1) - return null; - - let addon = new AddonSearchResult(guid); - let result = { - addon: addon, - xpiURL: null, - xpiHash: null - }; - - if (aCompatData && guid in aCompatData) - addon.compatibilityOverrides = aCompatData[guid].compatRanges; - - for (let node = aElement.firstChild; node; node = node.nextSibling) { - if (!(node instanceof Ci.nsIDOMElement)) - continue; - - let localName = node.localName; - - // Handle case where the wanted string value is located in text content - // but only if the content is not empty - if (localName in STRING_KEY_MAP) { - addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]]; - continue; - } - - // Handle case where the wanted string value is html located in text content - if (localName in HTML_KEY_MAP) { - addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node)); - continue; - } - - // Handle case where the wanted integer value is located in text content - if (localName in INTEGER_KEY_MAP) { - let value = parseInt(this._getTextContent(node)); - if (value >= 0) - addon[INTEGER_KEY_MAP[localName]] = value; - continue; - } - - // Handle cases that aren't as simple as grabbing the text content - switch (localName) { - case "type": - // Map AMO's type id to corresponding string - // https://github.com/mozilla/olympia/blob/master/apps/constants/base.py#L127 - // These definitions need to be updated whenever AMO adds a new type. - let id = parseInt(node.getAttribute("id")); - switch (id) { - case 1: - addon.type = "extension"; - break; - case 2: - addon.type = "theme"; - break; - case 3: - addon.type = "dictionary"; - break; - case 4: - addon.type = "search"; - break; - case 5: - case 6: - addon.type = "locale"; - break; - case 7: - addon.type = "plugin"; - break; - case 8: - addon.type = "api"; - break; - case 9: - addon.type = "lightweight-theme"; - break; - case 11: - addon.type = "webapp"; - break; - default: - logger.info("Unknown type id " + id + " found when parsing response for GUID " + guid); - } - break; - case "authors": - let authorNodes = node.getElementsByTagName("author"); - for (let authorNode of authorNodes) { - let name = this._getDescendantTextContent(authorNode, "name"); - let link = this._getDescendantTextContent(authorNode, "link"); - if (name == null || link == null) - continue; - - let author = new AddonManagerPrivate.AddonAuthor(name, link); - if (addon.creator == null) - addon.creator = author; - else { - if (addon.developers == null) - addon.developers = []; - - addon.developers.push(author); - } - } - break; - case "previews": - let previewNodes = node.getElementsByTagName("preview"); - for (let previewNode of previewNodes) { - let full = this._getUniqueDescendant(previewNode, "full"); - if (full == null) - continue; - - let fullURL = this._getTextContent(full); - let fullWidth = full.getAttribute("width"); - let fullHeight = full.getAttribute("height"); - - let thumbnailURL, thumbnailWidth, thumbnailHeight; - let thumbnail = this._getUniqueDescendant(previewNode, "thumbnail"); - if (thumbnail) { - thumbnailURL = this._getTextContent(thumbnail); - thumbnailWidth = thumbnail.getAttribute("width"); - thumbnailHeight = thumbnail.getAttribute("height"); - } - let caption = this._getDescendantTextContent(previewNode, "caption"); - let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight, - thumbnailURL, thumbnailWidth, - thumbnailHeight, caption); - - if (addon.screenshots == null) - addon.screenshots = []; - - if (previewNode.getAttribute("primary") == 1) - addon.screenshots.unshift(screenshot); - else - addon.screenshots.push(screenshot); - } - break; - case "learnmore": - addon.learnmoreURL = this._getTextContent(node); - addon.homepageURL = addon.homepageURL || addon.learnmoreURL; - break; - case "contribution_data": - let meetDevelopers = this._getDescendantTextContent(node, "meet_developers"); - let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount"); - if (meetDevelopers != null) { - addon.contributionURL = meetDevelopers; - addon.contributionAmount = suggestedAmount; - } - break - case "payment_data": - let link = this._getDescendantTextContent(node, "link"); - let amountTag = this._getUniqueDescendant(node, "amount"); - let amount = parseFloat(amountTag.getAttribute("amount")); - let displayAmount = this._getTextContent(amountTag); - if (link != null && amount != null && displayAmount != null) { - addon.purchaseURL = link; - addon.purchaseAmount = amount; - addon.purchaseDisplayAmount = displayAmount; - } - break - case "rating": - let averageRating = parseInt(this._getTextContent(node)); - if (averageRating >= 0) - addon.averageRating = Math.min(5, averageRating); - break; - case "reviews": - let url = this._getTextContent(node); - let num = parseInt(node.getAttribute("num")); - if (url != null && num >= 0) { - addon.reviewURL = url; - addon.reviewCount = num; - } - break; - case "status": - let repositoryStatus = parseInt(node.getAttribute("id")); - if (!isNaN(repositoryStatus)) - addon.repositoryStatus = repositoryStatus; - break; - case "all_compatible_os": - let nodes = node.getElementsByTagName("os"); - addon.isPlatformCompatible = Array.some(nodes, function(aNode) { - let text = aNode.textContent.toLowerCase().trim(); - return text == "all" || text == Services.appinfo.OS.toLowerCase(); - }); - break; - case "install": - // No os attribute means the xpi is compatible with any os - if (node.hasAttribute("os")) { - let os = node.getAttribute("os").trim().toLowerCase(); - // If the os is not ALL and not the current OS then ignore this xpi - if (os != "all" && os != Services.appinfo.OS.toLowerCase()) - break; - } - - let xpiURL = this._getTextContent(node); - if (xpiURL == null) - break; - - if (skipSourceURIs.indexOf(xpiURL) != -1) - return null; - - result.xpiURL = xpiURL; - addon.sourceURI = NetUtil.newURI(xpiURL); - - let size = parseInt(node.getAttribute("size")); - addon.size = (size >= 0) ? size : null; - - let xpiHash = node.getAttribute("hash"); - if (xpiHash != null) - xpiHash = xpiHash.trim(); - result.xpiHash = xpiHash ? xpiHash : null; - break; - case "last_updated": - let epoch = parseInt(node.getAttribute("epoch")); - if (!isNaN(epoch)) - addon.updateDate = new Date(1000 * epoch); - break; - case "icon": - addon.icons[node.getAttribute("size")] = this._getTextContent(node); - break; - } - } - - return result; - }, - - _parseAddons: function(aElements, aTotalResults, aSkip) { - let results = []; - - let isSameApplication = aAppNode => this._getTextContent(aAppNode) == Services.appinfo.ID; - - for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) { - let element = aElements[i]; - - let tags = this._getUniqueDescendant(element, "compatible_applications"); - if (tags == null) - continue; - - let applications = tags.getElementsByTagName("appID"); - let compatible = Array.some(applications, aAppNode => { - if (!isSameApplication(aAppNode)) - return false; - - let parent = aAppNode.parentNode; - let minVersion = this._getDescendantTextContent(parent, "min_version"); - let maxVersion = this._getDescendantTextContent(parent, "max_version"); - if (minVersion == null || maxVersion == null) - return false; - - let currentVersion = Services.appinfo.version; - return (Services.vc.compare(minVersion, currentVersion) <= 0 && - ((!AddonManager.strictCompatibility) || - Services.vc.compare(currentVersion, maxVersion) <= 0)); - }); - - // Ignore add-ons not compatible with this Application - if (!compatible) { - if (AddonManager.checkCompatibility) - continue; - - if (!Array.some(applications, isSameApplication)) - continue; - } - - // Add-on meets all requirements, so parse out data. - // Don't pass in compatiblity override data, because that's only returned - // in GUID searches, which don't use _parseAddons(). - let result = this._parseAddon(element, aSkip); - if (result == null) - continue; - - // Ignore add-on missing a required attribute - let requiredAttributes = ["id", "name", "version", "type", "creator"]; - if (requiredAttributes.some(aAttribute => !result.addon[aAttribute])) - continue; - - // Ignore add-on with a type AddonManager doesn't understand: - if (!(result.addon.type in AddonManager.addonTypes)) - continue; - - // Add only if the add-on is compatible with the platform - if (!result.addon.isPlatformCompatible) - continue; - - // Add only if there was an xpi compatible with this OS or there was a - // way to purchase the add-on - if (!result.xpiURL && !result.addon.purchaseURL) - continue; - - result.addon.isCompatible = compatible; - - results.push(result); - // Ignore this add-on from now on by adding it to the skip array - aSkip.ids.push(result.addon.id); - } - - // Immediately report success if no AddonInstall instances to create - let pendingResults = results.length; - if (pendingResults == 0) { - this._reportSuccess(results, aTotalResults); - return; - } - - // Create an AddonInstall for each result - for (let result of results) { - let addon = result.addon; - let callback = aInstall => { - addon.install = aInstall; - pendingResults--; - if (pendingResults == 0) - this._reportSuccess(results, aTotalResults); - } - - if (result.xpiURL) { - AddonManager.getInstallForURL(result.xpiURL, callback, - "application/x-xpinstall", result.xpiHash, - addon.name, addon.icons, addon.version); - } - else { - callback(null); - } - } - }, - - // Parses addon_compatibility nodes, that describe compatibility overrides. - _parseAddonCompatElement: function(aResultObj, aElement) { - let guid = this._getDescendantTextContent(aElement, "guid"); - if (!guid) { - logger.debug("Compatibility override is missing guid."); - return; - } - - let compat = {id: guid}; - compat.hosted = aElement.getAttribute("hosted") != "false"; - - function findMatchingAppRange(aNodes) { - let toolkitAppRange = null; - for (let node of aNodes) { - let appID = this._getDescendantTextContent(node, "appID"); - if (appID != Services.appinfo.ID && appID != TOOLKIT_ID) - continue; - - let minVersion = this._getDescendantTextContent(node, "min_version"); - let maxVersion = this._getDescendantTextContent(node, "max_version"); - if (minVersion == null || maxVersion == null) - continue; - - let appRange = { appID: appID, - appMinVersion: minVersion, - appMaxVersion: maxVersion }; - - // Only use Toolkit app ranges if no ranges match the application ID. - if (appID == TOOLKIT_ID) - toolkitAppRange = appRange; - else - return appRange; - } - return toolkitAppRange; - } - - function parseRangeNode(aNode) { - let type = aNode.getAttribute("type"); - // Only "incompatible" (blacklisting) is supported for now. - if (type != "incompatible") { - logger.debug("Compatibility override of unsupported type found."); - return null; - } - - let override = new AddonManagerPrivate.AddonCompatibilityOverride(type); - - override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version"); - override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version"); - - if (!override.minVersion) { - logger.debug("Compatibility override is missing min_version."); - return null; - } - if (!override.maxVersion) { - logger.debug("Compatibility override is missing max_version."); - return null; - } - - let appRanges = aNode.querySelectorAll("compatible_applications > application"); - let appRange = findMatchingAppRange.bind(this)(appRanges); - if (!appRange) { - logger.debug("Compatibility override is missing a valid application range."); - return null; - } - - override.appID = appRange.appID; - override.appMinVersion = appRange.appMinVersion; - override.appMaxVersion = appRange.appMaxVersion; - - return override; - } - - let rangeNodes = aElement.querySelectorAll("version_ranges > version_range"); - compat.compatRanges = Array.map(rangeNodes, parseRangeNode.bind(this)) - .filter(aItem => !!aItem); - if (compat.compatRanges.length == 0) - return; - - aResultObj[compat.id] = compat; - }, - - // Parses addon_compatibility elements. - _parseAddonCompatData: function(aElements) { - let compatData = {}; - Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData)); - return compatData; - }, - - // Begins a new search if one isn't currently executing - _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) { - if (this._searching || aURI == null || aMaxResults <= 0) { - logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI + - " aMaxResults " + aMaxResults); - aCallback.searchFailed(); - return; - } - - this._searching = true; - this._callback = aCallback; - this._maxResults = aMaxResults; - - logger.debug("Requesting " + aURI); - - this._request = new ServiceRequest(); - this._request.mozBackgroundRequest = true; - this._request.open("GET", aURI, true); - this._request.overrideMimeType("text/xml"); - if (aTimeout) { - this._request.timeout = aTimeout; - } - - this._request.addEventListener("error", aEvent => this._reportFailure(), false); - this._request.addEventListener("timeout", aEvent => this._reportFailure(), false); - this._request.addEventListener("load", aEvent => { - logger.debug("Got metadata search load event"); - let request = aEvent.target; - let responseXML = request.responseXML; - - if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR || - (request.status != 200 && request.status != 0)) { - this._reportFailure(); - return; - } - - let documentElement = responseXML.documentElement; - let elements = documentElement.getElementsByTagName("addon"); - let totalResults = elements.length; - let parsedTotalResults = parseInt(documentElement.getAttribute("total_results")); - // Parsed value of total results only makes sense if >= elements.length - if (parsedTotalResults >= totalResults) - totalResults = parsedTotalResults; - - let compatElements = documentElement.getElementsByTagName("addon_compatibility"); - let compatData = this._parseAddonCompatData(compatElements); - - aHandleResults(elements, totalResults, compatData); - }, false); - this._request.send(null); - }, - - // Gets the id's of local add-ons, and the sourceURI's of local installs, - // passing the results to aCallback - _getLocalAddonIds: function(aCallback) { - let localAddonIds = {ids: null, sourceURIs: null}; - - AddonManager.getAllAddons(function(aAddons) { - localAddonIds.ids = aAddons.map(a => a.id); - if (localAddonIds.sourceURIs) - aCallback(localAddonIds); - }); - - AddonManager.getAllInstalls(function(aInstalls) { - localAddonIds.sourceURIs = []; - for (let install of aInstalls) { - if (install.state != AddonManager.STATE_AVAILABLE) - localAddonIds.sourceURIs.push(install.sourceURI.spec); - } - - if (localAddonIds.ids) - aCallback(localAddonIds); - }); - }, - - // Create url from preference, returning null if preference does not exist - _formatURLPref: function(aPreference, aSubstitutions) { - let url = null; - try { - url = Services.prefs.getCharPref(aPreference); - } catch (e) { - logger.warn("_formatURLPref: Couldn't get pref: " + aPreference); - return null; - } - - url = url.replace(/%([A-Z_]+)%/g, function(aMatch, aKey) { - return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch; - }); - - return Services.urlFormatter.formatURL(url); - }, - - // Find a AddonCompatibilityOverride that matches a given aAddonVersion and - // application/platform version. - findMatchingCompatOverride: function(aAddonVersion, - aCompatOverrides, - aAppVersion, - aPlatformVersion) { - for (let override of aCompatOverrides) { - - let appVersion = null; - if (override.appID == TOOLKIT_ID) - appVersion = aPlatformVersion || Services.appinfo.platformVersion; - else - appVersion = aAppVersion || Services.appinfo.version; - - if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 && - Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 && - Services.vc.compare(override.appMinVersion, appVersion) <= 0 && - Services.vc.compare(appVersion, override.appMaxVersion) <= 0) { - return override; - } - } - return null; - }, - - flush: function() { - return AddonDatabase.flush(); - } -}; - -var AddonDatabase = { - connectionPromise: null, - // the in-memory database - DB: BLANK_DB(), - - /** - * A getter to retrieve the path to the DB - */ - get jsonFile() { - return OS.Path.join(OS.Constants.Path.profileDir, FILE_DATABASE); - }, - - /** - * Asynchronously opens a new connection to the database file. - * - * @return {Promise} a promise that resolves to the database. - */ - openConnection: function() { - if (!this.connectionPromise) { - this.connectionPromise = Task.spawn(function*() { - this.DB = BLANK_DB(); - - let inputDB, schema; - - try { - let data = yield OS.File.read(this.jsonFile, { encoding: "utf-8"}) - inputDB = JSON.parse(data); - - if (!inputDB.hasOwnProperty("addons") || - !Array.isArray(inputDB.addons)) { - throw new Error("No addons array."); - } - - if (!inputDB.hasOwnProperty("schema")) { - throw new Error("No schema specified."); - } - - schema = parseInt(inputDB.schema, 10); - - if (!Number.isInteger(schema) || - schema < DB_MIN_JSON_SCHEMA) { - throw new Error("Invalid schema value."); - } - } catch (e) { - if (e instanceof OS.File.Error && e.becauseNoSuchFile) { - logger.debug("No " + FILE_DATABASE + " found."); - } else { - logger.error(`Malformed ${FILE_DATABASE}: ${e} - resetting to empty`); - } - - // Create a blank addons.json file - this._saveDBToDisk(); - - let dbSchema = 0; - try { - dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA); - } catch (e) {} - - if (dbSchema < DB_MIN_JSON_SCHEMA) { - let results = yield new Promise((resolve, reject) => { - AddonRepository_SQLiteMigrator.migrate(resolve); - }); - - if (results.length) { - yield this._insertAddons(results); - } - - } - - Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA); - return this.DB; - } - - Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA); - - // We use _insertAddon manually instead of calling - // insertAddons to avoid the write to disk which would - // be a waste since this is the data that was just read. - for (let addon of inputDB.addons) { - this._insertAddon(addon); - } - - return this.DB; - }.bind(this)); - } - - return this.connectionPromise; - }, - - /** - * A lazy getter for the database connection. - */ - get connection() { - return this.openConnection(); - }, - - /** - * Asynchronously shuts down the database connection and releases all - * cached objects - * - * @param aCallback - * An optional callback to call once complete - * @param aSkipFlush - * An optional boolean to skip flushing data to disk. Useful - * when the database is going to be deleted afterwards. - */ - shutdown: function(aSkipFlush) { - if (!this.connectionPromise) { - return Promise.resolve(); - } - - this.connectionPromise = null; - - if (aSkipFlush) { - return Promise.resolve(); - } - return this.Writer.flush(); - }, - - /** - * Asynchronously deletes the database, shutting down the connection - * first if initialized - * - * @param aCallback - * An optional callback to call once complete - * @return Promise{null} resolves when the database has been deleted - */ - delete: function(aCallback) { - this.DB = BLANK_DB(); - - this._deleting = this.Writer.flush() - .then(null, () => {}) - // shutdown(true) never rejects - .then(() => this.shutdown(true)) - .then(() => OS.File.remove(this.jsonFile, {})) - .then(null, error => logger.error("Unable to delete Addon Repository file " + - this.jsonFile, error)) - .then(() => this._deleting = null) - .then(aCallback); - return this._deleting; - }, - - toJSON: function() { - let json = { - schema: this.DB.schema, - addons: [] - } - - for (let [, value] of this.DB.addons) - json.addons.push(value); - - return json; - }, - - /* - * This is a deferred task writer that is used - * to batch operations done within 50ms of each - * other and thus generating only one write to disk - */ - get Writer() { - delete this.Writer; - this.Writer = new DeferredSave( - this.jsonFile, - () => { return JSON.stringify(this); }, - DB_BATCH_TIMEOUT_MS - ); - return this.Writer; - }, - - /** - * Flush any pending I/O on the addons.json file - * @return: Promise{null} - * Resolves when the pending I/O (writing out or deleting - * addons.json) completes - */ - flush: function() { - if (this._deleting) { - return this._deleting; - } - return this.Writer.flush(); - }, - - /** - * Asynchronously retrieve all add-ons from the database - * @return: Promise{Map} - * Resolves when the add-ons are retrieved from the database - */ - retrieveStoredData: function() { - return this.openConnection().then(db => db.addons); - }, - - /** - * Asynchronously repopulates the database so it only contains the - * specified add-ons - * - * @param aAddons - * The array of add-ons to repopulate the database with - * @param aCallback - * An optional callback to call once complete - */ - repopulate: function(aAddons, aCallback) { - this.DB.addons.clear(); - this.insertAddons(aAddons, function() { - let now = Math.round(Date.now() / 1000); - logger.debug("Cache repopulated, setting " + PREF_METADATA_LASTUPDATE + " to " + now); - Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, now); - if (aCallback) - aCallback(); - }); - }, - - /** - * Asynchronously inserts an array of add-ons into the database - * - * @param aAddons - * The array of add-ons to insert - * @param aCallback - * An optional callback to call once complete - */ - insertAddons: Task.async(function*(aAddons, aCallback) { - yield this.openConnection(); - yield this._insertAddons(aAddons, aCallback); - }), - - _insertAddons: Task.async(function*(aAddons, aCallback) { - for (let addon of aAddons) { - this._insertAddon(addon); - } - - yield this._saveDBToDisk(); - aCallback && aCallback(); - }), - - /** - * Inserts an individual add-on into the database. If the add-on already - * exists in the database (by id), then the specified add-on will not be - * inserted. - * - * @param aAddon - * The add-on to insert into the database - * @param aCallback - * The callback to call once complete - */ - _insertAddon: function(aAddon) { - let newAddon = this._parseAddon(aAddon); - if (!newAddon || - !newAddon.id || - this.DB.addons.has(newAddon.id)) - return; - - this.DB.addons.set(newAddon.id, newAddon); - }, - - /* - * Creates an AddonSearchResult by parsing an object structure - * retrieved from the DB JSON representation. - * - * @param aObj - * The object to parse - * @return Returns an AddonSearchResult object. - */ - _parseAddon: function(aObj) { - if (aObj instanceof AddonSearchResult) - return aObj; - - let id = aObj.id; - if (!aObj.id) - return null; - - let addon = new AddonSearchResult(id); - - for (let expectedProperty of Object.keys(AddonSearchResult.prototype)) { - if (!(expectedProperty in aObj) || - typeof(aObj[expectedProperty]) === "function") - continue; - - let value = aObj[expectedProperty]; - - try { - switch (expectedProperty) { - case "sourceURI": - addon.sourceURI = value ? NetUtil.newURI(value) : null; - break; - - case "creator": - addon.creator = value - ? this._makeDeveloper(value) - : null; - break; - - case "updateDate": - addon.updateDate = value ? new Date(value) : null; - break; - - case "developers": - if (!addon.developers) addon.developers = []; - for (let developer of value) { - addon.developers.push(this._makeDeveloper(developer)); - } - break; - - case "screenshots": - if (!addon.screenshots) addon.screenshots = []; - for (let screenshot of value) { - addon.screenshots.push(this._makeScreenshot(screenshot)); - } - break; - - case "compatibilityOverrides": - if (!addon.compatibilityOverrides) addon.compatibilityOverrides = []; - for (let override of value) { - addon.compatibilityOverrides.push( - this._makeCompatOverride(override) - ); - } - break; - - case "icons": - if (!addon.icons) addon.icons = {}; - for (let size of Object.keys(aObj.icons)) { - addon.icons[size] = aObj.icons[size]; - } - break; - - case "iconURL": - break; - - default: - addon[expectedProperty] = value; - } - } catch (ex) { - logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex); - } - - // delete property from obj to indicate we've already - // handled it. The remaining public properties will - // be stored separately and just passed through to - // be written back to the DB. - delete aObj[expectedProperty]; - } - - // Copy remaining properties to a separate object - // to prevent accidental access on downgraded versions. - // The properties will be merged in the same object - // prior to being written back through toJSON. - for (let remainingProperty of Object.keys(aObj)) { - switch (typeof(aObj[remainingProperty])) { - case "boolean": - case "number": - case "string": - case "object": - // these types are accepted - break; - default: - continue; - } - - if (!remainingProperty.startsWith("_")) - addon._unsupportedProperties[remainingProperty] = - aObj[remainingProperty]; - } - - return addon; - }, - - /** - * Write the in-memory DB to disk, after waiting for - * the DB_BATCH_TIMEOUT_MS timeout. - * - * @return Promise A promise that resolves after the - * write to disk has completed. - */ - _saveDBToDisk: function() { - return this.Writer.saveChanges().then( - null, - e => logger.error("SaveDBToDisk failed", e)); - }, - - /** - * Make a developer object from a vanilla - * JS object from the JSON database - * - * @param aObj - * The JS object to use - * @return The created developer - */ - _makeDeveloper: function(aObj) { - let name = aObj.name; - let url = aObj.url; - return new AddonManagerPrivate.AddonAuthor(name, url); - }, - - /** - * Make a screenshot object from a vanilla - * JS object from the JSON database - * - * @param aObj - * The JS object to use - * @return The created screenshot - */ - _makeScreenshot: function(aObj) { - let url = aObj.url; - let width = aObj.width; - let height = aObj.height; - let thumbnailURL = aObj.thumbnailURL; - let thumbnailWidth = aObj.thumbnailWidth; - let thumbnailHeight = aObj.thumbnailHeight; - let caption = aObj.caption; - return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL, - thumbnailWidth, thumbnailHeight, caption); - }, - - /** - * Make a CompatibilityOverride from a vanilla - * JS object from the JSON database - * - * @param aObj - * The JS object to use - * @return The created CompatibilityOverride - */ - _makeCompatOverride: function(aObj) { - let type = aObj.type; - let minVersion = aObj.minVersion; - let maxVersion = aObj.maxVersion; - let appID = aObj.appID; - let appMinVersion = aObj.appMinVersion; - let appMaxVersion = aObj.appMaxVersion; - return new AddonManagerPrivate.AddonCompatibilityOverride(type, - minVersion, - maxVersion, - appID, - appMinVersion, - appMaxVersion); - }, -}; diff --git a/toolkit/mozapps/webextensions/internal/AddonRepository_SQLiteMigrator.jsm b/toolkit/mozapps/webextensions/internal/AddonRepository_SQLiteMigrator.jsm deleted file mode 100644 index e3479643b..000000000 --- a/toolkit/mozapps/webextensions/internal/AddonRepository_SQLiteMigrator.jsm +++ /dev/null @@ -1,522 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); -/* globals AddonManagerPrivate*/ -Cu.import("resource://gre/modules/FileUtils.jsm"); - -const KEY_PROFILEDIR = "ProfD"; -const FILE_DATABASE = "addons.sqlite"; -const LAST_DB_SCHEMA = 4; - -// Add-on properties present in the columns of the database -const PROP_SINGLE = ["id", "type", "name", "version", "creator", "description", - "fullDescription", "developerComments", "eula", - "homepageURL", "supportURL", "contributionURL", - "contributionAmount", "averageRating", "reviewCount", - "reviewURL", "totalDownloads", "weeklyDownloads", - "dailyUsers", "sourceURI", "repositoryStatus", "size", - "updateDate"]; - -Cu.import("resource://gre/modules/Log.jsm"); -const LOGGER_ID = "addons.repository.sqlmigrator"; - -// Create a new logger for use by the Addons Repository SQL Migrator -// (Requires AddonManager.jsm) -var logger = Log.repository.getLogger(LOGGER_ID); - -this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"]; - - -this.AddonRepository_SQLiteMigrator = { - - /** - * Migrates data from a previous SQLite version of the - * database to the JSON version. - * - * @param structFunctions an object that contains functions - * to create the various objects used - * in the new JSON format - * @param aCallback A callback to be called when migration - * finishes, with the results in an array - * @returns bool True if a migration will happen (DB was - * found and succesfully opened) - */ - migrate: function(aCallback) { - if (!this._openConnection()) { - this._closeConnection(); - aCallback([]); - return false; - } - - logger.debug("Importing addon repository from previous " + FILE_DATABASE + " storage."); - - this._retrieveStoredData((results) => { - this._closeConnection(); - let resultArray = Object.keys(results).map(k => results[k]); - logger.debug(resultArray.length + " addons imported.") - aCallback(resultArray); - }); - - return true; - }, - - /** - * Synchronously opens a new connection to the database file. - * - * @return bool Whether the DB was opened successfully. - */ - _openConnection: function() { - delete this.connection; - - let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); - if (!dbfile.exists()) - return false; - - try { - this.connection = Services.storage.openUnsharedDatabase(dbfile); - } catch (e) { - return false; - } - - this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE"); - - // Any errors in here should rollback - try { - this.connection.beginTransaction(); - - switch (this.connection.schemaVersion) { - case 0: - return false; - - case 1: - logger.debug("Upgrading database schema to version 2"); - this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN width INTEGER"); - this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN height INTEGER"); - this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailWidth INTEGER"); - this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailHeight INTEGER"); - case 2: - logger.debug("Upgrading database schema to version 3"); - this.connection.createTable("compatibility_override", - "addon_internal_id INTEGER, " + - "num INTEGER, " + - "type TEXT, " + - "minVersion TEXT, " + - "maxVersion TEXT, " + - "appID TEXT, " + - "appMinVersion TEXT, " + - "appMaxVersion TEXT, " + - "PRIMARY KEY (addon_internal_id, num)"); - case 3: - logger.debug("Upgrading database schema to version 4"); - this.connection.createTable("icon", - "addon_internal_id INTEGER, " + - "size INTEGER, " + - "url TEXT, " + - "PRIMARY KEY (addon_internal_id, size)"); - this._createIndices(); - this._createTriggers(); - this.connection.schemaVersion = LAST_DB_SCHEMA; - case LAST_DB_SCHEMA: - break; - default: - return false; - } - this.connection.commitTransaction(); - } catch (e) { - logger.error("Failed to open " + FILE_DATABASE + ". Data import will not happen.", e); - this.logSQLError(this.connection.lastError, this.connection.lastErrorString); - this.connection.rollbackTransaction(); - return false; - } - - return true; - }, - - _closeConnection: function() { - for (let key in this.asyncStatementsCache) { - let stmt = this.asyncStatementsCache[key]; - stmt.finalize(); - } - this.asyncStatementsCache = {}; - - if (this.connection) - this.connection.asyncClose(); - - delete this.connection; - }, - - /** - * Asynchronously retrieve all add-ons from the database, and pass it - * to the specified callback - * - * @param aCallback - * The callback to pass the add-ons back to - */ - _retrieveStoredData: function(aCallback) { - let addons = {}; - - // Retrieve all data from the addon table - let getAllAddons = () => { - this.getAsyncStatement("getAllAddons").executeAsync({ - handleResult: aResults => { - let row = null; - while ((row = aResults.getNextRow())) { - let internal_id = row.getResultByName("internal_id"); - addons[internal_id] = this._makeAddonFromAsyncRow(row); - } - }, - - handleError: this.asyncErrorLogger, - - handleCompletion: function(aReason) { - if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { - logger.error("Error retrieving add-ons from database. Returning empty results"); - aCallback({}); - return; - } - - getAllDevelopers(); - } - }); - } - - // Retrieve all data from the developer table - let getAllDevelopers = () => { - this.getAsyncStatement("getAllDevelopers").executeAsync({ - handleResult: aResults => { - let row = null; - while ((row = aResults.getNextRow())) { - let addon_internal_id = row.getResultByName("addon_internal_id"); - if (!(addon_internal_id in addons)) { - logger.warn("Found a developer not linked to an add-on in database"); - continue; - } - - let addon = addons[addon_internal_id]; - if (!addon.developers) - addon.developers = []; - - addon.developers.push(this._makeDeveloperFromAsyncRow(row)); - } - }, - - handleError: this.asyncErrorLogger, - - handleCompletion: function(aReason) { - if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { - logger.error("Error retrieving developers from database. Returning empty results"); - aCallback({}); - return; - } - - getAllScreenshots(); - } - }); - } - - // Retrieve all data from the screenshot table - let getAllScreenshots = () => { - this.getAsyncStatement("getAllScreenshots").executeAsync({ - handleResult: aResults => { - let row = null; - while ((row = aResults.getNextRow())) { - let addon_internal_id = row.getResultByName("addon_internal_id"); - if (!(addon_internal_id in addons)) { - logger.warn("Found a screenshot not linked to an add-on in database"); - continue; - } - - let addon = addons[addon_internal_id]; - if (!addon.screenshots) - addon.screenshots = []; - addon.screenshots.push(this._makeScreenshotFromAsyncRow(row)); - } - }, - - handleError: this.asyncErrorLogger, - - handleCompletion: function(aReason) { - if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { - logger.error("Error retrieving screenshots from database. Returning empty results"); - aCallback({}); - return; - } - - getAllCompatOverrides(); - } - }); - } - - let getAllCompatOverrides = () => { - this.getAsyncStatement("getAllCompatOverrides").executeAsync({ - handleResult: aResults => { - let row = null; - while ((row = aResults.getNextRow())) { - let addon_internal_id = row.getResultByName("addon_internal_id"); - if (!(addon_internal_id in addons)) { - logger.warn("Found a compatibility override not linked to an add-on in database"); - continue; - } - - let addon = addons[addon_internal_id]; - if (!addon.compatibilityOverrides) - addon.compatibilityOverrides = []; - addon.compatibilityOverrides.push(this._makeCompatOverrideFromAsyncRow(row)); - } - }, - - handleError: this.asyncErrorLogger, - - handleCompletion: function(aReason) { - if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { - logger.error("Error retrieving compatibility overrides from database. Returning empty results"); - aCallback({}); - return; - } - - getAllIcons(); - } - }); - } - - let getAllIcons = () => { - this.getAsyncStatement("getAllIcons").executeAsync({ - handleResult: aResults => { - let row = null; - while ((row = aResults.getNextRow())) { - let addon_internal_id = row.getResultByName("addon_internal_id"); - if (!(addon_internal_id in addons)) { - logger.warn("Found an icon not linked to an add-on in database"); - continue; - } - - let addon = addons[addon_internal_id]; - let { size, url } = this._makeIconFromAsyncRow(row); - addon.icons[size] = url; - if (size == 32) - addon.iconURL = url; - } - }, - - handleError: this.asyncErrorLogger, - - handleCompletion: function(aReason) { - if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { - logger.error("Error retrieving icons from database. Returning empty results"); - aCallback({}); - return; - } - - let returnedAddons = {}; - for (let id in addons) { - let addon = addons[id]; - returnedAddons[addon.id] = addon; - } - aCallback(returnedAddons); - } - }); - } - - // Begin asynchronous process - getAllAddons(); - }, - - // A cache of statements that are used and need to be finalized on shutdown - asyncStatementsCache: {}, - - /** - * Gets a cached async statement or creates a new statement if it doesn't - * already exist. - * - * @param aKey - * A unique key to reference the statement - * @return a mozIStorageAsyncStatement for the SQL corresponding to the - * unique key - */ - getAsyncStatement: function(aKey) { - if (aKey in this.asyncStatementsCache) - return this.asyncStatementsCache[aKey]; - - let sql = this.queries[aKey]; - try { - return this.asyncStatementsCache[aKey] = this.connection.createAsyncStatement(sql); - } catch (e) { - logger.error("Error creating statement " + aKey + " (" + sql + ")"); - throw Components.Exception("Error creating statement " + aKey + " (" + sql + "): " + e, - e.result); - } - }, - - // The queries used by the database - queries: { - getAllAddons: "SELECT internal_id, id, type, name, version, " + - "creator, creatorURL, description, fullDescription, " + - "developerComments, eula, homepageURL, supportURL, " + - "contributionURL, contributionAmount, averageRating, " + - "reviewCount, reviewURL, totalDownloads, weeklyDownloads, " + - "dailyUsers, sourceURI, repositoryStatus, size, updateDate " + - "FROM addon", - - getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " + - "ORDER BY addon_internal_id, num", - - getAllScreenshots: "SELECT addon_internal_id, url, width, height, " + - "thumbnailURL, thumbnailWidth, thumbnailHeight, caption " + - "FROM screenshot ORDER BY addon_internal_id, num", - - getAllCompatOverrides: "SELECT addon_internal_id, type, minVersion, " + - "maxVersion, appID, appMinVersion, appMaxVersion " + - "FROM compatibility_override " + - "ORDER BY addon_internal_id, num", - - getAllIcons: "SELECT addon_internal_id, size, url FROM icon " + - "ORDER BY addon_internal_id, size", - }, - - /** - * Make add-on structure from an asynchronous row. - * - * @param aRow - * The asynchronous row to use - * @return The created add-on - */ - _makeAddonFromAsyncRow: function(aRow) { - // This is intentionally not an AddonSearchResult object in order - // to allow AddonDatabase._parseAddon to parse it, same as if it - // was read from the JSON database. - - let addon = { icons: {} }; - - for (let prop of PROP_SINGLE) { - addon[prop] = aRow.getResultByName(prop) - } - - return addon; - }, - - /** - * Make a developer from an asynchronous row - * - * @param aRow - * The asynchronous row to use - * @return The created developer - */ - _makeDeveloperFromAsyncRow: function(aRow) { - let name = aRow.getResultByName("name"); - let url = aRow.getResultByName("url") - return new AddonManagerPrivate.AddonAuthor(name, url); - }, - - /** - * Make a screenshot from an asynchronous row - * - * @param aRow - * The asynchronous row to use - * @return The created screenshot - */ - _makeScreenshotFromAsyncRow: function(aRow) { - let url = aRow.getResultByName("url"); - let width = aRow.getResultByName("width"); - let height = aRow.getResultByName("height"); - let thumbnailURL = aRow.getResultByName("thumbnailURL"); - let thumbnailWidth = aRow.getResultByName("thumbnailWidth"); - let thumbnailHeight = aRow.getResultByName("thumbnailHeight"); - let caption = aRow.getResultByName("caption"); - return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL, - thumbnailWidth, thumbnailHeight, caption); - }, - - /** - * Make a CompatibilityOverride from an asynchronous row - * - * @param aRow - * The asynchronous row to use - * @return The created CompatibilityOverride - */ - _makeCompatOverrideFromAsyncRow: function(aRow) { - let type = aRow.getResultByName("type"); - let minVersion = aRow.getResultByName("minVersion"); - let maxVersion = aRow.getResultByName("maxVersion"); - let appID = aRow.getResultByName("appID"); - let appMinVersion = aRow.getResultByName("appMinVersion"); - let appMaxVersion = aRow.getResultByName("appMaxVersion"); - return new AddonManagerPrivate.AddonCompatibilityOverride(type, - minVersion, - maxVersion, - appID, - appMinVersion, - appMaxVersion); - }, - - /** - * Make an icon from an asynchronous row - * - * @param aRow - * The asynchronous row to use - * @return An object containing the size and URL of the icon - */ - _makeIconFromAsyncRow: function(aRow) { - let size = aRow.getResultByName("size"); - let url = aRow.getResultByName("url"); - return { size: size, url: url }; - }, - - /** - * A helper function to log an SQL error. - * - * @param aError - * The storage error code associated with the error - * @param aErrorString - * An error message - */ - logSQLError: function(aError, aErrorString) { - logger.error("SQL error " + aError + ": " + aErrorString); - }, - - /** - * A helper function to log any errors that occur during async statements. - * - * @param aError - * A mozIStorageError to log - */ - asyncErrorLogger: function(aError) { - logger.error("Async SQL error " + aError.result + ": " + aError.message); - }, - - /** - * Synchronously creates the triggers in the database. - */ - _createTriggers: function() { - this.connection.executeSimpleSQL("DROP TRIGGER IF EXISTS delete_addon"); - this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " + - "ON addon BEGIN " + - "DELETE FROM developer WHERE addon_internal_id=old.internal_id; " + - "DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " + - "DELETE FROM compatibility_override WHERE addon_internal_id=old.internal_id; " + - "DELETE FROM icon WHERE addon_internal_id=old.internal_id; " + - "END"); - }, - - /** - * Synchronously creates the indices in the database. - */ - _createIndices: function() { - this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS developer_idx " + - "ON developer (addon_internal_id)"); - this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS screenshot_idx " + - "ON screenshot (addon_internal_id)"); - this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS compatibility_override_idx " + - "ON compatibility_override (addon_internal_id)"); - this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS icon_idx " + - "ON icon (addon_internal_id)"); - } -} diff --git a/toolkit/mozapps/webextensions/internal/GMPProvider.jsm b/toolkit/mozapps/webextensions/internal/GMPProvider.jsm deleted file mode 100644 index 9bb34a7af..000000000 --- a/toolkit/mozapps/webextensions/internal/GMPProvider.jsm +++ /dev/null @@ -1,699 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -this.EXPORTED_SYMBOLS = []; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); -/* globals AddonManagerPrivate*/ -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/osfile.jsm"); -/* globals OS*/ -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/GMPUtils.jsm"); -/* globals EME_ADOBE_ID, GMP_PLUGIN_IDS, GMPPrefs, GMPUtils, OPEN_H264_ID, WIDEVINE_ID */ -Cu.import("resource://gre/modules/AppConstants.jsm"); -Cu.import("resource://gre/modules/UpdateUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter( - this, "GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"); -XPCOMUtils.defineLazyModuleGetter( - this, "setTimeout", "resource://gre/modules/Timer.jsm"); - -const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; -const STRING_TYPE_NAME = "type.%ID%.name"; - -const SEC_IN_A_DAY = 24 * 60 * 60; -// How long to wait after a user enabled EME before attempting to download CDMs. -const GMP_CHECK_DELAY = 10 * 1000; // milliseconds - -const NS_GRE_DIR = "GreD"; -const CLEARKEY_PLUGIN_ID = "gmp-clearkey"; -const CLEARKEY_VERSION = "0.1"; - -const GMP_LICENSE_INFO = "gmp_license_info"; -const GMP_PRIVACY_INFO = "gmp_privacy_info"; -const GMP_LEARN_MORE = "learn_more_label"; - -const GMP_PLUGINS = [ - { - id: OPEN_H264_ID, - name: "openH264_name", - description: "openH264_description2", - // The following licenseURL is part of an awful hack to include the OpenH264 - // license without having bug 624602 fixed yet, and intentionally ignores - // localisation. - licenseURL: "chrome://mozapps/content/extensions/OpenH264-license.txt", - homepageURL: "http://www.openh264.org/", - optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul", - }, - { - id: EME_ADOBE_ID, - name: "eme-adobe_name", - description: "eme-adobe_description", - // The following learnMoreURL is another hack to be able to support a SUMO page for this - // feature. - get learnMoreURL() { - return Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content"; - }, - licenseURL: "http://help.adobe.com/en_US/primetime/drm/HTML5_CDM_EULA/index.html", - homepageURL: "http://help.adobe.com/en_US/primetime/drm/HTML5_CDM", - optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul", - isEME: true, - }, - { - id: WIDEVINE_ID, - name: "widevine_description", - // Describe the purpose of both CDMs in the same way. - description: "eme-adobe_description", - licenseURL: "https://www.google.com/policies/privacy/", - homepageURL: "https://www.widevine.com/", - optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul", - isEME: true - }]; -XPCOMUtils.defineConstant(this, "GMP_PLUGINS", GMP_PLUGINS); - -XPCOMUtils.defineLazyGetter(this, "pluginsBundle", - () => Services.strings.createBundle("chrome://global/locale/plugins.properties")); -XPCOMUtils.defineLazyGetter(this, "gmpService", - () => Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(Ci.mozIGeckoMediaPluginChromeService)); - -var messageManager = Cc["@mozilla.org/globalmessagemanager;1"] - .getService(Ci.nsIMessageListenerManager); - -var gLogger; -var gLogAppenderDump = null; - -function configureLogging() { - if (!gLogger) { - gLogger = Log.repository.getLogger("Toolkit.GMP"); - gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); - } - gLogger.level = GMPPrefs.get(GMPPrefs.KEY_LOGGING_LEVEL, Log.Level.Warn); - - let logDumping = GMPPrefs.get(GMPPrefs.KEY_LOGGING_DUMP, false); - if (logDumping != !!gLogAppenderDump) { - if (logDumping) { - gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter()); - gLogger.addAppender(gLogAppenderDump); - } else { - gLogger.removeAppender(gLogAppenderDump); - gLogAppenderDump = null; - } - } -} - - - -/** - * The GMPWrapper provides the info for the various GMP plugins to public - * callers through the API. - */ -function GMPWrapper(aPluginInfo) { - this._plugin = aPluginInfo; - this._log = - Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", - "GMPWrapper(" + - this._plugin.id + ") "); - Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED, - this._plugin.id), - this.onPrefEnabledChanged, this); - Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION, - this._plugin.id), - this.onPrefVersionChanged, this); - if (this._plugin.isEME) { - Preferences.observe(GMPPrefs.KEY_EME_ENABLED, - this.onPrefEMEGlobalEnabledChanged, this); - messageManager.addMessageListener("EMEVideo:ContentMediaKeysRequest", this); - } -} - -GMPWrapper.prototype = { - // An active task that checks for plugin updates and installs them. - _updateTask: null, - _gmpPath: null, - _isUpdateCheckPending: false, - - optionsType: AddonManager.OPTIONS_TYPE_INLINE, - get optionsURL() { return this._plugin.optionsURL; }, - - set gmpPath(aPath) { this._gmpPath = aPath; }, - get gmpPath() { - if (!this._gmpPath && this.isInstalled) { - this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir, - this._plugin.id, - GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, - null, this._plugin.id)); - } - return this._gmpPath; - }, - - get id() { return this._plugin.id; }, - get type() { return "plugin"; }, - get isGMPlugin() { return true; }, - get name() { return this._plugin.name; }, - get creator() { return null; }, - get homepageURL() { return this._plugin.homepageURL; }, - - get description() { return this._plugin.description; }, - get fullDescription() { return this._plugin.fullDescription; }, - - get version() { return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, null, - this._plugin.id); }, - - get isActive() { - return !this.appDisabled && - !this.userDisabled && - !GMPUtils.isPluginHidden(this._plugin); - }, - get appDisabled() { - if (this._plugin.isEME && !GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) { - // If "media.eme.enabled" is false, all EME plugins are disabled. - return true; - } - return false; - }, - - get userDisabled() { - return !GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, this._plugin.id); - }, - set userDisabled(aVal) { GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ENABLED, - aVal === false, - this._plugin.id); }, - - get blocklistState() { return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; }, - get size() { return 0; }, - get scope() { return AddonManager.SCOPE_APPLICATION; }, - get pendingOperations() { return AddonManager.PENDING_NONE; }, - - get operationsRequiringRestart() { return AddonManager.OP_NEEDS_RESTART_NONE }, - - get permissions() { - let permissions = 0; - if (!this.appDisabled) { - permissions |= AddonManager.PERM_CAN_UPGRADE; - permissions |= this.userDisabled ? AddonManager.PERM_CAN_ENABLE : - AddonManager.PERM_CAN_DISABLE; - } - return permissions; - }, - - get updateDate() { - let time = Number(GMPPrefs.get(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, null, - this._plugin.id)); - if (!isNaN(time) && this.isInstalled) { - return new Date(time * 1000) - } - return null; - }, - - get isCompatible() { - return true; - }, - - get isPlatformCompatible() { - return true; - }, - - get providesUpdatesSecurely() { - return true; - }, - - get foreignInstall() { - return false; - }, - - isCompatibleWith: function(aAppVersion, aPlatformVersion) { - return true; - }, - - get applyBackgroundUpdates() { - if (!GMPPrefs.isSet(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) { - return AddonManager.AUTOUPDATE_DEFAULT; - } - - return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id) ? - AddonManager.AUTOUPDATE_ENABLE : AddonManager.AUTOUPDATE_DISABLE; - }, - - set applyBackgroundUpdates(aVal) { - if (aVal == AddonManager.AUTOUPDATE_DEFAULT) { - GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id); - } else if (aVal == AddonManager.AUTOUPDATE_ENABLE) { - GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id); - } else if (aVal == AddonManager.AUTOUPDATE_DISABLE) { - GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, this._plugin.id); - } - }, - - findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) { - this._log.trace("findUpdates() - " + this._plugin.id + " - reason=" + - aReason); - - AddonManagerPrivate.callNoUpdateListeners(this, aListener); - - if (aReason === AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) { - if (!AddonManager.shouldAutoUpdate(this)) { - this._log.trace("findUpdates() - " + this._plugin.id + - " - no autoupdate"); - return Promise.resolve(false); - } - - let secSinceLastCheck = - Date.now() / 1000 - Preferences.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0); - if (secSinceLastCheck <= SEC_IN_A_DAY) { - this._log.trace("findUpdates() - " + this._plugin.id + - " - last check was less then a day ago"); - return Promise.resolve(false); - } - } else if (aReason !== AddonManager.UPDATE_WHEN_USER_REQUESTED) { - this._log.trace("findUpdates() - " + this._plugin.id + - " - the given reason to update is not supported"); - return Promise.resolve(false); - } - - if (this._updateTask !== null) { - this._log.trace("findUpdates() - " + this._plugin.id + - " - update task already running"); - return this._updateTask; - } - - this._updateTask = Task.spawn(function*() { - this._log.trace("findUpdates() - updateTask"); - try { - let installManager = new GMPInstallManager(); - let res = yield installManager.checkForAddons(); - let update = res.gmpAddons.find(addon => addon.id === this._plugin.id); - if (update && update.isValid && !update.isInstalled) { - this._log.trace("findUpdates() - found update for " + - this._plugin.id + ", installing"); - yield installManager.installAddon(update); - } else { - this._log.trace("findUpdates() - no updates for " + this._plugin.id); - } - this._log.info("findUpdates() - updateTask succeeded for " + - this._plugin.id); - } catch (e) { - this._log.error("findUpdates() - updateTask for " + this._plugin.id + - " threw", e); - throw e; - } finally { - this._updateTask = null; - return true; - } - }.bind(this)); - - return this._updateTask; - }, - - get pluginMimeTypes() { return []; }, - get pluginLibraries() { - if (this.isInstalled) { - let path = this.version; - return [path]; - } - return []; - }, - get pluginFullpath() { - if (this.isInstalled) { - let path = OS.Path.join(OS.Constants.Path.profileDir, - this._plugin.id, - this.version); - return [path]; - } - return []; - }, - - get isInstalled() { - return this.version && this.version.length > 0; - }, - - _handleEnabledChanged: function() { - this._log.info("_handleEnabledChanged() id=" + - this._plugin.id + " isActive=" + this.isActive); - - AddonManagerPrivate.callAddonListeners(this.isActive ? - "onEnabling" : "onDisabling", - this, false); - if (this._gmpPath) { - if (this.isActive) { - this._log.info("onPrefEnabledChanged() - adding gmp directory " + - this._gmpPath); - gmpService.addPluginDirectory(this._gmpPath); - } else { - this._log.info("onPrefEnabledChanged() - removing gmp directory " + - this._gmpPath); - gmpService.removePluginDirectory(this._gmpPath); - } - } - AddonManagerPrivate.callAddonListeners(this.isActive ? - "onEnabled" : "onDisabled", - this); - }, - - onPrefEMEGlobalEnabledChanged: function() { - this._log.info("onPrefEMEGlobalEnabledChanged() id=" + this._plugin.id + - " appDisabled=" + this.appDisabled + " isActive=" + this.isActive + - " hidden=" + GMPUtils.isPluginHidden(this._plugin)); - - AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, - ["appDisabled"]); - // If EME or the GMP itself are disabled, uninstall the GMP. - // Otherwise, check for updates, so we download and install the GMP. - if (this.appDisabled) { - this.uninstallPlugin(); - } else if (!GMPUtils.isPluginHidden(this._plugin)) { - AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this, - null, false); - AddonManagerPrivate.callAddonListeners("onInstalling", this, false); - AddonManagerPrivate.callAddonListeners("onInstalled", this); - this.checkForUpdates(GMP_CHECK_DELAY); - } - if (!this.userDisabled) { - this._handleEnabledChanged(); - } - }, - - checkForUpdates: function(delay) { - if (this._isUpdateCheckPending) { - return; - } - this._isUpdateCheckPending = true; - GMPPrefs.reset(GMPPrefs.KEY_UPDATE_LAST_CHECK, null); - // Delay this in case the user changes his mind and doesn't want to - // enable EME after all. - setTimeout(() => { - if (!this.appDisabled) { - let gmpInstallManager = new GMPInstallManager(); - // We don't really care about the results, if someone is interested - // they can check the log. - gmpInstallManager.simpleCheckAndInstall().then(null, () => {}); - } - this._isUpdateCheckPending = false; - }, delay); - }, - - receiveMessage: function({target: browser, data: data}) { - this._log.trace("receiveMessage() data=" + data); - let parsedData; - try { - parsedData = JSON.parse(data); - } catch (ex) { - this._log.error("Malformed EME video message with data: " + data); - return; - } - let {status: status, keySystem: keySystem} = parsedData; - if (status == "cdm-not-installed") { - this.checkForUpdates(0); - } - }, - - onPrefEnabledChanged: function() { - if (!this._plugin.isEME || !this.appDisabled) { - this._handleEnabledChanged(); - } - }, - - onPrefVersionChanged: function() { - AddonManagerPrivate.callAddonListeners("onUninstalling", this, false); - if (this._gmpPath) { - this._log.info("onPrefVersionChanged() - unregistering gmp directory " + - this._gmpPath); - gmpService.removeAndDeletePluginDirectory(this._gmpPath, true /* can defer */); - } - AddonManagerPrivate.callAddonListeners("onUninstalled", this); - - AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this, - null, false); - AddonManagerPrivate.callAddonListeners("onInstalling", this, false); - this._gmpPath = null; - if (this.isInstalled) { - this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir, - this._plugin.id, - GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, - null, this._plugin.id)); - } - if (this._gmpPath && this.isActive) { - this._log.info("onPrefVersionChanged() - registering gmp directory " + - this._gmpPath); - gmpService.addPluginDirectory(this._gmpPath); - } - AddonManagerPrivate.callAddonListeners("onInstalled", this); - }, - - uninstallPlugin: function() { - AddonManagerPrivate.callAddonListeners("onUninstalling", this, false); - if (this.gmpPath) { - this._log.info("uninstallPlugin() - unregistering gmp directory " + - this.gmpPath); - gmpService.removeAndDeletePluginDirectory(this.gmpPath); - } - GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_VERSION, this.id); - GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_ABI, this.id); - GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, this.id); - AddonManagerPrivate.callAddonListeners("onUninstalled", this); - }, - - shutdown: function() { - Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED, - this._plugin.id), - this.onPrefEnabledChanged, this); - Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION, - this._plugin.id), - this.onPrefVersionChanged, this); - if (this._plugin.isEME) { - Preferences.ignore(GMPPrefs.KEY_EME_ENABLED, - this.onPrefEMEGlobalEnabledChanged, this); - messageManager.removeMessageListener("EMEVideo:ContentMediaKeysRequest", this); - } - return this._updateTask; - }, - - _arePluginFilesOnDisk: function() { - let fileExists = function(aGmpPath, aFileName) { - let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - let path = OS.Path.join(aGmpPath, aFileName); - f.initWithPath(path); - return f.exists(); - }; - - let id = this._plugin.id.substring(4); - let libName = AppConstants.DLL_PREFIX + id + AppConstants.DLL_SUFFIX; - let infoName; - if (this._plugin.id == WIDEVINE_ID) { - infoName = "manifest.json"; - } else { - infoName = id + ".info"; - } - - return fileExists(this.gmpPath, libName) && - fileExists(this.gmpPath, infoName) && - (this._plugin.id != EME_ADOBE_ID || fileExists(this.gmpPath, id + ".voucher")); - }, - - validate: function() { - if (!this.isInstalled) { - // Not installed -> Valid. - return { - installed: false, - valid: true - }; - } - - let abi = GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ABI, UpdateUtils.ABI, this._plugin.id); - if (abi != UpdateUtils.ABI) { - // ABI doesn't match. Possibly this is a profile migrated across platforms - // or from 32 -> 64 bit. - return { - installed: true, - mismatchedABI: true, - valid: false - }; - } - - // Installed -> Check if files are missing. - let filesOnDisk = this._arePluginFilesOnDisk(); - return { - installed: true, - valid: filesOnDisk - }; - }, -}; - -var GMPProvider = { - get name() { return "GMPProvider"; }, - - _plugins: null, - - startup: function() { - configureLogging(); - this._log = Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", - "GMPProvider."); - this.buildPluginList(); - this.ensureProperCDMInstallState(); - - Preferences.observe(GMPPrefs.KEY_LOG_BASE, configureLogging); - - for (let [id, plugin] of this._plugins) { - let wrapper = plugin.wrapper; - let gmpPath = wrapper.gmpPath; - let isEnabled = wrapper.isActive; - this._log.trace("startup - enabled=" + isEnabled + ", gmpPath=" + - gmpPath); - - if (gmpPath && isEnabled) { - let validation = wrapper.validate(); - if (validation.mismatchedABI) { - this._log.info("startup - gmp " + plugin.id + - " mismatched ABI, uninstalling"); - wrapper.uninstallPlugin(); - continue; - } - if (!validation.valid) { - this._log.info("startup - gmp " + plugin.id + - " invalid, uninstalling"); - wrapper.uninstallPlugin(); - continue; - } - this._log.info("startup - adding gmp directory " + gmpPath); - try { - gmpService.addPluginDirectory(gmpPath); - } catch (e) { - if (e.name != 'NS_ERROR_NOT_AVAILABLE') - throw e; - this._log.warn("startup - adding gmp directory failed with " + - e.name + " - sandboxing not available?", e); - } - } - } - - try { - let greDir = Services.dirsvc.get(NS_GRE_DIR, - Ci.nsILocalFile); - let clearkeyPath = OS.Path.join(greDir.path, - CLEARKEY_PLUGIN_ID, - CLEARKEY_VERSION); - this._log.info("startup - adding clearkey CDM directory " + - clearkeyPath); - gmpService.addPluginDirectory(clearkeyPath); - } catch (e) { - this._log.warn("startup - adding clearkey CDM failed", e); - } - }, - - shutdown: function() { - this._log.trace("shutdown"); - Preferences.ignore(GMPPrefs.KEY_LOG_BASE, configureLogging); - - let shutdownTask = Task.spawn(function*() { - this._log.trace("shutdown - shutdownTask"); - let shutdownSucceeded = true; - - for (let plugin of this._plugins.values()) { - try { - yield plugin.wrapper.shutdown(); - } catch (e) { - shutdownSucceeded = false; - } - } - - this._plugins = null; - - if (!shutdownSucceeded) { - throw new Error("Shutdown failed"); - } - }.bind(this)); - - return shutdownTask; - }, - - getAddonByID: function(aId, aCallback) { - if (!this.isEnabled) { - aCallback(null); - return; - } - - let plugin = this._plugins.get(aId); - if (plugin && !GMPUtils.isPluginHidden(plugin)) { - aCallback(plugin.wrapper); - } else { - aCallback(null); - } - }, - - getAddonsByTypes: function(aTypes, aCallback) { - if (!this.isEnabled || - (aTypes && aTypes.indexOf("plugin") < 0)) { - aCallback([]); - return; - } - - let results = Array.from(this._plugins.values()) - .filter(p => !GMPUtils.isPluginHidden(p)) - .map(p => p.wrapper); - - aCallback(results); - }, - - get isEnabled() { - return GMPPrefs.get(GMPPrefs.KEY_PROVIDER_ENABLED, false); - }, - - generateFullDescription: function(aPlugin) { - let rv = []; - for (let [urlProp, labelId] of [["learnMoreURL", GMP_LEARN_MORE], - ["licenseURL", aPlugin.id == WIDEVINE_ID ? - GMP_PRIVACY_INFO : GMP_LICENSE_INFO]]) { - if (aPlugin[urlProp]) { - let label = pluginsBundle.GetStringFromName(labelId); - rv.push(`<xhtml:a href="${aPlugin[urlProp]}" target="_blank">${label}</xhtml:a>.`); - } - } - return rv.length ? rv.join("<xhtml:br /><xhtml:br />") : undefined; - }, - - buildPluginList: function() { - this._plugins = new Map(); - for (let aPlugin of GMP_PLUGINS) { - let plugin = { - id: aPlugin.id, - name: pluginsBundle.GetStringFromName(aPlugin.name), - description: pluginsBundle.GetStringFromName(aPlugin.description), - homepageURL: aPlugin.homepageURL, - optionsURL: aPlugin.optionsURL, - wrapper: null, - isEME: aPlugin.isEME, - }; - plugin.fullDescription = this.generateFullDescription(aPlugin); - plugin.wrapper = new GMPWrapper(plugin); - this._plugins.set(plugin.id, plugin); - } - }, - - ensureProperCDMInstallState: function() { - if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) { - for (let [id, plugin] of this._plugins) { - if (plugin.isEME && plugin.wrapper.isInstalled) { - gmpService.addPluginDirectory(plugin.wrapper.gmpPath); - plugin.wrapper.uninstallPlugin(); - } - } - } - }, -}; - -AddonManagerPrivate.registerProvider(GMPProvider, [ - new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 6000, - AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) -]); diff --git a/toolkit/mozapps/webextensions/internal/LightweightThemeImageOptimizer.jsm b/toolkit/mozapps/webextensions/internal/LightweightThemeImageOptimizer.jsm deleted file mode 100644 index 49dfa237f..000000000 --- a/toolkit/mozapps/webextensions/internal/LightweightThemeImageOptimizer.jsm +++ /dev/null @@ -1,180 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["LightweightThemeImageOptimizer"]; - -const Cu = Components.utils; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); - -const ORIGIN_TOP_RIGHT = 1; -const ORIGIN_BOTTOM_LEFT = 2; - -this.LightweightThemeImageOptimizer = { - optimize: function(aThemeData, aScreen) { - let data = Object.assign({}, aThemeData); - if (!data.headerURL) { - return data; - } - - data.headerURL = ImageCropper.getCroppedImageURL( - data.headerURL, aScreen, ORIGIN_TOP_RIGHT); - - if (data.footerURL) { - data.footerURL = ImageCropper.getCroppedImageURL( - data.footerURL, aScreen, ORIGIN_BOTTOM_LEFT); - } - - return data; - }, - - purge: function() { - let dir = FileUtils.getDir("ProfD", ["lwtheme"]); - dir.followLinks = false; - try { - dir.remove(true); - } catch (e) {} - } -}; - -Object.freeze(LightweightThemeImageOptimizer); - -var ImageCropper = { - _inProgress: {}, - - getCroppedImageURL: function(aImageURL, aScreen, aOrigin) { - // We can crop local files, only. - if (!aImageURL.startsWith("file://")) { - return aImageURL; - } - - // Generate the cropped image's file name using its - // base name and the current screen size. - let uri = Services.io.newURI(aImageURL, null, null); - let file = uri.QueryInterface(Ci.nsIFileURL).file; - - // Make sure the source file exists. - if (!file.exists()) { - return aImageURL; - } - - let fileName = file.leafName + "-" + aScreen.width + "x" + aScreen.height; - let croppedFile = FileUtils.getFile("ProfD", ["lwtheme", fileName]); - - // If we have a local file that is not in progress, return it. - if (croppedFile.exists() && !(croppedFile.path in this._inProgress)) { - let fileURI = Services.io.newFileURI(croppedFile); - - // Copy the query part to avoid wrong caching. - fileURI.QueryInterface(Ci.nsIURL).query = uri.query; - return fileURI.spec; - } - - // Crop the given image in the background. - this._crop(uri, croppedFile, aScreen, aOrigin); - - // Return the original image while we're waiting for the cropped version - // to be written to disk. - return aImageURL; - }, - - _crop: function(aURI, aTargetFile, aScreen, aOrigin) { - let inProgress = this._inProgress; - inProgress[aTargetFile.path] = true; - - function resetInProgress() { - delete inProgress[aTargetFile.path]; - } - - ImageFile.read(aURI, function(aInputStream, aContentType) { - if (aInputStream && aContentType) { - let image = ImageTools.decode(aInputStream, aContentType); - if (image && image.width && image.height) { - let stream = ImageTools.encode(image, aScreen, aOrigin, aContentType); - if (stream) { - ImageFile.write(aTargetFile, stream, resetInProgress); - return; - } - } - } - - resetInProgress(); - }); - } -}; - -var ImageFile = { - read: function(aURI, aCallback) { - this._netUtil.asyncFetch({ - uri: aURI, - loadUsingSystemPrincipal: true, - contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE - }, function(aInputStream, aStatus, aRequest) { - if (Components.isSuccessCode(aStatus) && aRequest instanceof Ci.nsIChannel) { - let channel = aRequest.QueryInterface(Ci.nsIChannel); - aCallback(aInputStream, channel.contentType); - } else { - aCallback(); - } - }); - }, - - write: function(aFile, aInputStream, aCallback) { - let fos = FileUtils.openSafeFileOutputStream(aFile); - this._netUtil.asyncCopy(aInputStream, fos, function(aResult) { - FileUtils.closeSafeFileOutputStream(fos); - - // Remove the file if writing was not successful. - if (!Components.isSuccessCode(aResult)) { - try { - aFile.remove(false); - } catch (e) {} - } - - aCallback(); - }); - } -}; - -XPCOMUtils.defineLazyModuleGetter(ImageFile, "_netUtil", - "resource://gre/modules/NetUtil.jsm", "NetUtil"); - -var ImageTools = { - decode: function(aInputStream, aContentType) { - let outParam = {value: null}; - - try { - this._imgTools.decodeImageData(aInputStream, aContentType, outParam); - } catch (e) {} - - return outParam.value; - }, - - encode: function(aImage, aScreen, aOrigin, aContentType) { - let stream; - let width = Math.min(aImage.width, aScreen.width); - let height = Math.min(aImage.height, aScreen.height); - let x = aOrigin == ORIGIN_TOP_RIGHT ? aImage.width - width : 0; - - try { - stream = this._imgTools.encodeCroppedImage(aImage, aContentType, x, 0, - width, height); - } catch (e) {} - - return stream; - } -}; - -XPCOMUtils.defineLazyServiceGetter(ImageTools, "_imgTools", - "@mozilla.org/image/tools;1", "imgITools"); - diff --git a/toolkit/mozapps/webextensions/internal/PluginProvider.jsm b/toolkit/mozapps/webextensions/internal/PluginProvider.jsm deleted file mode 100644 index 075159a9a..000000000 --- a/toolkit/mozapps/webextensions/internal/PluginProvider.jsm +++ /dev/null @@ -1,600 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -this.EXPORTED_SYMBOLS = []; - -Cu.import("resource://gre/modules/AddonManager.jsm"); -/* globals AddonManagerPrivate*/ -Cu.import("resource://gre/modules/Services.jsm"); - -const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; -const STRING_TYPE_NAME = "type.%ID%.name"; -const LIST_UPDATED_TOPIC = "plugins-list-updated"; -const FLASH_MIME_TYPE = "application/x-shockwave-flash"; - -Cu.import("resource://gre/modules/Log.jsm"); -const LOGGER_ID = "addons.plugins"; - -// Create a new logger for use by the Addons Plugin Provider -// (Requires AddonManager.jsm) -var logger = Log.repository.getLogger(LOGGER_ID); - -function getIDHashForString(aStr) { - // return the two-digit hexadecimal code for a byte - let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2); - - let hasher = Cc["@mozilla.org/security/hash;1"]. - createInstance(Ci.nsICryptoHash); - hasher.init(Ci.nsICryptoHash.MD5); - let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]. - createInstance(Ci.nsIStringInputStream); - stringStream.data = aStr ? aStr : "null"; - hasher.updateFromStream(stringStream, -1); - - // convert the binary hash data to a hex string. - let binary = hasher.finish(false); - let hash = Array.from(binary, c => toHexString(c.charCodeAt(0))); - hash = hash.join("").toLowerCase(); - return "{" + hash.substr(0, 8) + "-" + - hash.substr(8, 4) + "-" + - hash.substr(12, 4) + "-" + - hash.substr(16, 4) + "-" + - hash.substr(20) + "}"; -} - -var PluginProvider = { - get name() { - return "PluginProvider"; - }, - - // A dictionary mapping IDs to names and descriptions - plugins: null, - - startup: function() { - Services.obs.addObserver(this, LIST_UPDATED_TOPIC, false); - Services.obs.addObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false); - }, - - /** - * Called when the application is shutting down. Only necessary for tests - * to be able to simulate a shutdown. - */ - shutdown: function() { - this.plugins = null; - Services.obs.removeObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED); - Services.obs.removeObserver(this, LIST_UPDATED_TOPIC); - }, - - observe: function(aSubject, aTopic, aData) { - switch (aTopic) { - case AddonManager.OPTIONS_NOTIFICATION_DISPLAYED: - this.getAddonByID(aData, function(plugin) { - if (!plugin) - return; - - let libLabel = aSubject.getElementById("pluginLibraries"); - libLabel.textContent = plugin.pluginLibraries.join(", "); - - let typeLabel = aSubject.getElementById("pluginMimeTypes"), types = []; - for (let type of plugin.pluginMimeTypes) { - let extras = [type.description.trim(), type.suffixes]. - filter(x => x).join(": "); - types.push(type.type + (extras ? " (" + extras + ")" : "")); - } - typeLabel.textContent = types.join(",\n"); - let showProtectedModePref = canDisableFlashProtectedMode(plugin); - aSubject.getElementById("pluginEnableProtectedMode") - .setAttribute("collapsed", showProtectedModePref ? "" : "true"); - }); - break; - case LIST_UPDATED_TOPIC: - if (this.plugins) - this.updatePluginList(); - break; - } - }, - - /** - * Creates a PluginWrapper for a plugin object. - */ - buildWrapper: function(aPlugin) { - return new PluginWrapper(aPlugin.id, - aPlugin.name, - aPlugin.description, - aPlugin.tags); - }, - - /** - * Called to get an Addon with a particular ID. - * - * @param aId - * The ID of the add-on to retrieve - * @param aCallback - * A callback to pass the Addon to - */ - getAddonByID: function(aId, aCallback) { - if (!this.plugins) - this.buildPluginList(); - - if (aId in this.plugins) - aCallback(this.buildWrapper(this.plugins[aId])); - else - aCallback(null); - }, - - /** - * Called to get Addons of a particular type. - * - * @param aTypes - * An array of types to fetch. Can be null to get all types. - * @param callback - * A callback to pass an array of Addons to - */ - getAddonsByTypes: function(aTypes, aCallback) { - if (aTypes && aTypes.indexOf("plugin") < 0) { - aCallback([]); - return; - } - - if (!this.plugins) - this.buildPluginList(); - - let results = []; - - for (let id in this.plugins) - this.getAddonByID(id, (addon) => results.push(addon)); - - aCallback(results); - }, - - /** - * Called to get Addons that have pending operations. - * - * @param aTypes - * An array of types to fetch. Can be null to get all types - * @param aCallback - * A callback to pass an array of Addons to - */ - getAddonsWithOperationsByTypes: function(aTypes, aCallback) { - aCallback([]); - }, - - /** - * Called to get the current AddonInstalls, optionally restricting by type. - * - * @param aTypes - * An array of types or null to get all types - * @param aCallback - * A callback to pass the array of AddonInstalls to - */ - getInstallsByTypes: function(aTypes, aCallback) { - aCallback([]); - }, - - /** - * Builds a list of the current plugins reported by the plugin host - * - * @return a dictionary of plugins indexed by our generated ID - */ - getPluginList: function() { - let tags = Cc["@mozilla.org/plugin/host;1"]. - getService(Ci.nsIPluginHost). - getPluginTags({}); - - let list = {}; - let seenPlugins = {}; - for (let tag of tags) { - if (!(tag.name in seenPlugins)) - seenPlugins[tag.name] = {}; - if (!(tag.description in seenPlugins[tag.name])) { - let plugin = { - id: getIDHashForString(tag.name + tag.description), - name: tag.name, - description: tag.description, - tags: [tag] - }; - - seenPlugins[tag.name][tag.description] = plugin; - list[plugin.id] = plugin; - } - else { - seenPlugins[tag.name][tag.description].tags.push(tag); - } - } - - return list; - }, - - /** - * Builds the list of known plugins from the plugin host - */ - buildPluginList: function() { - this.plugins = this.getPluginList(); - }, - - /** - * Updates the plugins from the plugin host by comparing the current plugins - * to the last known list sending out any necessary API notifications for - * changes. - */ - updatePluginList: function() { - let newList = this.getPluginList(); - - let lostPlugins = Object.keys(this.plugins).filter(id => !(id in newList)). - map(id => this.buildWrapper(this.plugins[id])); - let newPlugins = Object.keys(newList).filter(id => !(id in this.plugins)). - map(id => this.buildWrapper(newList[id])); - let matchedIDs = Object.keys(newList).filter(id => id in this.plugins); - - // The plugin host generates new tags for every plugin after a scan and - // if the plugin's filename has changed then the disabled state won't have - // been carried across, send out notifications for anything that has - // changed (see bug 830267). - let changedWrappers = []; - for (let id of matchedIDs) { - let oldWrapper = this.buildWrapper(this.plugins[id]); - let newWrapper = this.buildWrapper(newList[id]); - - if (newWrapper.isActive != oldWrapper.isActive) { - AddonManagerPrivate.callAddonListeners(newWrapper.isActive ? - "onEnabling" : "onDisabling", - newWrapper, false); - changedWrappers.push(newWrapper); - } - } - - // Notify about new installs - for (let plugin of newPlugins) { - AddonManagerPrivate.callInstallListeners("onExternalInstall", null, - plugin, null, false); - AddonManagerPrivate.callAddonListeners("onInstalling", plugin, false); - } - - // Notify for any plugins that have vanished. - for (let plugin of lostPlugins) - AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false); - - this.plugins = newList; - - // Signal that new installs are complete - for (let plugin of newPlugins) - AddonManagerPrivate.callAddonListeners("onInstalled", plugin); - - // Signal that enables/disables are complete - for (let wrapper of changedWrappers) { - AddonManagerPrivate.callAddonListeners(wrapper.isActive ? - "onEnabled" : "onDisabled", - wrapper); - } - - // Signal that uninstalls are complete - for (let plugin of lostPlugins) - AddonManagerPrivate.callAddonListeners("onUninstalled", plugin); - } -}; - -function isFlashPlugin(aPlugin) { - for (let type of aPlugin.pluginMimeTypes) { - if (type.type == FLASH_MIME_TYPE) { - return true; - } - } - return false; -} -// Protected mode is win32-only, not win64 -function canDisableFlashProtectedMode(aPlugin) { - return isFlashPlugin(aPlugin) && Services.appinfo.XPCOMABI == "x86-msvc"; -} - -const wrapperMap = new WeakMap(); -let pluginFor = wrapper => wrapperMap.get(wrapper); - -/** - * The PluginWrapper wraps a set of nsIPluginTags to provide the data visible to - * public callers through the API. - */ -function PluginWrapper(id, name, description, tags) { - wrapperMap.set(this, { id, name, description, tags }); -} - -PluginWrapper.prototype = { - get id() { - return pluginFor(this).id; - }, - - get type() { - return "plugin"; - }, - - get name() { - return pluginFor(this).name; - }, - - get creator() { - return null; - }, - - get description() { - return pluginFor(this).description.replace(/<\/?[a-z][^>]*>/gi, " "); - }, - - get version() { - let { tags: [tag] } = pluginFor(this); - return tag.version; - }, - - get homepageURL() { - let { description } = pluginFor(this); - if (/<A\s+HREF=[^>]*>/i.test(description)) - return /<A\s+HREF=["']?([^>"'\s]*)/i.exec(description)[1]; - return null; - }, - - get isActive() { - let { tags: [tag] } = pluginFor(this); - return !tag.blocklisted && !tag.disabled; - }, - - get appDisabled() { - let { tags: [tag] } = pluginFor(this); - return tag.blocklisted; - }, - - get userDisabled() { - let { tags: [tag] } = pluginFor(this); - if (tag.disabled) - return true; - - if ((Services.prefs.getBoolPref("plugins.click_to_play") && tag.clicktoplay) || - this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE || - this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) - return AddonManager.STATE_ASK_TO_ACTIVATE; - - return false; - }, - - set userDisabled(val) { - let previousVal = this.userDisabled; - if (val === previousVal) - return val; - - let { tags } = pluginFor(this); - - for (let tag of tags) { - if (val === true) - tag.enabledState = Ci.nsIPluginTag.STATE_DISABLED; - else if (val === false) - tag.enabledState = Ci.nsIPluginTag.STATE_ENABLED; - else if (val == AddonManager.STATE_ASK_TO_ACTIVATE) - tag.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; - } - - // If 'userDisabled' was 'true' and we're going to a state that's not - // that, we're enabling, so call those listeners. - if (previousVal === true && val !== true) { - AddonManagerPrivate.callAddonListeners("onEnabling", this, false); - AddonManagerPrivate.callAddonListeners("onEnabled", this); - } - - // If 'userDisabled' was not 'true' and we're going to a state where - // it is, we're disabling, so call those listeners. - if (previousVal !== true && val === true) { - AddonManagerPrivate.callAddonListeners("onDisabling", this, false); - AddonManagerPrivate.callAddonListeners("onDisabled", this); - } - - // If the 'userDisabled' value involved AddonManager.STATE_ASK_TO_ACTIVATE, - // call the onPropertyChanged listeners. - if (previousVal == AddonManager.STATE_ASK_TO_ACTIVATE || - val == AddonManager.STATE_ASK_TO_ACTIVATE) { - AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]); - } - - return val; - }, - - get blocklistState() { - let { tags: [tag] } = pluginFor(this); - let bs = Cc["@mozilla.org/extensions/blocklist;1"]. - getService(Ci.nsIBlocklistService); - return bs.getPluginBlocklistState(tag); - }, - - get blocklistURL() { - let { tags: [tag] } = pluginFor(this); - let bs = Cc["@mozilla.org/extensions/blocklist;1"]. - getService(Ci.nsIBlocklistService); - return bs.getPluginBlocklistURL(tag); - }, - - get size() { - function getDirectorySize(aFile) { - let size = 0; - let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); - let entry; - while ((entry = entries.nextFile)) { - if (entry.isSymlink() || !entry.isDirectory()) - size += entry.fileSize; - else - size += getDirectorySize(entry); - } - entries.close(); - return size; - } - - let size = 0; - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - for (let tag of pluginFor(this).tags) { - file.initWithPath(tag.fullpath); - if (file.isDirectory()) - size += getDirectorySize(file); - else - size += file.fileSize; - } - return size; - }, - - get pluginLibraries() { - let libs = []; - for (let tag of pluginFor(this).tags) - libs.push(tag.filename); - return libs; - }, - - get pluginFullpath() { - let paths = []; - for (let tag of pluginFor(this).tags) - paths.push(tag.fullpath); - return paths; - }, - - get pluginMimeTypes() { - let types = []; - for (let tag of pluginFor(this).tags) { - let mimeTypes = tag.getMimeTypes({}); - let mimeDescriptions = tag.getMimeDescriptions({}); - let extensions = tag.getExtensions({}); - for (let i = 0; i < mimeTypes.length; i++) { - let type = {}; - type.type = mimeTypes[i]; - type.description = mimeDescriptions[i]; - type.suffixes = extensions[i]; - - types.push(type); - } - } - return types; - }, - - get installDate() { - let date = 0; - for (let tag of pluginFor(this).tags) { - date = Math.max(date, tag.lastModifiedTime); - } - return new Date(date); - }, - - get scope() { - let { tags: [tag] } = pluginFor(this); - let path = tag.fullpath; - // Plugins inside the application directory are in the application scope - let dir = Services.dirsvc.get("APlugns", Ci.nsIFile); - if (path.startsWith(dir.path)) - return AddonManager.SCOPE_APPLICATION; - - // Plugins inside the profile directory are in the profile scope - dir = Services.dirsvc.get("ProfD", Ci.nsIFile); - if (path.startsWith(dir.path)) - return AddonManager.SCOPE_PROFILE; - - // Plugins anywhere else in the user's home are in the user scope, - // but not all platforms have a home directory. - try { - dir = Services.dirsvc.get("Home", Ci.nsIFile); - if (path.startsWith(dir.path)) - return AddonManager.SCOPE_USER; - } catch (e) { - if (!e.result || e.result != Components.results.NS_ERROR_FAILURE) - throw e; - // Do nothing: missing "Home". - } - - // Any other locations are system scope - return AddonManager.SCOPE_SYSTEM; - }, - - get pendingOperations() { - return AddonManager.PENDING_NONE; - }, - - get operationsRequiringRestart() { - return AddonManager.OP_NEEDS_RESTART_NONE; - }, - - get permissions() { - let { tags: [tag] } = pluginFor(this); - let permissions = 0; - if (tag.isEnabledStateLocked) { - return permissions; - } - if (!this.appDisabled) { - - if (this.userDisabled !== true) - permissions |= AddonManager.PERM_CAN_DISABLE; - - let blocklistState = this.blocklistState; - let isCTPBlocklisted = - (blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE || - blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE); - - if (this.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE && - (Services.prefs.getBoolPref("plugins.click_to_play") || - isCTPBlocklisted)) { - permissions |= AddonManager.PERM_CAN_ASK_TO_ACTIVATE; - } - - if (this.userDisabled !== false && !isCTPBlocklisted) { - permissions |= AddonManager.PERM_CAN_ENABLE; - } - } - return permissions; - }, - - get optionsType() { - if (canDisableFlashProtectedMode(this)) { - return AddonManager.OPTIONS_TYPE_INLINE; - } - return AddonManager.OPTIONS_TYPE_INLINE_INFO; - }, - - get optionsURL() { - return "chrome://mozapps/content/extensions/pluginPrefs.xul"; - }, - - get updateDate() { - return this.installDate; - }, - - get isCompatible() { - return true; - }, - - get isPlatformCompatible() { - return true; - }, - - get providesUpdatesSecurely() { - return true; - }, - - get foreignInstall() { - return true; - }, - - isCompatibleWith: function(aAppVersion, aPlatformVersion) { - return true; - }, - - findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) { - if ("onNoCompatibilityUpdateAvailable" in aListener) - aListener.onNoCompatibilityUpdateAvailable(this); - if ("onNoUpdateAvailable" in aListener) - aListener.onNoUpdateAvailable(this); - if ("onUpdateFinished" in aListener) - aListener.onUpdateFinished(this); - } -}; - -AddonManagerPrivate.registerProvider(PluginProvider, [ - new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 6000, - AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) -]); diff --git a/toolkit/mozapps/webextensions/internal/WebExtensionBootstrap.js b/toolkit/mozapps/webextensions/internal/WebExtensionBootstrap.js deleted file mode 100644 index a920c2eae..000000000 --- a/toolkit/mozapps/webextensions/internal/WebExtensionBootstrap.js +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -Components.utils.import("resource://gre/modules/Extension.jsm"); - -var extension; - -const BOOTSTRAP_REASON_TO_STRING_MAP = { - 1: "APP_STARTUP", - 2: "APP_SHUTDOWN", - 3: "ADDON_ENABLE", - 4: "ADDON_DISABLE", - 5: "ADDON_INSTALL", - 6: "ADDON_UNINSTALL", - 7: "ADDON_UPGRADE", - 8: "ADDON_DOWNGRADE", -} - -function install(data, reason) -{ -} - -function startup(data, reason) -{ - extension = new Extension(data, BOOTSTRAP_REASON_TO_STRING_MAP[reason]); - extension.startup(); -} - -function shutdown(data, reason) -{ - extension.shutdown(); -} - -function uninstall(data, reason) -{ -} diff --git a/toolkit/mozapps/webextensions/internal/XPIProvider.jsm b/toolkit/mozapps/webextensions/internal/XPIProvider.jsm deleted file mode 100644 index 256765439..000000000 --- a/toolkit/mozapps/webextensions/internal/XPIProvider.jsm +++ /dev/null @@ -1,9213 +0,0 @@ - /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -this.EXPORTED_SYMBOLS = ["XPIProvider"]; - -const CONSTANTS = {}; -Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS); -const { ADDON_SIGNING, REQUIRE_SIGNING } = CONSTANTS - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", - "resource://gre/modules/addons/AddonRepository.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", - "resource://gre/modules/ChromeManifestParser.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", - "resource://gre/modules/LightweightThemeManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", - "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Locale", - "resource://gre/modules/Locale.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", - "resource://gre/modules/ZipUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", - "resource://gre/modules/PermissionsUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess", - "resource://devtools/client/framework/ToolboxProcess.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ProductAddonChecker", - "resource://gre/modules/addons/ProductAddonChecker.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", - "resource://gre/modules/UpdateUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils", - "resource://gre/modules/LegacyExtensionsUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "Blocklist", - "@mozilla.org/extensions/blocklist;1", - Ci.nsIBlocklistService); -XPCOMUtils.defineLazyServiceGetter(this, - "ChromeRegistry", - "@mozilla.org/chrome/chrome-registry;1", - "nsIChromeRegistry"); -XPCOMUtils.defineLazyServiceGetter(this, - "ResProtocolHandler", - "@mozilla.org/network/protocol;1?name=resource", - "nsIResProtocolHandler"); -XPCOMUtils.defineLazyServiceGetter(this, - "AddonPolicyService", - "@mozilla.org/addons/policy-service;1", - "nsIAddonPolicyService"); -XPCOMUtils.defineLazyServiceGetter(this, - "AddonPathService", - "@mozilla.org/addon-path-service;1", - "amIAddonPathService"); - -XPCOMUtils.defineLazyGetter(this, "CertUtils", function() { - let certUtils = {}; - Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); - return certUtils; -}); - -Cu.importGlobalProperties(["URL"]); - -const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", - "initWithPath"); - -const PREF_DB_SCHEMA = "extensions.databaseSchema"; -const PREF_INSTALL_CACHE = "extensions.installCache"; -const PREF_XPI_STATE = "extensions.xpiState"; -const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons"; -const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; -const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; -const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending"; -const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin"; -const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; -const PREF_EM_UPDATE_URL = "extensions.update.url"; -const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url"; -const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; -const PREF_EM_EXTENSION_FORMAT = "extensions."; -const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes"; -const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI"; -const PREF_XPI_ENABLED = "xpinstall.enabled"; -const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; -const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest"; -const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; -// xpinstall.signatures.required only supported in dev builds -const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required"; -const PREF_XPI_SIGNATURES_DEV_ROOT = "xpinstall.signatures.dev-root"; -const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall."; -const PREF_XPI_UNPACK = "extensions.alwaysUnpack"; -const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; -const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin"; -const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons"; -const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon."; -const PREF_INTERPOSITION_ENABLED = "extensions.interposition.enabled"; -const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet"; -const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url"; - -const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; -const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; - -const PREF_EM_HOTFIX_ID = "extensions.hotfix.id"; -const PREF_EM_CERT_CHECKATTRIBUTES = "extensions.hotfix.cert.checkAttributes"; -const PREF_EM_HOTFIX_CERTS = "extensions.hotfix.certs."; - -const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul"; -const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; - -const STRING_TYPE_NAME = "type.%ID%.name"; - -const DIR_EXTENSIONS = "extensions"; -const DIR_SYSTEM_ADDONS = "features"; -const DIR_STAGE = "staged"; -const DIR_TRASH = "trash"; - -const FILE_DATABASE = "extensions.json"; -const FILE_OLD_CACHE = "extensions.cache"; -const FILE_RDF_MANIFEST = "install.rdf"; -const FILE_WEB_MANIFEST = "manifest.json"; -const FILE_XPI_ADDONS_LIST = "extensions.ini"; - -const KEY_PROFILEDIR = "ProfD"; -const KEY_ADDON_APP_DIR = "XREAddonAppDir"; -const KEY_TEMPDIR = "TmpD"; -const KEY_APP_DISTRIBUTION = "XREAppDist"; -const KEY_APP_FEATURES = "XREAppFeat"; - -const KEY_APP_PROFILE = "app-profile"; -const KEY_APP_SYSTEM_ADDONS = "app-system-addons"; -const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults"; -const KEY_APP_GLOBAL = "app-global"; -const KEY_APP_SYSTEM_LOCAL = "app-system-local"; -const KEY_APP_SYSTEM_SHARE = "app-system-share"; -const KEY_APP_SYSTEM_USER = "app-system-user"; -const KEY_APP_TEMPORARY = "app-temporary"; - -const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; -const XPI_PERMISSION = "install"; - -const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; -const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; - -const TOOLKIT_ID = "toolkit@mozilla.org"; -const WEBEXTENSIONS_ID = "webextensions@mozilla.org"; -const WEBEXTENSIONS_VERSION = "52.0"; - -const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60; - -XPCOMUtils.defineConstant(this, "DB_SCHEMA", 19); - -const NOTIFICATION_TOOLBOXPROCESS_LOADED = "ToolboxProcessLoaded"; - -// Properties that exist in the install manifest -const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", - "updateKey", "optionsURL", "optionsType", "aboutURL", - "iconURL", "icon64URL"]; -const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; -const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; -const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"]; - -// Properties to cache and reload when an addon installation is pending -const PENDING_INSTALL_METADATA = - ["syncGUID", "targetApplications", "userDisabled", "softDisabled", - "existingAddonID", "sourceURI", "releaseNotesURI", "installDate", - "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"]; - -// Note: When adding/changing/removing items here, remember to change the -// DB schema version to ensure changes are picked up ASAP. -const STATIC_BLOCKLIST_PATTERNS = [ - { creator: "Mozilla Corp.", - level: Blocklist.STATE_BLOCKED, - blockID: "i162" }, - { creator: "Mozilla.org", - level: Blocklist.STATE_BLOCKED, - blockID: "i162" } -]; - - -const BOOTSTRAP_REASONS = { - APP_STARTUP : 1, - APP_SHUTDOWN : 2, - ADDON_ENABLE : 3, - ADDON_DISABLE : 4, - ADDON_INSTALL : 5, - ADDON_UNINSTALL : 6, - ADDON_UPGRADE : 7, - ADDON_DOWNGRADE : 8 -}; - -// Map new string type identifiers to old style nsIUpdateItem types -const TYPES = { - extension: 2, - theme: 4, - locale: 8, - multipackage: 32, - dictionary: 64, - experiment: 128, -}; - -if (!AppConstants.RELEASE_OR_BETA) - TYPES.apiextension = 256; - -// Some add-on types that we track internally are presented as other types -// externally -const TYPE_ALIASES = { - "webextension": "extension", - "apiextension": "extension", -}; - -const CHROME_TYPES = new Set([ - "extension", - "locale", - "experiment", -]); - -const RESTARTLESS_TYPES = new Set([ - "webextension", - "dictionary", - "experiment", - "locale", - "apiextension", -]); - -const SIGNED_TYPES = new Set([ - "webextension", - "extension", - "experiment", - "apiextension", -]); - -// This is a random number array that can be used as "salt" when generating -// an automatic ID based on the directory path of an add-on. It will prevent -// someone from creating an ID for a permanent add-on that could be replaced -// by a temporary add-on (because that would be confusing, I guess). -const TEMP_INSTALL_ID_GEN_SESSION = - new Uint8Array(Float64Array.of(Math.random()).buffer); - -// Whether add-on signing is required. -function mustSign(aType) { - if (!SIGNED_TYPES.has(aType)) - return false; - return REQUIRE_SIGNING || Preferences.get(PREF_XPI_SIGNATURES_REQUIRED, false); -} - -// Keep track of where we are in startup for telemetry -// event happened during XPIDatabase.startup() -const XPI_STARTING = "XPIStarting"; -// event happened after startup() but before the final-ui-startup event -const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup"; -// event happened after final-ui-startup -const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup"; - -const COMPATIBLE_BY_DEFAULT_TYPES = { - extension: true, - dictionary: true -}; - -const MSG_JAR_FLUSH = "AddonJarFlush"; -const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush"; - -var gGlobalScope = this; - -/** - * Valid IDs fit this pattern. - */ -var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i; - -Cu.import("resource://gre/modules/Log.jsm"); -const LOGGER_ID = "addons.xpi"; - -// Create a new logger for use by all objects in this Addons XPI Provider module -// (Requires AddonManager.jsm) -var logger = Log.repository.getLogger(LOGGER_ID); - -const LAZY_OBJECTS = ["XPIDatabase", "XPIDatabaseReconcile"]; -/* globals XPIDatabase, XPIDatabaseReconcile*/ - -var gLazyObjectsLoaded = false; - -function loadLazyObjects() { - let uri = "resource://gre/modules/addons/XPIProviderUtils.js"; - let scope = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { - sandboxName: uri, - wantGlobalProperties: ["TextDecoder"], - }); - - let shared = { - ADDON_SIGNING, - SIGNED_TYPES, - BOOTSTRAP_REASONS, - DB_SCHEMA, - AddonInternal, - XPIProvider, - XPIStates, - syncLoadManifestFromFile, - isUsableAddon, - recordAddonTelemetry, - applyBlocklistChanges, - flushChromeCaches, - canRunInSafeMode, - } - - for (let key of Object.keys(shared)) - scope[key] = shared[key]; - - Services.scriptloader.loadSubScript(uri, scope); - - for (let name of LAZY_OBJECTS) { - delete gGlobalScope[name]; - gGlobalScope[name] = scope[name]; - } - gLazyObjectsLoaded = true; - return scope; -} - -LAZY_OBJECTS.forEach(name => { - Object.defineProperty(gGlobalScope, name, { - get: function() { - let objs = loadLazyObjects(); - return objs[name]; - }, - configurable: true - }); -}); - - -// Behaves like Promise.all except waits for all promises to resolve/reject -// before resolving/rejecting itself -function waitForAllPromises(promises) { - return new Promise((resolve, reject) => { - let shouldReject = false; - let rejectValue = null; - - let newPromises = promises.map( - p => p.catch(value => { - shouldReject = true; - rejectValue = value; - }) - ); - Promise.all(newPromises) - .then((results) => shouldReject ? reject(rejectValue) : resolve(results)); - }); -} - -function findMatchingStaticBlocklistItem(aAddon) { - for (let item of STATIC_BLOCKLIST_PATTERNS) { - if ("creator" in item && typeof item.creator == "string") { - if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) || - (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) { - return item; - } - } - } - return null; -} - -/** - * Converts an iterable of addon objects into a map with the add-on's ID as key. - */ -function addonMap(addons) { - return new Map(addons.map(a => [a.id, a])); -} - -/** - * Sets permissions on a file - * - * @param aFile - * The file or directory to operate on. - * @param aPermissions - * The permisions to set - */ -function setFilePermissions(aFile, aPermissions) { - try { - aFile.permissions = aPermissions; - } - catch (e) { - logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " + - aFile.path, e); - } -} - -/** - * Write a given string to a file - * - * @param file - * The nsIFile instance to write into - * @param string - * The string to write - */ -function writeStringToFile(file, string) { - let stream = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. - createInstance(Ci.nsIConverterOutputStream); - - try { - stream.init(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | - FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, - 0); - converter.init(stream, "UTF-8", 0, 0x0000); - converter.writeString(string); - } - finally { - converter.close(); - stream.close(); - } -} - -/** - * A safe way to install a file or the contents of a directory to a new - * directory. The file or directory is moved or copied recursively and if - * anything fails an attempt is made to rollback the entire operation. The - * operation may also be rolled back to its original state after it has - * completed by calling the rollback method. - * - * Operations can be chained. Calling move or copy multiple times will remember - * the whole set and if one fails all of the operations will be rolled back. - */ -function SafeInstallOperation() { - this._installedFiles = []; - this._createdDirs = []; -} - -SafeInstallOperation.prototype = { - _installedFiles: null, - _createdDirs: null, - - _installFile: function(aFile, aTargetDirectory, aCopy) { - let oldFile = aCopy ? null : aFile.clone(); - let newFile = aFile.clone(); - try { - if (aCopy) { - newFile.copyTo(aTargetDirectory, null); - // copyTo does not update the nsIFile with the new. - newFile = aTargetDirectory.clone(); - newFile.append(aFile.leafName); - // Windows roaming profiles won't properly sync directories if a new file - // has an older lastModifiedTime than a previous file, so update. - newFile.lastModifiedTime = Date.now(); - } - else { - newFile.moveTo(aTargetDirectory, null); - } - } - catch (e) { - logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path + - " to " + aTargetDirectory.path, e); - throw e; - } - this._installedFiles.push({ oldFile: oldFile, newFile: newFile }); - }, - - _installDirectory: function(aDirectory, aTargetDirectory, aCopy) { - if (aDirectory.contains(aTargetDirectory)) { - let err = new Error(`Not installing ${aDirectory} into its own descendent ${aTargetDirectory}`); - logger.error(err); - throw err; - } - - let newDir = aTargetDirectory.clone(); - newDir.append(aDirectory.leafName); - try { - newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - } - catch (e) { - logger.error("Failed to create directory " + newDir.path, e); - throw e; - } - this._createdDirs.push(newDir); - - // Use a snapshot of the directory contents to avoid possible issues with - // iterating over a directory while removing files from it (the YAFFS2 - // embedded filesystem has this issue, see bug 772238), and to remove - // normal files before their resource forks on OSX (see bug 733436). - let entries = getDirectoryEntries(aDirectory, true); - for (let entry of entries) { - try { - this._installDirEntry(entry, newDir, aCopy); - } - catch (e) { - logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " + - entry.path, e); - throw e; - } - } - - // If this is only a copy operation then there is nothing else to do - if (aCopy) - return; - - // The directory should be empty by this point. If it isn't this will throw - // and all of the operations will be rolled back - try { - setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY); - aDirectory.remove(false); - } - catch (e) { - logger.error("Failed to remove directory " + aDirectory.path, e); - throw e; - } - - // Note we put the directory move in after all the file moves so the - // directory is recreated before all the files are moved back - this._installedFiles.push({ oldFile: aDirectory, newFile: newDir }); - }, - - _installDirEntry: function(aDirEntry, aTargetDirectory, aCopy) { - let isDir = null; - - try { - isDir = aDirEntry.isDirectory() && !aDirEntry.isSymlink(); - } - catch (e) { - // If the file has already gone away then don't worry about it, this can - // happen on OSX where the resource fork is automatically moved with the - // data fork for the file. See bug 733436. - if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) - return; - - logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + - " to " + aTargetDirectory.path); - throw e; - } - - try { - if (isDir) - this._installDirectory(aDirEntry, aTargetDirectory, aCopy); - else - this._installFile(aDirEntry, aTargetDirectory, aCopy); - } - catch (e) { - logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + - " to " + aTargetDirectory.path); - throw e; - } - }, - - /** - * Moves a file or directory into a new directory. If an error occurs then all - * files that have been moved will be moved back to their original location. - * - * @param aFile - * The file or directory to be moved. - * @param aTargetDirectory - * The directory to move into, this is expected to be an empty - * directory. - */ - moveUnder: function(aFile, aTargetDirectory) { - try { - this._installDirEntry(aFile, aTargetDirectory, false); - } - catch (e) { - this.rollback(); - throw e; - } - }, - - /** - * Renames a file to a new location. If an error occurs then all - * files that have been moved will be moved back to their original location. - * - * @param aOldLocation - * The old location of the file. - * @param aNewLocation - * The new location of the file. - */ - moveTo: function(aOldLocation, aNewLocation) { - try { - let oldFile = aOldLocation.clone(), newFile = aNewLocation.clone(); - oldFile.moveTo(newFile.parent, newFile.leafName); - this._installedFiles.push({ oldFile: oldFile, newFile: newFile, isMoveTo: true}); - } - catch (e) { - this.rollback(); - throw e; - } - }, - - /** - * Copies a file or directory into a new directory. If an error occurs then - * all new files that have been created will be removed. - * - * @param aFile - * The file or directory to be copied. - * @param aTargetDirectory - * The directory to copy into, this is expected to be an empty - * directory. - */ - copy: function(aFile, aTargetDirectory) { - try { - this._installDirEntry(aFile, aTargetDirectory, true); - } - catch (e) { - this.rollback(); - throw e; - } - }, - - /** - * Rolls back all the moves that this operation performed. If an exception - * occurs here then both old and new directories are left in an indeterminate - * state - */ - rollback: function() { - while (this._installedFiles.length > 0) { - let move = this._installedFiles.pop(); - if (move.isMoveTo) { - move.newFile.moveTo(move.oldDir.parent, move.oldDir.leafName); - } - else if (move.newFile.isDirectory() && !move.newFile.isSymlink()) { - let oldDir = move.oldFile.parent.clone(); - oldDir.append(move.oldFile.leafName); - oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - } - else if (!move.oldFile) { - // No old file means this was a copied file - move.newFile.remove(true); - } - else { - move.newFile.moveTo(move.oldFile.parent, null); - } - } - - while (this._createdDirs.length > 0) - recursiveRemove(this._createdDirs.pop()); - } -}; - -/** - * Sets the userDisabled and softDisabled properties of an add-on based on what - * values those properties had for a previous instance of the add-on. The - * previous instance may be a previous install or in the case of an application - * version change the same add-on. - * - * NOTE: this may modify aNewAddon in place; callers should save the database if - * necessary - * - * @param aOldAddon - * The previous instance of the add-on - * @param aNewAddon - * The new instance of the add-on - * @param aAppVersion - * The optional application version to use when checking the blocklist - * or undefined to use the current application - * @param aPlatformVersion - * The optional platform version to use when checking the blocklist or - * undefined to use the current platform - */ -function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion, - aOldPlatformVersion) { - // Copy the properties by default - aNewAddon.userDisabled = aOldAddon.userDisabled; - aNewAddon.softDisabled = aOldAddon.softDisabled; - - let oldBlocklistState = Blocklist.getAddonBlocklistState(aOldAddon.wrapper, - aOldAppVersion, - aOldPlatformVersion); - let newBlocklistState = Blocklist.getAddonBlocklistState(aNewAddon.wrapper); - - // If the blocklist state hasn't changed then the properties don't need to - // change - if (newBlocklistState == oldBlocklistState) - return; - - if (newBlocklistState == Blocklist.STATE_SOFTBLOCKED) { - if (aNewAddon.type != "theme") { - // The add-on has become softblocked, set softDisabled if it isn't already - // userDisabled - aNewAddon.softDisabled = !aNewAddon.userDisabled; - } - else { - // Themes just get userDisabled to switch back to the default theme - aNewAddon.userDisabled = true; - } - } - else { - // If the new add-on is not softblocked then it cannot be softDisabled - aNewAddon.softDisabled = false; - } -} - -/** - * Evaluates whether an add-on is allowed to run in safe mode. - * - * @param aAddon - * The add-on to check - * @return true if the add-on should run in safe mode - */ -function canRunInSafeMode(aAddon) { - // Even though the updated system add-ons aren't generally run in safe mode we - // include them here so their uninstall functions get called when switching - // back to the default set. - - // TODO product should make the call about temporary add-ons running - // in safe mode. assuming for now that they are. - if (aAddon._installLocation.name == KEY_APP_TEMPORARY) - return true; - - return aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || - aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS; -} - -/** - * Calculates whether an add-on should be appDisabled or not. - * - * @param aAddon - * The add-on to check - * @return true if the add-on should not be appDisabled - */ -function isUsableAddon(aAddon) { - // Hack to ensure the default theme is always usable - if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin) - return true; - - if (mustSign(aAddon.type) && !aAddon.isCorrectlySigned) { - logger.warn(`Add-on ${aAddon.id} is not correctly signed.`); - return false; - } - - if (aAddon.blocklistState == Blocklist.STATE_BLOCKED) { - logger.warn(`Add-on ${aAddon.id} is blocklisted.`); - return false; - } - - // Experiments are installed through an external mechanism that - // limits target audience to compatible clients. We trust it knows what - // it's doing and skip compatibility checks. - // - // This decision does forfeit defense in depth. If the experiments system - // is ever wrong about targeting an add-on to a specific application - // or platform, the client will likely see errors. - if (aAddon.type == "experiment") - return true; - - if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely) { - logger.warn(`Updates for add-on ${aAddon.id} must be provided over HTTPS.`); - return false; - } - - - if (!aAddon.isPlatformCompatible) { - logger.warn(`Add-on ${aAddon.id} is not compatible with platform.`); - return false; - } - - if (aAddon.dependencies.length) { - let isActive = id => { - let active = XPIProvider.activeAddons.get(id); - return active && !active.disable; - }; - - if (aAddon.dependencies.some(id => !isActive(id))) - return false; - } - - if (AddonManager.checkCompatibility) { - if (!aAddon.isCompatible) { - logger.warn(`Add-on ${aAddon.id} is not compatible with application version.`); - return false; - } - } - else { - if (!aAddon.matchingTargetApplication) { - logger.warn(`Add-on ${aAddon.id} is not compatible with target application.`); - return false; - } - } - - return true; -} - -XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", - Ci.nsIRDFService); - -function EM_R(aProperty) { - return gRDF.GetResource(PREFIX_NS_EM + aProperty); -} - -function createAddonDetails(id, aAddon) { - return { - id: id || aAddon.id, - type: aAddon.type, - version: aAddon.version, - multiprocessCompatible: aAddon.multiprocessCompatible, - mpcOptedOut: aAddon.mpcOptedOut, - runInSafeMode: aAddon.runInSafeMode, - dependencies: aAddon.dependencies, - hasEmbeddedWebExtension: aAddon.hasEmbeddedWebExtension, - }; -} - -/** - * Converts an internal add-on type to the type presented through the API. - * - * @param aType - * The internal add-on type - * @return an external add-on type - */ -function getExternalType(aType) { - if (aType in TYPE_ALIASES) - return TYPE_ALIASES[aType]; - return aType; -} - -function getManifestFileForDir(aDir) { - let file = aDir.clone(); - file.append(FILE_RDF_MANIFEST); - if (file.exists() && file.isFile()) - return file; - file.leafName = FILE_WEB_MANIFEST; - if (file.exists() && file.isFile()) - return file; - return null; -} - -function getManifestEntryForZipReader(aZipReader) { - if (aZipReader.hasEntry(FILE_RDF_MANIFEST)) - return FILE_RDF_MANIFEST; - if (aZipReader.hasEntry(FILE_WEB_MANIFEST)) - return FILE_WEB_MANIFEST; - return null; -} - -/** - * Converts a list of API types to a list of API types and any aliases for those - * types. - * - * @param aTypes - * An array of types or null for all types - * @return an array of types or null for all types - */ -function getAllAliasesForTypes(aTypes) { - if (!aTypes) - return null; - - // Build a set of all requested types and their aliases - let typeset = new Set(aTypes); - - for (let alias of Object.keys(TYPE_ALIASES)) { - // Ignore any requested internal types - typeset.delete(alias); - - // Add any alias for the internal type - if (typeset.has(TYPE_ALIASES[alias])) - typeset.add(alias); - } - - return [...typeset]; -} - -/** - * Converts an RDF literal, resource or integer into a string. - * - * @param aLiteral - * The RDF object to convert - * @return a string if the object could be converted or null - */ -function getRDFValue(aLiteral) { - if (aLiteral instanceof Ci.nsIRDFLiteral) - return aLiteral.Value; - if (aLiteral instanceof Ci.nsIRDFResource) - return aLiteral.Value; - if (aLiteral instanceof Ci.nsIRDFInt) - return aLiteral.Value; - return null; -} - -/** - * Gets an RDF property as a string - * - * @param aDs - * The RDF datasource to read the property from - * @param aResource - * The RDF resource to read the property from - * @param aProperty - * The property to read - * @return a string if the property existed or null - */ -function getRDFProperty(aDs, aResource, aProperty) { - return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); -} - -/** - * Reads an AddonInternal object from a manifest stream. - * - * @param aUri - * A |file:| or |jar:| URL for the manifest - * @return an AddonInternal object - * @throws if the install manifest in the stream is corrupt or could not - * be read - */ -var loadManifestFromWebManifest = Task.async(function*(aUri) { - // We're passed the URI for the manifest file. Get the URI for its - // parent directory. - let uri = NetUtil.newURI("./", null, aUri); - - let extension = new ExtensionData(uri); - - let manifest = yield extension.readManifest(); - - // Read the list of available locales, and pre-load messages for - // all locales. - let locales = yield extension.initAllLocales(); - - // If there were any errors loading the extension, bail out now. - if (extension.errors.length) - throw new Error("Extension is invalid"); - - let bss = (manifest.browser_specific_settings && manifest.browser_specific_settings.gecko) - || (manifest.applications && manifest.applications.gecko) || {}; - if (manifest.browser_specific_settings && manifest.applications) { - logger.warn("Ignoring applications property in manifest"); - } - - // A * is illegal in strict_min_version - if (bss.strict_min_version && bss.strict_min_version.split(".").some(part => part == "*")) { - throw new Error("The use of '*' in strict_min_version is invalid"); - } - - let addon = new AddonInternal(); - addon.id = bss.id; - addon.version = manifest.version; - addon.type = "webextension"; - addon.unpack = false; - addon.strictCompatibility = true; - addon.bootstrap = true; - addon.hasBinaryComponents = false; - addon.multiprocessCompatible = true; - addon.internalName = null; - addon.updateURL = bss.update_url; - addon.updateKey = null; - addon.optionsURL = null; - addon.optionsType = null; - addon.aboutURL = null; - addon.dependencies = Object.freeze(Array.from(extension.dependencies)); - - if (manifest.options_ui) { - // Store just the relative path here, the AddonWrapper getURL - // wrapper maps this to a full URL. - addon.optionsURL = manifest.options_ui.page; - if (manifest.options_ui.open_in_tab) - addon.optionsType = AddonManager.OPTIONS_TYPE_TAB; - else - addon.optionsType = AddonManager.OPTIONS_TYPE_INLINE_BROWSER; - } - - // WebExtensions don't use iconURLs - addon.iconURL = null; - addon.icon64URL = null; - addon.icons = manifest.icons || {}; - - addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; - - function getLocale(aLocale) { - // Use the raw manifest, here, since we need values with their - // localization placeholders still in place. - let rawManifest = extension.rawManifest; - - // As a convenience, allow author to be set if its a string bug 1313567. - let creator = typeof(rawManifest.author) === 'string' ? rawManifest.author : null; - let homepageURL = rawManifest.homepage_url; - - // Allow developer to override creator and homepage_url. - if (rawManifest.developer) { - if (rawManifest.developer.name) { - creator = rawManifest.developer.name; - } - if (rawManifest.developer.url) { - homepageURL = rawManifest.developer.url; - } - } - - let result = { - name: extension.localize(rawManifest.name, aLocale), - description: extension.localize(rawManifest.description, aLocale), - creator: extension.localize(creator, aLocale), - homepageURL: extension.localize(homepageURL, aLocale), - - developers: null, - translators: null, - contributors: null, - locales: [aLocale], - }; - return result; - } - - addon.defaultLocale = getLocale(extension.defaultLocale); - addon.locales = Array.from(locales.keys(), getLocale); - - delete addon.defaultLocale.locales; - - addon.targetApplications = [{ - id: WEBEXTENSIONS_ID, - minVersion: bss.strict_min_version, - maxVersion: bss.strict_max_version, - }]; - - addon.targetPlatforms = []; - addon.userDisabled = false; - addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED; - - return addon; -}); - -/** - * Reads an AddonInternal object from an RDF stream. - * - * @param aUri - * The URI that the manifest is being read from - * @param aStream - * An open stream to read the RDF from - * @return an AddonInternal object - * @throws if the install manifest in the RDF stream is corrupt or could not - * be read - */ -let loadManifestFromRDF = Task.async(function*(aUri, aStream) { - function getPropertyArray(aDs, aSource, aProperty) { - let values = []; - let targets = aDs.GetTargets(aSource, EM_R(aProperty), true); - while (targets.hasMoreElements()) - values.push(getRDFValue(targets.getNext())); - - return values; - } - - /** - * Reads locale properties from either the main install manifest root or - * an em:localized section in the install manifest. - * - * @param aDs - * The nsIRDFDatasource to read from - * @param aSource - * The nsIRDFResource to read the properties from - * @param isDefault - * True if the locale is to be read from the main install manifest - * root - * @param aSeenLocales - * An array of locale names already seen for this install manifest. - * Any locale names seen as a part of this function will be added to - * this array - * @return an object containing the locale properties - */ - function readLocale(aDs, aSource, isDefault, aSeenLocales) { - let locale = { }; - if (!isDefault) { - locale.locales = []; - let targets = ds.GetTargets(aSource, EM_R("locale"), true); - while (targets.hasMoreElements()) { - let localeName = getRDFValue(targets.getNext()); - if (!localeName) { - logger.warn("Ignoring empty locale in localized properties"); - continue; - } - if (aSeenLocales.indexOf(localeName) != -1) { - logger.warn("Ignoring duplicate locale in localized properties"); - continue; - } - aSeenLocales.push(localeName); - locale.locales.push(localeName); - } - - if (locale.locales.length == 0) { - logger.warn("Ignoring localized properties with no listed locales"); - return null; - } - } - - for (let prop of PROP_LOCALE_SINGLE) { - locale[prop] = getRDFProperty(aDs, aSource, prop); - } - - for (let prop of PROP_LOCALE_MULTI) { - // Don't store empty arrays - let props = getPropertyArray(aDs, aSource, - prop.substring(0, prop.length - 1)); - if (props.length > 0) - locale[prop] = props; - } - - return locale; - } - - let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. - createInstance(Ci.nsIRDFXMLParser) - let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. - createInstance(Ci.nsIRDFDataSource); - let listener = rdfParser.parseAsync(ds, aUri); - let channel = Cc["@mozilla.org/network/input-stream-channel;1"]. - createInstance(Ci.nsIInputStreamChannel); - channel.setURI(aUri); - channel.contentStream = aStream; - channel.QueryInterface(Ci.nsIChannel); - channel.contentType = "text/xml"; - - listener.onStartRequest(channel, null); - - try { - let pos = 0; - let count = aStream.available(); - while (count > 0) { - listener.onDataAvailable(channel, null, aStream, pos, count); - pos += count; - count = aStream.available(); - } - listener.onStopRequest(channel, null, Components.results.NS_OK); - } - catch (e) { - listener.onStopRequest(channel, null, e.result); - throw e; - } - - let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT); - let addon = new AddonInternal(); - for (let prop of PROP_METADATA) { - addon[prop] = getRDFProperty(ds, root, prop); - } - addon.unpack = getRDFProperty(ds, root, "unpack") == "true"; - - if (!addon.type) { - addon.type = addon.internalName ? "theme" : "extension"; - } - else { - let type = addon.type; - addon.type = null; - for (let name in TYPES) { - if (TYPES[name] == type) { - addon.type = name; - break; - } - } - } - - if (!(addon.type in TYPES)) - throw new Error("Install manifest specifies unknown type: " + addon.type); - - if (addon.type != "multipackage") { - if (!addon.id) - throw new Error("No ID in install manifest"); - if (!gIDTest.test(addon.id)) - throw new Error("Illegal add-on ID " + addon.id); - if (!addon.version) - throw new Error("No version in install manifest"); - } - - addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || - getRDFProperty(ds, root, "strictCompatibility") == "true"; - - // Only read these properties for extensions. - if (addon.type == "extension") { - addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true"; - - let mpcValue = getRDFProperty(ds, root, "multiprocessCompatible"); - addon.multiprocessCompatible = mpcValue == "true"; - addon.mpcOptedOut = mpcValue == "false"; - - addon.hasEmbeddedWebExtension = getRDFProperty(ds, root, "hasEmbeddedWebExtension") == "true"; - - if (addon.optionsType && - addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG && - addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE && - addon.optionsType != AddonManager.OPTIONS_TYPE_TAB && - addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) { - throw new Error("Install manifest specifies unknown type: " + addon.optionsType); - } - - if (addon.hasEmbeddedWebExtension) { - let uri = NetUtil.newURI("webextension/manifest.json", null, aUri); - let embeddedAddon = yield loadManifestFromWebManifest(uri); - if (embeddedAddon.optionsURL) { - if (addon.optionsType || addon.optionsURL) - logger.warn(`Addon ${addon.id} specifies optionsType or optionsURL ` + - `in both install.rdf and manifest.json`); - - addon.optionsURL = embeddedAddon.optionsURL; - addon.optionsType = embeddedAddon.optionsType; - } - } - } - else { - // Some add-on types are always restartless. - if (RESTARTLESS_TYPES.has(addon.type)) { - addon.bootstrap = true; - } - - // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For - // all other types they are silently ignored - addon.optionsURL = null; - addon.optionsType = null; - addon.aboutURL = null; - - if (addon.type == "theme") { - if (!addon.internalName) - throw new Error("Themes must include an internalName property"); - addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true"; - } - } - - addon.defaultLocale = readLocale(ds, root, true); - - let seenLocales = []; - addon.locales = []; - let targets = ds.GetTargets(root, EM_R("localized"), true); - while (targets.hasMoreElements()) { - let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); - let locale = readLocale(ds, target, false, seenLocales); - if (locale) - addon.locales.push(locale); - } - - let dependencies = new Set(); - targets = ds.GetTargets(root, EM_R("dependency"), true); - while (targets.hasMoreElements()) { - let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); - let id = getRDFProperty(ds, target, "id"); - dependencies.add(id); - } - addon.dependencies = Object.freeze(Array.from(dependencies)); - - let seenApplications = []; - addon.targetApplications = []; - targets = ds.GetTargets(root, EM_R("targetApplication"), true); - while (targets.hasMoreElements()) { - let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); - let targetAppInfo = {}; - for (let prop of PROP_TARGETAPP) { - targetAppInfo[prop] = getRDFProperty(ds, target, prop); - } - if (!targetAppInfo.id || !targetAppInfo.minVersion || - !targetAppInfo.maxVersion) { - logger.warn("Ignoring invalid targetApplication entry in install manifest"); - continue; - } - if (seenApplications.indexOf(targetAppInfo.id) != -1) { - logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id + - " in install manifest"); - continue; - } - seenApplications.push(targetAppInfo.id); - addon.targetApplications.push(targetAppInfo); - } - - // Note that we don't need to check for duplicate targetPlatform entries since - // the RDF service coalesces them for us. - let targetPlatforms = getPropertyArray(ds, root, "targetPlatform"); - addon.targetPlatforms = []; - for (let targetPlatform of targetPlatforms) { - let platform = { - os: null, - abi: null - }; - - let pos = targetPlatform.indexOf("_"); - if (pos != -1) { - platform.os = targetPlatform.substring(0, pos); - platform.abi = targetPlatform.substring(pos + 1); - } - else { - platform.os = targetPlatform; - } - - addon.targetPlatforms.push(platform); - } - - // A theme's userDisabled value is true if the theme is not the selected skin - // or if there is an active lightweight theme. We ignore whether softblocking - // is in effect since it would change the active theme. - if (addon.type == "theme") { - addon.userDisabled = !!LightweightThemeManager.currentTheme || - addon.internalName != XPIProvider.selectedSkin; - } - else if (addon.type == "experiment") { - // Experiments are disabled by default. It is up to the Experiments Manager - // to enable them (it drives installation). - addon.userDisabled = true; - } - else { - addon.userDisabled = false; - } - - addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED; - addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; - - // Experiments are managed and updated through an external "experiments - // manager." So disable some built-in mechanisms. - if (addon.type == "experiment") { - addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; - addon.updateURL = null; - addon.updateKey = null; - } - - // icons will be filled by the calling function - addon.icons = {}; - - return addon; -}); - -function defineSyncGUID(aAddon) { - // Define .syncGUID as a lazy property which is also settable - Object.defineProperty(aAddon, "syncGUID", { - get: () => { - // Generate random GUID used for Sync. - let guid = Cc["@mozilla.org/uuid-generator;1"] - .getService(Ci.nsIUUIDGenerator) - .generateUUID().toString(); - - delete aAddon.syncGUID; - aAddon.syncGUID = guid; - return guid; - }, - set: (val) => { - delete aAddon.syncGUID; - aAddon.syncGUID = val; - }, - configurable: true, - enumerable: true, - }); -} - -// Generate a unique ID based on the path to this temporary add-on location. -function generateTemporaryInstallID(aFile) { - const hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA1); - const data = new TextEncoder().encode(aFile.path); - // Make it so this ID cannot be guessed. - const sess = TEMP_INSTALL_ID_GEN_SESSION; - hasher.update(sess, sess.length); - hasher.update(data, data.length); - let id = `${getHashStringForCrypto(hasher)}@temporary-addon`; - logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`); - return id; -} - -/** - * Loads an AddonInternal object from an add-on extracted in a directory. - * - * @param aDir - * The nsIFile directory holding the add-on - * @return an AddonInternal object - * @throws if the directory does not contain a valid install manifest - */ -var loadManifestFromDir = Task.async(function*(aDir, aInstallLocation) { - function getFileSize(aFile) { - if (aFile.isSymlink()) - return 0; - - if (!aFile.isDirectory()) - return aFile.fileSize; - - let size = 0; - let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); - let entry; - while ((entry = entries.nextFile)) - size += getFileSize(entry); - entries.close(); - return size; - } - - function* loadFromRDF(aUri) { - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - fis.init(aUri.file, -1, -1, false); - let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. - createInstance(Ci.nsIBufferedInputStream); - bis.init(fis, 4096); - try { - var addon = yield loadManifestFromRDF(aUri, bis); - } finally { - bis.close(); - fis.close(); - } - - let iconFile = aDir.clone(); - iconFile.append("icon.png"); - - if (iconFile.exists()) { - addon.icons[32] = "icon.png"; - addon.icons[48] = "icon.png"; - } - - let icon64File = aDir.clone(); - icon64File.append("icon64.png"); - - if (icon64File.exists()) { - addon.icons[64] = "icon64.png"; - } - - let file = aDir.clone(); - file.append("chrome.manifest"); - let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file)); - addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, - "binary-component"); - return addon; - } - - let file = getManifestFileForDir(aDir); - if (!file) { - throw new Error("Directory " + aDir.path + " does not contain a valid " + - "install manifest"); - } - - let uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); - - let addon; - if (file.leafName == FILE_WEB_MANIFEST) { - addon = yield loadManifestFromWebManifest(uri); - if (!addon.id) { - if (aInstallLocation == TemporaryInstallLocation) { - addon.id = generateTemporaryInstallID(aDir); - } else { - addon.id = aDir.leafName; - } - } - } else { - addon = yield loadFromRDF(uri); - } - - addon._sourceBundle = aDir.clone(); - addon._installLocation = aInstallLocation; - addon.size = getFileSize(aDir); - addon.signedState = yield verifyDirSignedState(aDir, addon) - .then(({signedState}) => signedState); - addon.appDisabled = !isUsableAddon(addon); - - defineSyncGUID(addon); - - return addon; -}); - -/** - * Loads an AddonInternal object from an nsIZipReader for an add-on. - * - * @param aZipReader - * An open nsIZipReader for the add-on's files - * @return an AddonInternal object - * @throws if the XPI file does not contain a valid install manifest - */ -var loadManifestFromZipReader = Task.async(function*(aZipReader, aInstallLocation) { - function* loadFromRDF(aUri) { - let zis = aZipReader.getInputStream(entry); - let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. - createInstance(Ci.nsIBufferedInputStream); - bis.init(zis, 4096); - try { - var addon = yield loadManifestFromRDF(aUri, bis); - } finally { - bis.close(); - zis.close(); - } - - if (aZipReader.hasEntry("icon.png")) { - addon.icons[32] = "icon.png"; - addon.icons[48] = "icon.png"; - } - - if (aZipReader.hasEntry("icon64.png")) { - addon.icons[64] = "icon64.png"; - } - - // Binary components can only be loaded from unpacked addons. - if (addon.unpack) { - let uri = buildJarURI(aZipReader.file, "chrome.manifest"); - let chromeManifest = ChromeManifestParser.parseSync(uri); - addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, - "binary-component"); - } else { - addon.hasBinaryComponents = false; - } - - return addon; - } - - let entry = getManifestEntryForZipReader(aZipReader); - if (!entry) { - throw new Error("File " + aZipReader.file.path + " does not contain a valid " + - "install manifest"); - } - - let uri = buildJarURI(aZipReader.file, entry); - - let isWebExtension = (entry == FILE_WEB_MANIFEST); - - let addon = isWebExtension ? - yield loadManifestFromWebManifest(uri) : - yield loadFromRDF(uri); - - addon._sourceBundle = aZipReader.file; - addon._installLocation = aInstallLocation; - - addon.size = 0; - let entries = aZipReader.findEntries(null); - while (entries.hasMore()) - addon.size += aZipReader.getEntry(entries.getNext()).realSize; - - let {signedState, cert} = yield verifyZipSignedState(aZipReader.file, addon); - addon.signedState = signedState; - if (isWebExtension && !addon.id) { - if (cert) { - addon.id = cert.commonName; - if (!gIDTest.test(addon.id)) { - throw new Error(`Webextension is signed with an invalid id (${addon.id})`); - } - } - if (!addon.id && aInstallLocation == TemporaryInstallLocation) { - addon.id = generateTemporaryInstallID(aZipReader.file); - } - } - addon.appDisabled = !isUsableAddon(addon); - - defineSyncGUID(addon); - - return addon; -}); - -/** - * Loads an AddonInternal object from an add-on in an XPI file. - * - * @param aXPIFile - * An nsIFile pointing to the add-on's XPI file - * @return an AddonInternal object - * @throws if the XPI file does not contain a valid install manifest - */ -var loadManifestFromZipFile = Task.async(function*(aXPIFile, aInstallLocation) { - let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. - createInstance(Ci.nsIZipReader); - try { - zipReader.open(aXPIFile); - - // Can't return this promise because that will make us close the zip reader - // before it has finished loading the manifest. Wait for the result and then - // return. - let manifest = yield loadManifestFromZipReader(zipReader, aInstallLocation); - return manifest; - } - finally { - zipReader.close(); - } -}); - -function loadManifestFromFile(aFile, aInstallLocation) { - if (aFile.isFile()) - return loadManifestFromZipFile(aFile, aInstallLocation); - return loadManifestFromDir(aFile, aInstallLocation); -} - -/** - * A synchronous method for loading an add-on's manifest. This should only ever - * be used during startup or a sync load of the add-ons DB - */ -function syncLoadManifestFromFile(aFile, aInstallLocation) { - let success = undefined; - let result = null; - - loadManifestFromFile(aFile, aInstallLocation).then(val => { - success = true; - result = val; - }, val => { - success = false; - result = val - }); - - let thread = Services.tm.currentThread; - - while (success === undefined) - thread.processNextEvent(true); - - if (!success) - throw result; - return result; -} - -/** - * Gets an nsIURI for a file within another file, either a directory or an XPI - * file. If aFile is a directory then this will return a file: URI, if it is an - * XPI file then it will return a jar: URI. - * - * @param aFile - * The file containing the resources, must be either a directory or an - * XPI file - * @param aPath - * The path to find the resource at, "/" separated. If aPath is empty - * then the uri to the root of the contained files will be returned - * @return an nsIURI pointing at the resource - */ -function getURIForResourceInFile(aFile, aPath) { - if (aFile.isDirectory()) { - let resource = aFile.clone(); - if (aPath) - aPath.split("/").forEach(part => resource.append(part)); - - return NetUtil.newURI(resource); - } - - return buildJarURI(aFile, aPath); -} - -/** - * Creates a jar: URI for a file inside a ZIP file. - * - * @param aJarfile - * The ZIP file as an nsIFile - * @param aPath - * The path inside the ZIP file - * @return an nsIURI for the file - */ -function buildJarURI(aJarfile, aPath) { - let uri = Services.io.newFileURI(aJarfile); - uri = "jar:" + uri.spec + "!/" + aPath; - return NetUtil.newURI(uri); -} - -/** - * Sends local and remote notifications to flush a JAR file cache entry - * - * @param aJarFile - * The ZIP/XPI/JAR file as a nsIFile - */ -function flushJarCache(aJarFile) { - Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null); - Services.mm.broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path); -} - -function flushChromeCaches() { - // Init this, so it will get the notification. - Services.obs.notifyObservers(null, "startupcache-invalidate", null); - // Flush message manager cached scripts - Services.obs.notifyObservers(null, "message-manager-flush-caches", null); - // Also dispatch this event to child processes - Services.mm.broadcastAsyncMessage(MSG_MESSAGE_MANAGER_CACHES_FLUSH, null); -} - -/** - * Creates and returns a new unique temporary file. The caller should delete - * the file when it is no longer needed. - * - * @return an nsIFile that points to a randomly named, initially empty file in - * the OS temporary files directory - */ -function getTemporaryFile() { - let file = FileUtils.getDir(KEY_TEMPDIR, []); - let random = Math.random().toString(36).replace(/0./, '').substr(-3); - file.append("tmp-" + random + ".xpi"); - file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); - - return file; -} - -/** - * Verifies that a zip file's contents are all signed by the same principal. - * Directory entries and anything in the META-INF directory are not checked. - * - * @param aZip - * A nsIZipReader to check - * @param aCertificate - * The nsIX509Cert to compare against - * @return true if all the contents that should be signed were signed by the - * principal - */ -function verifyZipSigning(aZip, aCertificate) { - var count = 0; - var entries = aZip.findEntries(null); - while (entries.hasMore()) { - var entry = entries.getNext(); - // Nothing in META-INF is in the manifest. - if (entry.substr(0, 9) == "META-INF/") - continue; - // Directory entries aren't in the manifest. - if (entry.substr(-1) == "/") - continue; - count++; - var entryCertificate = aZip.getSigningCert(entry); - if (!entryCertificate || !aCertificate.equals(entryCertificate)) { - return false; - } - } - return aZip.manifestEntriesCount == count; -} - -/** - * Returns the signedState for a given return code and certificate by verifying - * it against the expected ID. - */ -function getSignedStatus(aRv, aCert, aAddonID) { - let expectedCommonName = aAddonID; - if (aAddonID && aAddonID.length > 64) { - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - let data = converter.convertToByteArray(aAddonID, {}); - - let crypto = Cc["@mozilla.org/security/hash;1"]. - createInstance(Ci.nsICryptoHash); - crypto.init(Ci.nsICryptoHash.SHA256); - crypto.update(data, data.length); - expectedCommonName = getHashStringForCrypto(crypto); - } - - switch (aRv) { - case Cr.NS_OK: - if (expectedCommonName && expectedCommonName != aCert.commonName) - return AddonManager.SIGNEDSTATE_BROKEN; - - let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined); - if (hotfixID && hotfixID == aAddonID && Preferences.get(PREF_EM_CERT_CHECKATTRIBUTES, false)) { - // The hotfix add-on has some more rigorous certificate checks - try { - CertUtils.validateCert(aCert, - CertUtils.readCertPrefs(PREF_EM_HOTFIX_CERTS)); - } - catch (e) { - logger.warn("The hotfix add-on was not signed by the expected " + - "certificate and so will not be installed.", e); - return AddonManager.SIGNEDSTATE_BROKEN; - } - } - - if (aCert.organizationalUnit == "Mozilla Components") - return AddonManager.SIGNEDSTATE_SYSTEM; - - return /preliminary/i.test(aCert.organizationalUnit) - ? AddonManager.SIGNEDSTATE_PRELIMINARY - : AddonManager.SIGNEDSTATE_SIGNED; - case Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED: - return AddonManager.SIGNEDSTATE_MISSING; - case Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID: - case Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID: - case Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING: - case Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE: - case Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY: - case Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY: - return AddonManager.SIGNEDSTATE_BROKEN; - default: - // Any other error indicates that either the add-on isn't signed or it - // is signed by a signature that doesn't chain to the trusted root. - return AddonManager.SIGNEDSTATE_UNKNOWN; - } -} - -function shouldVerifySignedState(aAddon) { - // Updated system add-ons should always have their signature checked - if (aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS) - return true; - - // We don't care about signatures for default system add-ons - if (aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS) - return false; - - // Hotfixes should always have their signature checked - let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined); - if (hotfixID && aAddon.id == hotfixID) - return true; - - // Otherwise only check signatures if signing is enabled and the add-on is one - // of the signed types. - return ADDON_SIGNING && SIGNED_TYPES.has(aAddon.type); -} - -let gCertDB = Cc["@mozilla.org/security/x509certdb;1"] - .getService(Ci.nsIX509CertDB); - -/** - * Verifies that a zip file's contents are all correctly signed by an - * AMO-issued certificate - * - * @param aFile - * the xpi file to check - * @param aAddon - * the add-on object to verify - * @return a Promise that resolves to an object with properties: - * signedState: an AddonManager.SIGNEDSTATE_* constant - * cert: an nsIX509Cert - */ -function verifyZipSignedState(aFile, aAddon) { - if (!shouldVerifySignedState(aAddon)) - return Promise.resolve({ - signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED, - cert: null - }); - - let root = Ci.nsIX509CertDB.AddonsPublicRoot; - if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false)) - root = Ci.nsIX509CertDB.AddonsStageRoot; - - return new Promise(resolve => { - let callback = { - openSignedAppFileFinished: function(aRv, aZipReader, aCert) { - if (aZipReader) - aZipReader.close(); - resolve({ - signedState: getSignedStatus(aRv, aCert, aAddon.id), - cert: aCert - }); - } - }; - // This allows the certificate DB to get the raw JS callback object so the - // test code can pass through objects that XPConnect would reject. - callback.wrappedJSObject = callback; - - gCertDB.openSignedAppFileAsync(root, aFile, callback); - }); -} - -/** - * Verifies that a directory's contents are all correctly signed by an - * AMO-issued certificate - * - * @param aDir - * the directory to check - * @param aAddon - * the add-on object to verify - * @return a Promise that resolves to an object with properties: - * signedState: an AddonManager.SIGNEDSTATE_* constant - * cert: an nsIX509Cert - */ -function verifyDirSignedState(aDir, aAddon) { - if (!shouldVerifySignedState(aAddon)) - return Promise.resolve({ - signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED, - cert: null, - }); - - let root = Ci.nsIX509CertDB.AddonsPublicRoot; - if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false)) - root = Ci.nsIX509CertDB.AddonsStageRoot; - - return new Promise(resolve => { - let callback = { - verifySignedDirectoryFinished: function(aRv, aCert) { - resolve({ - signedState: getSignedStatus(aRv, aCert, aAddon.id), - cert: null, - }); - } - }; - // This allows the certificate DB to get the raw JS callback object so the - // test code can pass through objects that XPConnect would reject. - callback.wrappedJSObject = callback; - - gCertDB.verifySignedDirectoryAsync(root, aDir, callback); - }); -} - -/** - * Verifies that a bundle's contents are all correctly signed by an - * AMO-issued certificate - * - * @param aBundle - * the nsIFile for the bundle to check, either a directory or zip file - * @param aAddon - * the add-on object to verify - * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant. - */ -function verifyBundleSignedState(aBundle, aAddon) { - let promise = aBundle.isFile() ? verifyZipSignedState(aBundle, aAddon) - : verifyDirSignedState(aBundle, aAddon); - return promise.then(({signedState}) => signedState); -} - -/** - * Replaces %...% strings in an addon url (update and updateInfo) with - * appropriate values. - * - * @param aAddon - * The AddonInternal representing the add-on - * @param aUri - * The uri to escape - * @param aUpdateType - * An optional number representing the type of update, only applicable - * when creating a url for retrieving an update manifest - * @param aAppVersion - * The optional application version to use for %APP_VERSION% - * @return the appropriately escaped uri. - */ -function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion) -{ - let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion); - - // If there is an updateType then replace the UPDATE_TYPE string - if (aUpdateType) - uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType); - - // If this add-on has compatibility information for either the current - // application or toolkit then replace the ITEM_MAXAPPVERSION with the - // maxVersion - let app = aAddon.matchingTargetApplication; - if (app) - var maxVersion = app.maxVersion; - else - maxVersion = ""; - uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion); - - let compatMode = "normal"; - if (!AddonManager.checkCompatibility) - compatMode = "ignore"; - else if (AddonManager.strictCompatibility) - compatMode = "strict"; - uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode); - - return uri; -} - -function removeAsync(aFile) { - return Task.spawn(function*() { - let info = null; - try { - info = yield OS.File.stat(aFile.path); - if (info.isDir) - yield OS.File.removeDir(aFile.path); - else - yield OS.File.remove(aFile.path); - } - catch (e) { - if (!(e instanceof OS.File.Error) || ! e.becauseNoSuchFile) - throw e; - // The file has already gone away - return; - } - }); -} - -/** - * Recursively removes a directory or file fixing permissions when necessary. - * - * @param aFile - * The nsIFile to remove - */ -function recursiveRemove(aFile) { - let isDir = null; - - try { - isDir = aFile.isDirectory(); - } - catch (e) { - // If the file has already gone away then don't worry about it, this can - // happen on OSX where the resource fork is automatically moved with the - // data fork for the file. See bug 733436. - if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) - return; - if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) - return; - - throw e; - } - - setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY - : FileUtils.PERMS_FILE); - - try { - aFile.remove(true); - return; - } - catch (e) { - if (!aFile.isDirectory() || aFile.isSymlink()) { - logger.error("Failed to remove file " + aFile.path, e); - throw e; - } - } - - // Use a snapshot of the directory contents to avoid possible issues with - // iterating over a directory while removing files from it (the YAFFS2 - // embedded filesystem has this issue, see bug 772238), and to remove - // normal files before their resource forks on OSX (see bug 733436). - let entries = getDirectoryEntries(aFile, true); - entries.forEach(recursiveRemove); - - try { - aFile.remove(true); - } - catch (e) { - logger.error("Failed to remove empty directory " + aFile.path, e); - throw e; - } -} - -/** - * Returns the timestamp and leaf file name of the most recently modified - * entry in a directory, - * or simply the file's own timestamp if it is not a directory. - * Also returns the total number of items (directories and files) visited in the scan - * - * @param aFile - * A non-null nsIFile object - * @return [File Name, Epoch time, items visited], as described above. - */ -function recursiveLastModifiedTime(aFile) { - try { - let modTime = aFile.lastModifiedTime; - let fileName = aFile.leafName; - if (aFile.isFile()) - return [fileName, modTime, 1]; - - if (aFile.isDirectory()) { - let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); - let entry; - let totalItems = 1; - while ((entry = entries.nextFile)) { - let [subName, subTime, items] = recursiveLastModifiedTime(entry); - totalItems += items; - if (subTime > modTime) { - modTime = subTime; - fileName = subName; - } - } - entries.close(); - return [fileName, modTime, totalItems]; - } - } - catch (e) { - logger.warn("Problem getting last modified time for " + aFile.path, e); - } - - // If the file is something else, just ignore it. - return ["", 0, 0]; -} - -/** - * Gets a snapshot of directory entries. - * - * @param aDir - * Directory to look at - * @param aSortEntries - * True to sort entries by filename - * @return An array of nsIFile, or an empty array if aDir is not a readable directory - */ -function getDirectoryEntries(aDir, aSortEntries) { - let dirEnum; - try { - dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); - let entries = []; - while (dirEnum.hasMoreElements()) - entries.push(dirEnum.nextFile); - - if (aSortEntries) { - entries.sort(function(a, b) { - return a.path > b.path ? -1 : 1; - }); - } - - return entries - } - catch (e) { - logger.warn("Can't iterate directory " + aDir.path, e); - return []; - } - finally { - if (dirEnum) { - dirEnum.close(); - } - } -} - -/** - * Record a bit of per-addon telemetry - * @param aAddon the addon to record - */ -function recordAddonTelemetry(aAddon) { - let locale = aAddon.defaultLocale; - if (locale) { - if (locale.name) - XPIProvider.setTelemetry(aAddon.id, "name", locale.name); - if (locale.creator) - XPIProvider.setTelemetry(aAddon.id, "creator", locale.creator); - } -} - -/** - * The on-disk state of an individual XPI, created from an Object - * as stored in the 'extensions.xpiState' pref. - */ -function XPIState(saved) { - for (let [short, long] of XPIState.prototype.fields) { - if (short in saved) { - this[long] = saved[short]; - } - } -} - -XPIState.prototype = { - fields: [['d', 'descriptor'], - ['e', 'enabled'], - ['v', 'version'], - ['st', 'scanTime'], - ['mt', 'manifestTime']], - /** - * Return the last modified time, based on enabled/disabled - */ - get mtime() { - if (!this.enabled && ('manifestTime' in this) && this.manifestTime > this.scanTime) { - return this.manifestTime; - } - return this.scanTime; - }, - - toJSON() { - let json = {}; - for (let [short, long] of XPIState.prototype.fields) { - if (long in this) { - json[short] = this[long]; - } - } - return json; - }, - - /** - * Update the last modified time for an add-on on disk. - * @param aFile: nsIFile path of the add-on. - * @param aId: The add-on ID. - * @return True if the time stamp has changed. - */ - getModTime(aFile, aId) { - let changed = false; - let scanStarted = Cu.now(); - // For an unknown or enabled add-on, we do a full recursive scan. - if (!('scanTime' in this) || this.enabled) { - logger.debug('getModTime: Recursive scan of ' + aId); - let [modFile, modTime, items] = recursiveLastModifiedTime(aFile); - XPIProvider._mostRecentlyModifiedFile[aId] = modFile; - XPIProvider.setTelemetry(aId, "scan_items", items); - if (modTime != this.scanTime) { - this.scanTime = modTime; - changed = true; - } - } - // if the add-on is disabled, modified time is the install manifest time, if - // any. If no manifest exists, we assume this is a packed .xpi and use - // the time stamp of {path} - try { - // Get the install manifest update time, if any. - let maniFile = getManifestFileForDir(aFile); - if (!(aId in XPIProvider._mostRecentlyModifiedFile)) { - XPIProvider._mostRecentlyModifiedFile[aId] = maniFile.leafName; - } - let maniTime = maniFile.lastModifiedTime; - if (maniTime != this.manifestTime) { - this.manifestTime = maniTime; - changed = true; - } - } catch (e) { - // No manifest - delete this.manifestTime; - try { - let dtime = aFile.lastModifiedTime; - if (dtime != this.scanTime) { - changed = true; - this.scanTime = dtime; - } - } catch (e) { - logger.warn("Can't get modified time of ${file}: ${e}", {file: aFile.path, e: e}); - changed = true; - this.scanTime = 0; - } - } - // Record duration of file-modified check - XPIProvider.setTelemetry(aId, "scan_MS", Math.round(Cu.now() - scanStarted)); - - return changed; - }, - - /** - * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true, - * update the last-modified time. This should probably be made async, but for now we - * don't want to maintain parallel sync and async versions of the scan. - * Caller is responsible for doing XPIStates.save() if necessary. - * @param aDBAddon The DBAddonInternal for this add-on. - * @param aUpdated The add-on was updated, so we must record new modified time. - */ - syncWithDB(aDBAddon, aUpdated = false) { - logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon)); - // If the add-on changes from disabled to enabled, we should re-check the modified time. - // If this is a newly found add-on, it won't have an 'enabled' field but we - // did a full recursive scan in that case, so we don't need to do it again. - // We don't use aDBAddon.active here because it's not updated until after restart. - let mustGetMod = (aDBAddon.visible && !aDBAddon.disabled && !this.enabled); - this.enabled = (aDBAddon.visible && !aDBAddon.disabled); - this.version = aDBAddon.version; - // XXX Eventually also copy bootstrap, etc. - if (aUpdated || mustGetMod) { - this.getModTime(new nsIFile(this.descriptor), aDBAddon.id); - if (this.scanTime != aDBAddon.updateDate) { - aDBAddon.updateDate = this.scanTime; - XPIDatabase.saveChanges(); - } - } - }, -}; - -// Constructor for an ES6 Map that knows how to convert itself into a -// regular object for toJSON(). -function SerializableMap() { - let m = new Map(); - m.toJSON = function() { - let out = {} - for (let [key, val] of m) { - out[key] = val; - } - return out; - }; - return m; -} - -/** - * Keeps track of the state of XPI add-ons on the file system. - */ -this.XPIStates = { - // Map(location name -> Map(add-on ID -> XPIState)) - db: null, - - get size() { - if (!this.db) { - return 0; - } - let count = 0; - for (let location of this.db.values()) { - count += location.size; - } - return count; - }, - - /** - * Load extension state data from preferences. - */ - loadExtensionState() { - let state = {}; - - // Clear out old directory state cache. - Preferences.reset(PREF_INSTALL_CACHE); - - let cache = Preferences.get(PREF_XPI_STATE, "{}"); - try { - state = JSON.parse(cache); - } catch (e) { - logger.warn("Error parsing extensions.xpiState ${state}: ${error}", - {state: cache, error: e}); - } - logger.debug("Loaded add-on state from prefs: ${}", state); - return state; - }, - - /** - * Walk through all install locations, highest priority first, - * comparing the on-disk state of extensions to what is stored in prefs. - * @return true if anything has changed. - */ - getInstallState() { - let oldState = this.loadExtensionState(); - let changed = false; - this.db = new SerializableMap(); - - for (let location of XPIProvider.installLocations) { - // The list of add-on like file/directory names in the install location. - let addons = location.getAddonLocations(); - // The results of scanning this location. - let foundAddons = new SerializableMap(); - - // What our old state thinks should be in this location. - let locState = {}; - if (location.name in oldState) { - locState = oldState[location.name]; - // We've seen this location. - delete oldState[location.name]; - } - - for (let [id, file] of addons) { - if (!(id in locState)) { - logger.debug("New add-on ${id} in ${location}", {id: id, location: location.name}); - let xpiState = new XPIState({d: file.persistentDescriptor}); - changed = xpiState.getModTime(file, id) || changed; - foundAddons.set(id, xpiState); - } else { - let xpiState = new XPIState(locState[id]); - // We found this add-on in the file system - delete locState[id]; - - changed = xpiState.getModTime(file, id) || changed; - - if (file.persistentDescriptor != xpiState.descriptor) { - xpiState.descriptor = file.persistentDescriptor; - changed = true; - } - if (changed) { - logger.debug("Changed add-on ${id} in ${location}", {id: id, location: location.name}); - } - else { - logger.debug("Existing add-on ${id} in ${location}", {id: id, location: location.name}); - } - foundAddons.set(id, xpiState); - } - XPIProvider.setTelemetry(id, "location", location.name); - } - - // Anything left behind in oldState was removed from the file system. - for (let id in locState) { - changed = true; - break; - } - // If we found anything, add this location to our database. - if (foundAddons.size != 0) { - this.db.set(location.name, foundAddons); - } - } - - // If there's anything left in oldState, an install location that held add-ons - // was removed from the browser configuration. - for (let location in oldState) { - changed = true; - break; - } - - logger.debug("getInstallState changed: ${rv}, state: ${state}", - {rv: changed, state: this.db}); - return changed; - }, - - /** - * Get the Map of XPI states for a particular location. - * @param aLocation The name of the install location. - * @return Map (id -> XPIState) or null if there are no add-ons in the location. - */ - getLocation(aLocation) { - return this.db.get(aLocation); - }, - - /** - * Get the XPI state for a specific add-on in a location. - * If the state is not in our cache, return null. - * @param aLocation The name of the location where the add-on is installed. - * @param aId The add-on ID - * @return The XPIState entry for the add-on, or null. - */ - getAddon(aLocation, aId) { - let location = this.db.get(aLocation); - if (!location) { - return null; - } - return location.get(aId); - }, - - /** - * Find the highest priority location of an add-on by ID and return the - * location and the XPIState. - * @param aId The add-on ID - * @return [locationName, XPIState] if the add-on is found, [undefined, undefined] - * if the add-on is not found. - */ - findAddon(aId) { - // Fortunately the Map iterator returns in order of insertion, which is - // also our highest -> lowest priority order. - for (let [name, location] of this.db) { - if (location.has(aId)) { - return [name, location.get(aId)]; - } - } - return [undefined, undefined]; - }, - - /** - * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal. - * @param aAddon DBAddonInternal for the new add-on. - */ - addAddon(aAddon) { - let location = this.db.get(aAddon.location); - if (!location) { - // First add-on in this location. - location = new SerializableMap(); - this.db.set(aAddon.location, location); - } - logger.debug("XPIStates adding add-on ${id} in ${location}: ${descriptor}", aAddon); - let xpiState = new XPIState({d: aAddon.descriptor}); - location.set(aAddon.id, xpiState); - xpiState.syncWithDB(aAddon, true); - XPIProvider.setTelemetry(aAddon.id, "location", aAddon.location); - }, - - /** - * Save the current state of installed add-ons. - * XXX this *totally* should be a .json file using DeferredSave... - */ - save() { - let cache = JSON.stringify(this.db); - Services.prefs.setCharPref(PREF_XPI_STATE, cache); - }, - - /** - * Remove the XPIState for an add-on and save the new state. - * @param aLocation The name of the add-on location. - * @param aId The ID of the add-on. - */ - removeAddon(aLocation, aId) { - logger.debug("Removing XPIState for " + aLocation + ":" + aId); - let location = this.db.get(aLocation); - if (!location) { - return; - } - location.delete(aId); - if (location.size == 0) { - this.db.delete(aLocation); - } - this.save(); - }, -}; - -this.XPIProvider = { - get name() { - return "XPIProvider"; - }, - - // An array of known install locations - installLocations: null, - // A dictionary of known install locations by name - installLocationsByName: null, - // An array of currently active AddonInstalls - installs: null, - // The default skin for the application - defaultSkin: "classic/1.0", - // The current skin used by the application - currentSkin: null, - // The selected skin to be used by the application when it is restarted. This - // will be the same as currentSkin when it is the skin to be used when the - // application is restarted - selectedSkin: null, - // The value of the minCompatibleAppVersion preference - minCompatibleAppVersion: null, - // The value of the minCompatiblePlatformVersion preference - minCompatiblePlatformVersion: null, - // A dictionary of the file descriptors for bootstrappable add-ons by ID - bootstrappedAddons: {}, - // A Map of active addons to their bootstrapScope by ID - activeAddons: new Map(), - // True if the platform could have activated extensions - extensionsActive: false, - // True if all of the add-ons found during startup were installed in the - // application install location - allAppGlobal: true, - // A string listing the enabled add-ons for annotating crash reports - enabledAddons: null, - // Keep track of startup phases for telemetry - runPhase: XPI_STARTING, - // Keep track of the newest file in each add-on, in case we want to - // report it to telemetry. - _mostRecentlyModifiedFile: {}, - // Per-addon telemetry information - _telemetryDetails: {}, - // A Map from an add-on install to its ID - _addonFileMap: new Map(), - // Flag to know if ToolboxProcess.jsm has already been loaded by someone or not - _toolboxProcessLoaded: false, - // Have we started shutting down bootstrap add-ons? - _closing: false, - - /** - * Returns an array of the add-on values in `bootstrappedAddons`, - * sorted so that all of an add-on's dependencies appear in the array - * before itself. - * - * @returns {Array<object>} - * A sorted array of add-on objects. Each value is a copy of the - * corresponding value in the `bootstrappedAddons` object, with an - * additional `id` property, which corresponds to the key in that - * object, which is the same as the add-ons ID. - */ - sortBootstrappedAddons: function() { - let addons = {}; - - // Sort the list of IDs so that the ordering is deterministic. - for (let id of Object.keys(this.bootstrappedAddons).sort()) { - addons[id] = Object.assign({id}, this.bootstrappedAddons[id]); - } - - let res = new Set(); - let seen = new Set(); - - let add = addon => { - seen.add(addon.id); - - for (let id of addon.dependencies || []) { - if (id in addons && !seen.has(id)) { - add(addons[id]); - } - } - - res.add(addon.id); - } - - Object.values(addons).forEach(add); - - return Array.from(res, id => addons[id]); - }, - - /* - * Set a value in the telemetry hash for a given ID - */ - setTelemetry: function(aId, aName, aValue) { - if (!this._telemetryDetails[aId]) - this._telemetryDetails[aId] = {}; - this._telemetryDetails[aId][aName] = aValue; - }, - - // Keep track of in-progress operations that support cancel() - _inProgress: [], - - doing: function(aCancellable) { - this._inProgress.push(aCancellable); - }, - - done: function(aCancellable) { - let i = this._inProgress.indexOf(aCancellable); - if (i != -1) { - this._inProgress.splice(i, 1); - return true; - } - return false; - }, - - cancelAll: function() { - // Cancelling one may alter _inProgress, so don't use a simple iterator - while (this._inProgress.length > 0) { - let c = this._inProgress.shift(); - try { - c.cancel(); - } - catch (e) { - logger.warn("Cancel failed", e); - } - } - }, - - /** - * Adds or updates a URI mapping for an Addon.id. - * - * Mappings should not be removed at any point. This is so that the mappings - * will be still valid after an add-on gets disabled or uninstalled, as - * consumers may still have URIs of (leaked) resources they want to map. - */ - _addURIMapping: function(aID, aFile) { - logger.info("Mapping " + aID + " to " + aFile.path); - this._addonFileMap.set(aID, aFile.path); - - AddonPathService.insertPath(aFile.path, aID); - }, - - /** - * Resolve a URI back to physical file. - * - * Of course, this works only for URIs pointing to local resources. - * - * @param aURI - * URI to resolve - * @return - * resolved nsIFileURL - */ - _resolveURIToFile: function(aURI) { - switch (aURI.scheme) { - case "jar": - case "file": - if (aURI instanceof Ci.nsIJARURI) { - return this._resolveURIToFile(aURI.JARFile); - } - return aURI; - - case "chrome": - aURI = ChromeRegistry.convertChromeURL(aURI); - return this._resolveURIToFile(aURI); - - case "resource": - aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null, - null); - return this._resolveURIToFile(aURI); - - case "view-source": - aURI = Services.io.newURI(aURI.path, null, null); - return this._resolveURIToFile(aURI); - - case "about": - if (aURI.spec == "about:blank") { - // Do not attempt to map about:blank - return null; - } - - let chan; - try { - chan = NetUtil.newChannel({ - uri: aURI, - loadUsingSystemPrincipal: true - }); - } - catch (ex) { - return null; - } - // Avoid looping - if (chan.URI.equals(aURI)) { - return null; - } - // We want to clone the channel URI to avoid accidentially keeping - // unnecessary references to the channel or implementation details - // around. - return this._resolveURIToFile(chan.URI.clone()); - - default: - return null; - } - }, - - /** - * Starts the XPI provider initializes the install locations and prefs. - * - * @param aAppChanged - * A tri-state value. Undefined means the current profile was created - * for this session, true means the profile already existed but was - * last used with an application with a different version number, - * false means that the profile was last used by this version of the - * application. - * @param aOldAppVersion - * The version of the application last run with this profile or null - * if it is a new profile or the version is unknown - * @param aOldPlatformVersion - * The version of the platform last run with this profile or null - * if it is a new profile or the version is unknown - */ - startup: function(aAppChanged, aOldAppVersion, aOldPlatformVersion) { - function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) { - try { - var dir = FileUtils.getDir(aKey, aPaths); - } - catch (e) { - // Some directories aren't defined on some platforms, ignore them - logger.debug("Skipping unavailable install location " + aName); - return; - } - - try { - var location = aLocked ? new DirectoryInstallLocation(aName, dir, aScope) - : new MutableDirectoryInstallLocation(aName, dir, aScope); - } - catch (e) { - logger.warn("Failed to add directory install location " + aName, e); - return; - } - - XPIProvider.installLocations.push(location); - XPIProvider.installLocationsByName[location.name] = location; - } - - function addSystemAddonInstallLocation(aName, aKey, aPaths, aScope) { - try { - var dir = FileUtils.getDir(aKey, aPaths); - } - catch (e) { - // Some directories aren't defined on some platforms, ignore them - logger.debug("Skipping unavailable install location " + aName); - return; - } - - try { - var location = new SystemAddonInstallLocation(aName, dir, aScope, aAppChanged !== false); - } - catch (e) { - logger.warn("Failed to add system add-on install location " + aName, e); - return; - } - - XPIProvider.installLocations.push(location); - XPIProvider.installLocationsByName[location.name] = location; - } - - function addRegistryInstallLocation(aName, aRootkey, aScope) { - try { - var location = new WinRegInstallLocation(aName, aRootkey, aScope); - } - catch (e) { - logger.warn("Failed to add registry install location " + aName, e); - return; - } - - XPIProvider.installLocations.push(location); - XPIProvider.installLocationsByName[location.name] = location; - } - - try { - AddonManagerPrivate.recordTimestamp("XPI_startup_begin"); - - logger.debug("startup"); - this.runPhase = XPI_STARTING; - this.installs = new Set(); - this.installLocations = []; - this.installLocationsByName = {}; - // Hook for tests to detect when saving database at shutdown time fails - this._shutdownError = null; - // Clear this at startup for xpcshell test restarts - this._telemetryDetails = {}; - // Register our details structure with AddonManager - AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails); - - let hasRegistry = ("nsIWindowsRegKey" in Ci); - - let enabledScopes = Preferences.get(PREF_EM_ENABLED_SCOPES, - AddonManager.SCOPE_ALL); - - // These must be in order of priority, highest to lowest, - // for processFileChanges etc. to work - - XPIProvider.installLocations.push(TemporaryInstallLocation); - XPIProvider.installLocationsByName[TemporaryInstallLocation.name] = - TemporaryInstallLocation; - - // The profile location is always enabled - addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR, - [DIR_EXTENSIONS], - AddonManager.SCOPE_PROFILE, false); - - addSystemAddonInstallLocation(KEY_APP_SYSTEM_ADDONS, KEY_PROFILEDIR, - [DIR_SYSTEM_ADDONS], - AddonManager.SCOPE_PROFILE); - - addDirectoryInstallLocation(KEY_APP_SYSTEM_DEFAULTS, KEY_APP_FEATURES, - [], AddonManager.SCOPE_PROFILE, true); - - if (enabledScopes & AddonManager.SCOPE_USER) { - addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt", - [Services.appinfo.ID], - AddonManager.SCOPE_USER, true); - if (hasRegistry) { - addRegistryInstallLocation("winreg-app-user", - Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, - AddonManager.SCOPE_USER); - } - } - - addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_ADDON_APP_DIR, - [DIR_EXTENSIONS], - AddonManager.SCOPE_APPLICATION, true); - - if (enabledScopes & AddonManager.SCOPE_SYSTEM) { - addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD", - [Services.appinfo.ID], - AddonManager.SCOPE_SYSTEM, true); - addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD", - [Services.appinfo.ID], - AddonManager.SCOPE_SYSTEM, true); - if (hasRegistry) { - addRegistryInstallLocation("winreg-app-global", - Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, - AddonManager.SCOPE_SYSTEM); - } - } - - let defaultPrefs = new Preferences({ defaultBranch: true }); - this.defaultSkin = defaultPrefs.get(PREF_GENERAL_SKINS_SELECTEDSKIN, - "classic/1.0"); - this.currentSkin = Preferences.get(PREF_GENERAL_SKINS_SELECTEDSKIN, - this.defaultSkin); - this.selectedSkin = this.currentSkin; - this.applyThemeChange(); - - this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION, - null); - this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, - null); - this.enabledAddons = ""; - - Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false); - Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false); - if (!REQUIRE_SIGNING) - Services.prefs.addObserver(PREF_XPI_SIGNATURES_REQUIRED, this, false); - Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false); - - // Cu.isModuleLoaded can fail here for external XUL apps where there is - // no chrome.manifest that defines resource://devtools. - if (ResProtocolHandler.hasSubstitution("devtools")) { - if (Cu.isModuleLoaded("resource://devtools/client/framework/ToolboxProcess.jsm")) { - // If BrowserToolboxProcess is already loaded, set the boolean to true - // and do whatever is needed - this._toolboxProcessLoaded = true; - BrowserToolboxProcess.on("connectionchange", - this.onDebugConnectionChange.bind(this)); - } else { - // Else, wait for it to load - Services.obs.addObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false); - } - } - - let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion, - aOldPlatformVersion); - - // Changes to installed extensions may have changed which theme is selected - this.applyThemeChange(); - - AddonManagerPrivate.markProviderSafe(this); - - if (aAppChanged && !this.allAppGlobal && - Preferences.get(PREF_EM_SHOW_MISMATCH_UI, true)) { - let addonsToUpdate = this.shouldForceUpdateCheck(aAppChanged); - if (addonsToUpdate) { - this.showUpgradeUI(addonsToUpdate); - flushCaches = true; - } - } - - if (flushCaches) { - Services.obs.notifyObservers(null, "startupcache-invalidate", null); - // UI displayed early in startup (like the compatibility UI) may have - // caused us to cache parts of the skin or locale in memory. These must - // be flushed to allow extension provided skins and locales to take full - // effect - Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null); - Services.obs.notifyObservers(null, "chrome-flush-caches", null); - } - - this.enabledAddons = Preferences.get(PREF_EM_ENABLED_ADDONS, ""); - - if ("nsICrashReporter" in Ci && - Services.appinfo instanceof Ci.nsICrashReporter) { - // Annotate the crash report with relevant add-on information. - try { - Services.appinfo.annotateCrashReport("Theme", this.currentSkin); - } catch (e) { } - try { - Services.appinfo.annotateCrashReport("EMCheckCompatibility", - AddonManager.checkCompatibility); - } catch (e) { } - this.addAddonsToCrashReporter(); - } - - try { - AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin"); - - for (let addon of this.sortBootstrappedAddons()) { - try { - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.persistentDescriptor = addon.descriptor; - let reason = BOOTSTRAP_REASONS.APP_STARTUP; - // Eventually set INSTALLED reason when a bootstrap addon - // is dropped in profile folder and automatically installed - if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) - .indexOf(addon.id) !== -1) - reason = BOOTSTRAP_REASONS.ADDON_INSTALL; - this.callBootstrapMethod(createAddonDetails(addon.id, addon), - file, "startup", reason); - } - catch (e) { - logger.error("Failed to load bootstrap addon " + addon.id + " from " + - addon.descriptor, e); - } - } - AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end"); - } - catch (e) { - logger.error("bootstrap startup failed", e); - AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e); - } - - // Let these shutdown a little earlier when they still have access to most - // of XPCOM - Services.obs.addObserver({ - observe: function(aSubject, aTopic, aData) { - XPIProvider._closing = true; - for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) { - // If no scope has been loaded for this add-on then there is no need - // to shut it down (should only happen when a bootstrapped add-on is - // pending enable) - if (!XPIProvider.activeAddons.has(addon.id)) - continue; - - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.persistentDescriptor = addon.descriptor; - let addonDetails = createAddonDetails(addon.id, addon); - - // If the add-on was pending disable then shut it down and remove it - // from the persisted data. - if (addon.disable) { - XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown", - BOOTSTRAP_REASONS.ADDON_DISABLE); - delete XPIProvider.bootstrappedAddons[addon.id]; - } - else { - XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown", - BOOTSTRAP_REASONS.APP_SHUTDOWN); - } - } - Services.obs.removeObserver(this, "quit-application-granted"); - } - }, "quit-application-granted", false); - - // Detect final-ui-startup for telemetry reporting - Services.obs.addObserver({ - observe: function(aSubject, aTopic, aData) { - AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup"); - XPIProvider.runPhase = XPI_AFTER_UI_STARTUP; - Services.obs.removeObserver(this, "final-ui-startup"); - } - }, "final-ui-startup", false); - - AddonManagerPrivate.recordTimestamp("XPI_startup_end"); - - this.extensionsActive = true; - this.runPhase = XPI_BEFORE_UI_STARTUP; - - let timerManager = Cc["@mozilla.org/updates/timer-manager;1"]. - getService(Ci.nsIUpdateTimerManager); - timerManager.registerTimer("xpi-signature-verification", () => { - this.verifySignatures(); - }, XPI_SIGNATURE_CHECK_PERIOD); - } - catch (e) { - logger.error("startup failed", e); - AddonManagerPrivate.recordException("XPI", "startup failed", e); - } - }, - - /** - * Shuts down the database and releases all references. - * Return: Promise{integer} resolves / rejects with the result of - * flushing the XPI Database if it was loaded, - * 0 otherwise. - */ - shutdown: function() { - logger.debug("shutdown"); - - // Stop anything we were doing asynchronously - this.cancelAll(); - - this.bootstrappedAddons = {}; - this.activeAddons.clear(); - this.enabledAddons = null; - this.allAppGlobal = true; - - // If there are pending operations then we must update the list of active - // add-ons - if (Preferences.get(PREF_PENDING_OPERATIONS, false)) { - AddonManagerPrivate.recordSimpleMeasure("XPIDB_pending_ops", 1); - XPIDatabase.updateActiveAddons(); - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, - !XPIDatabase.writeAddonsList()); - } - - this.installs = null; - this.installLocations = null; - this.installLocationsByName = null; - - // This is needed to allow xpcshell tests to simulate a restart - this.extensionsActive = false; - this._addonFileMap.clear(); - - if (gLazyObjectsLoaded) { - let done = XPIDatabase.shutdown(); - done.then( - ret => { - logger.debug("Notifying XPI shutdown observers"); - Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); - }, - err => { - logger.debug("Notifying XPI shutdown observers"); - this._shutdownError = err; - Services.obs.notifyObservers(null, "xpi-provider-shutdown", err); - } - ); - return done; - } - logger.debug("Notifying XPI shutdown observers"); - Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); - return undefined; - }, - - /** - * Applies any pending theme change to the preferences. - */ - applyThemeChange: function() { - if (!Preferences.get(PREF_DSS_SWITCHPENDING, false)) - return; - - // Tell the Chrome Registry which Skin to select - try { - this.selectedSkin = Preferences.get(PREF_DSS_SKIN_TO_SELECT); - Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, - this.selectedSkin); - Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); - logger.debug("Changed skin to " + this.selectedSkin); - this.currentSkin = this.selectedSkin; - } - catch (e) { - logger.error("Error applying theme change", e); - } - Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); - }, - - /** - * If the application has been upgraded and there are add-ons outside the - * application directory then we may need to synchronize compatibility - * information but only if the mismatch UI isn't disabled. - * - * @returns False if no update check is needed, otherwise an array of add-on - * IDs to check for updates. Array may be empty if no add-ons can be/need - * to be updated, but the metadata check needs to be performed. - */ - shouldForceUpdateCheck: function(aAppChanged) { - AddonManagerPrivate.recordSimpleMeasure("XPIDB_metadata_age", AddonRepository.metadataAge()); - - let startupChanges = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED); - logger.debug("shouldForceUpdateCheck startupChanges: " + startupChanges.toSource()); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_disabled", startupChanges.length); - - let forceUpdate = []; - if (startupChanges.length > 0) { - let addons = XPIDatabase.getAddons(); - for (let addon of addons) { - if ((startupChanges.indexOf(addon.id) != -1) && - (addon.permissions() & AddonManager.PERM_CAN_UPGRADE) && - !addon.isCompatible) { - logger.debug("shouldForceUpdateCheck: can upgrade disabled add-on " + addon.id); - forceUpdate.push(addon.id); - } - } - } - - if (AddonRepository.isMetadataStale()) { - logger.debug("shouldForceUpdateCheck: metadata is stale"); - return forceUpdate; - } - if (forceUpdate.length > 0) { - return forceUpdate; - } - - return false; - }, - - /** - * Shows the "Compatibility Updates" UI. - * - * @param aAddonIDs - * Array opf addon IDs that were disabled by the application update, and - * should therefore be checked for updates. - */ - showUpgradeUI: function(aAddonIDs) { - logger.debug("XPI_showUpgradeUI: " + aAddonIDs.toSource()); - Services.telemetry.getHistogramById("ADDON_MANAGER_UPGRADE_UI_SHOWN").add(1); - - // Flip a flag to indicate that we interrupted startup with an interactive prompt - Services.startup.interrupted = true; - - var variant = Cc["@mozilla.org/variant;1"]. - createInstance(Ci.nsIWritableVariant); - variant.setFromVariant(aAddonIDs); - - // This *must* be modal as it has to block startup. - var features = "chrome,centerscreen,dialog,titlebar,modal"; - var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. - getService(Ci.nsIWindowWatcher); - ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant); - - // Ensure any changes to the add-ons list are flushed to disk - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, - !XPIDatabase.writeAddonsList()); - }, - - updateSystemAddons: Task.async(function*() { - let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS]; - if (!systemAddonLocation) - return; - - // Don't do anything in safe mode - if (Services.appinfo.inSafeMode) - return; - - // Download the list of system add-ons - let url = Preferences.get(PREF_SYSTEM_ADDON_UPDATE_URL, null); - if (!url) { - yield systemAddonLocation.cleanDirectories(); - return; - } - - url = UpdateUtils.formatUpdateURL(url); - - logger.info(`Starting system add-on update check from ${url}.`); - let res = yield ProductAddonChecker.getProductAddonList(url); - - // If there was no list then do nothing. - if (!res || !res.gmpAddons) { - logger.info("No system add-ons list was returned."); - yield systemAddonLocation.cleanDirectories(); - return; - } - - let addonList = new Map( - res.gmpAddons.map(spec => [spec.id, { spec, path: null, addon: null }])); - - let getAddonsInLocation = (location) => { - return new Promise(resolve => { - XPIDatabase.getAddonsInLocation(location, resolve); - }); - }; - - let setMatches = (wanted, existing) => { - if (wanted.size != existing.size) - return false; - - for (let [id, addon] of existing) { - let wantedInfo = wanted.get(id); - - if (!wantedInfo) - return false; - if (wantedInfo.spec.version != addon.version) - return false; - } - - return true; - }; - - // If this matches the current set in the profile location then do nothing. - let updatedAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_ADDONS)); - if (setMatches(addonList, updatedAddons)) { - logger.info("Retaining existing updated system add-ons."); - yield systemAddonLocation.cleanDirectories(); - return; - } - - // If this matches the current set in the default location then reset the - // updated set. - let defaultAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS)); - if (setMatches(addonList, defaultAddons)) { - logger.info("Resetting system add-ons."); - systemAddonLocation.resetAddonSet(); - yield systemAddonLocation.cleanDirectories(); - return; - } - - // Download all the add-ons - let downloadAddon = Task.async(function*(item) { - try { - let sourceAddon = updatedAddons.get(item.spec.id); - if (sourceAddon && sourceAddon.version == item.spec.version) { - // Copying the file to a temporary location has some benefits. If the - // file is locked and cannot be read then we'll fall back to - // downloading a fresh copy. It also means we don't have to remember - // whether to delete the temporary copy later. - try { - let path = OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon"); - let unique = yield OS.File.openUnique(path); - unique.file.close(); - yield OS.File.copy(sourceAddon._sourceBundle.path, unique.path); - // Make sure to update file modification times so this is detected - // as a new add-on. - yield OS.File.setDates(unique.path); - item.path = unique.path; - } - catch (e) { - logger.warn(`Failed make temporary copy of ${sourceAddon._sourceBundle.path}.`, e); - } - } - if (!item.path) { - item.path = yield ProductAddonChecker.downloadAddon(item.spec); - } - item.addon = yield loadManifestFromFile(nsIFile(item.path), systemAddonLocation); - } - catch (e) { - logger.error(`Failed to download system add-on ${item.spec.id}`, e); - } - }); - yield Promise.all(Array.from(addonList.values()).map(downloadAddon)); - - // The download promises all resolve regardless, now check if they all - // succeeded - let validateAddon = (item) => { - if (item.spec.id != item.addon.id) { - logger.warn(`Downloaded system add-on expected to be ${item.spec.id} but was ${item.addon.id}.`); - return false; - } - - if (item.spec.version != item.addon.version) { - logger.warn(`Expected system add-on ${item.spec.id} to be version ${item.spec.version} but was ${item.addon.version}.`); - return false; - } - - if (!systemAddonLocation.isValidAddon(item.addon)) - return false; - - return true; - } - - if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) { - throw new Error("Rejecting updated system add-on set that either could not " + - "be downloaded or contained unusable add-ons."); - } - - // Install into the install location - logger.info("Installing new system add-on set"); - yield systemAddonLocation.installAddonSet(Array.from(addonList.values()) - .map(a => a.addon)); - }), - - /** - * Verifies that all installed add-ons are still correctly signed. - */ - verifySignatures: function() { - XPIDatabase.getAddonList(a => true, (addons) => { - Task.spawn(function*() { - let changes = { - enabled: [], - disabled: [] - }; - - for (let addon of addons) { - // The add-on might have vanished, we'll catch that on the next startup - if (!addon._sourceBundle.exists()) - continue; - - let signedState = yield verifyBundleSignedState(addon._sourceBundle, addon); - - if (signedState != addon.signedState) { - addon.signedState = signedState; - AddonManagerPrivate.callAddonListeners("onPropertyChanged", - addon.wrapper, - ["signedState"]); - } - - let disabled = XPIProvider.updateAddonDisabledState(addon); - if (disabled !== undefined) - changes[disabled ? "disabled" : "enabled"].push(addon.id); - } - - XPIDatabase.saveChanges(); - - Services.obs.notifyObservers(null, "xpi-signature-changed", JSON.stringify(changes)); - }).then(null, err => { - logger.error("XPI_verifySignature: " + err); - }) - }); - }, - - /** - * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref). - */ - persistBootstrappedAddons: function() { - // Experiments are disabled upon app load, so don't persist references. - let filtered = {}; - for (let id in this.bootstrappedAddons) { - let entry = this.bootstrappedAddons[id]; - if (entry.type == "experiment") { - continue; - } - - filtered[id] = entry; - } - - Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, - JSON.stringify(filtered)); - }, - - /** - * Adds a list of currently active add-ons to the next crash report. - */ - addAddonsToCrashReporter: function() { - if (!("nsICrashReporter" in Ci) || - !(Services.appinfo instanceof Ci.nsICrashReporter)) - return; - - // In safe mode no add-ons are loaded so we should not include them in the - // crash report - if (Services.appinfo.inSafeMode) - return; - - let data = this.enabledAddons; - for (let id in this.bootstrappedAddons) { - data += (data ? "," : "") + encodeURIComponent(id) + ":" + - encodeURIComponent(this.bootstrappedAddons[id].version); - } - - try { - Services.appinfo.annotateCrashReport("Add-ons", data); - } - catch (e) { } - - let TelemetrySession = - Cu.import("resource://gre/modules/TelemetrySession.jsm", {}).TelemetrySession; - TelemetrySession.setAddOns(data); - }, - - /** - * Check the staging directories of install locations for any add-ons to be - * installed or add-ons to be uninstalled. - * - * @param aManifests - * A dictionary to add detected install manifests to for the purpose - * of passing through updated compatibility information - * @return true if an add-on was installed or uninstalled - */ - processPendingFileChanges: function(aManifests) { - let changed = false; - for (let location of this.installLocations) { - aManifests[location.name] = {}; - // We can't install or uninstall anything in locked locations - if (location.locked) { - continue; - } - - let stagingDir = location.getStagingDir(); - - try { - if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory()) - continue; - } - catch (e) { - logger.warn("Failed to find staging directory", e); - continue; - } - - let seenFiles = []; - // Use a snapshot of the directory contents to avoid possible issues with - // iterating over a directory while removing files from it (the YAFFS2 - // embedded filesystem has this issue, see bug 772238), and to remove - // normal files before their resource forks on OSX (see bug 733436). - let stagingDirEntries = getDirectoryEntries(stagingDir, true); - for (let stageDirEntry of stagingDirEntries) { - let id = stageDirEntry.leafName; - - let isDir; - try { - isDir = stageDirEntry.isDirectory(); - } - catch (e) { - if (e.result != Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) - throw e; - // If the file has already gone away then don't worry about it, this - // can happen on OSX where the resource fork is automatically moved - // with the data fork for the file. See bug 733436. - continue; - } - - if (!isDir) { - if (id.substring(id.length - 4).toLowerCase() == ".xpi") { - id = id.substring(0, id.length - 4); - } - else { - if (id.substring(id.length - 5).toLowerCase() != ".json") { - logger.warn("Ignoring file: " + stageDirEntry.path); - seenFiles.push(stageDirEntry.leafName); - } - continue; - } - } - - // Check that the directory's name is a valid ID. - if (!gIDTest.test(id)) { - logger.warn("Ignoring directory whose name is not a valid add-on ID: " + - stageDirEntry.path); - seenFiles.push(stageDirEntry.leafName); - continue; - } - - changed = true; - - if (isDir) { - // Check if the directory contains an install manifest. - let manifest = getManifestFileForDir(stageDirEntry); - - // If the install manifest doesn't exist uninstall this add-on in this - // install location. - if (!manifest) { - logger.debug("Processing uninstall of " + id + " in " + location.name); - - try { - let addonFile = location.getLocationForID(id); - let addonToUninstall = syncLoadManifestFromFile(addonFile, location); - if (addonToUninstall.bootstrap) { - this.callBootstrapMethod(addonToUninstall, addonToUninstall._sourceBundle, - "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL); - } - } - catch (e) { - logger.warn("Failed to call uninstall for " + id, e); - } - - try { - location.uninstallAddon(id); - seenFiles.push(stageDirEntry.leafName); - } - catch (e) { - logger.error("Failed to uninstall add-on " + id + " in " + location.name, e); - } - // The file check later will spot the removal and cleanup the database - continue; - } - } - - aManifests[location.name][id] = null; - let existingAddonID = id; - - let jsonfile = stagingDir.clone(); - jsonfile.append(id + ".json"); - // Assume this was a foreign install if there is no cached metadata file - let foreignInstall = !jsonfile.exists(); - let addon; - - try { - addon = syncLoadManifestFromFile(stageDirEntry, location); - } - catch (e) { - logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e); - // This add-on can't be installed so just remove it now - seenFiles.push(stageDirEntry.leafName); - seenFiles.push(jsonfile.leafName); - continue; - } - - if (mustSign(addon.type) && - addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) { - logger.warn("Refusing to install staged add-on " + id + " with signed state " + addon.signedState); - seenFiles.push(stageDirEntry.leafName); - seenFiles.push(jsonfile.leafName); - continue; - } - - // Check for a cached metadata for this add-on, it may contain updated - // compatibility information - if (!foreignInstall) { - logger.debug("Found updated metadata for " + id + " in " + location.name); - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - let json = Cc["@mozilla.org/dom/json;1"]. - createInstance(Ci.nsIJSON); - - try { - fis.init(jsonfile, -1, 0, 0); - let metadata = json.decodeFromStream(fis, jsonfile.fileSize); - addon.importMetadata(metadata); - - // Pass this through to addMetadata so it knows this add-on was - // likely installed through the UI - aManifests[location.name][id] = addon; - } - catch (e) { - // If some data can't be recovered from the cached metadata then it - // is unlikely to be a problem big enough to justify throwing away - // the install, just log an error and continue - logger.error("Unable to read metadata from " + jsonfile.path, e); - } - finally { - fis.close(); - } - } - seenFiles.push(jsonfile.leafName); - - existingAddonID = addon.existingAddonID || id; - - var oldBootstrap = null; - logger.debug("Processing install of " + id + " in " + location.name); - if (existingAddonID in this.bootstrappedAddons) { - try { - var existingAddon = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - existingAddon.persistentDescriptor = this.bootstrappedAddons[existingAddonID].descriptor; - if (existingAddon.exists()) { - oldBootstrap = this.bootstrappedAddons[existingAddonID]; - - // We'll be replacing a currently active bootstrapped add-on so - // call its uninstall method - let newVersion = addon.version; - let oldVersion = oldBootstrap.version; - let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ? - BOOTSTRAP_REASONS.ADDON_UPGRADE : - BOOTSTRAP_REASONS.ADDON_DOWNGRADE; - - this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap), - existingAddon, "uninstall", uninstallReason, - { newVersion: newVersion }); - this.unloadBootstrapScope(existingAddonID); - flushChromeCaches(); - } - } - catch (e) { - } - } - - try { - addon._sourceBundle = location.installAddon({ - id, - source: stageDirEntry, - existingAddonID - }); - } - catch (e) { - logger.error("Failed to install staged add-on " + id + " in " + location.name, - e); - // Re-create the staged install - new StagedAddonInstall(location, stageDirEntry, addon); - // Make sure not to delete the cached manifest json file - seenFiles.pop(); - - delete aManifests[location.name][id]; - - if (oldBootstrap) { - // Re-install the old add-on - this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap), - existingAddon, "install", - BOOTSTRAP_REASONS.ADDON_INSTALL); - } - continue; - } - } - - try { - location.cleanStagingDir(seenFiles); - } - catch (e) { - // Non-critical, just saves some perf on startup if we clean this up. - logger.debug("Error cleaning staging dir " + stagingDir.path, e); - } - } - return changed; - }, - - /** - * Installs any add-ons located in the extensions directory of the - * application's distribution specific directory into the profile unless a - * newer version already exists or the user has previously uninstalled the - * distributed add-on. - * - * @param aManifests - * A dictionary to add new install manifests to to save having to - * reload them later - * @param aAppChanged - * See checkForChanges - * @return true if any new add-ons were installed - */ - installDistributionAddons: function(aManifests, aAppChanged) { - let distroDir; - try { - distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]); - } - catch (e) { - return false; - } - - if (!distroDir.exists()) - return false; - - if (!distroDir.isDirectory()) - return false; - - let changed = false; - let profileLocation = this.installLocationsByName[KEY_APP_PROFILE]; - - let entries = distroDir.directoryEntries - .QueryInterface(Ci.nsIDirectoryEnumerator); - let entry; - while ((entry = entries.nextFile)) { - - let id = entry.leafName; - - if (entry.isFile()) { - if (id.substring(id.length - 4).toLowerCase() == ".xpi") { - id = id.substring(0, id.length - 4); - } - else { - logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path); - continue; - } - } - else if (!entry.isDirectory()) { - logger.debug("Ignoring distribution add-on that isn't a file or directory: " + - entry.path); - continue; - } - - if (!gIDTest.test(id)) { - logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " + - entry.path); - continue; - } - - /* If this is not an upgrade and we've already handled this extension - * just continue */ - if (!aAppChanged && Preferences.isSet(PREF_BRANCH_INSTALLED_ADDON + id)) { - continue; - } - - let addon; - try { - addon = syncLoadManifestFromFile(entry, profileLocation); - } - catch (e) { - logger.warn("File entry " + entry.path + " contains an invalid add-on", e); - continue; - } - - if (addon.id != id) { - logger.warn("File entry " + entry.path + " contains an add-on with an " + - "incorrect ID") - continue; - } - - let existingEntry = null; - try { - existingEntry = profileLocation.getLocationForID(id); - } - catch (e) { - } - - if (existingEntry) { - let existingAddon; - try { - existingAddon = syncLoadManifestFromFile(existingEntry, profileLocation); - - if (Services.vc.compare(addon.version, existingAddon.version) <= 0) - continue; - } - catch (e) { - // Bad add-on in the profile so just proceed and install over the top - logger.warn("Profile contains an add-on with a bad or missing install " + - "manifest at " + existingEntry.path + ", overwriting", e); - } - } - else if (Preferences.get(PREF_BRANCH_INSTALLED_ADDON + id, false)) { - continue; - } - - // Install the add-on - try { - addon._sourceBundle = profileLocation.installAddon({ id, source: entry, action: "copy" }); - logger.debug("Installed distribution add-on " + id); - - Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true) - - // aManifests may contain a copy of a newly installed add-on's manifest - // and we'll have overwritten that so instead cache our install manifest - // which will later be put into the database in processFileChanges - if (!(KEY_APP_PROFILE in aManifests)) - aManifests[KEY_APP_PROFILE] = {}; - aManifests[KEY_APP_PROFILE][id] = addon; - changed = true; - } - catch (e) { - logger.error("Failed to install distribution add-on " + entry.path, e); - } - } - - entries.close(); - - return changed; - }, - - /** - * Imports the xpinstall permissions from preferences into the permissions - * manager for the user to change later. - */ - importPermissions: function() { - PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH, - XPI_PERMISSION); - }, - - getDependentAddons: function(aAddon) { - return Array.from(XPIDatabase.getAddons()) - .filter(addon => addon.dependencies.includes(aAddon.id)); - }, - - /** - * Checks for any changes that have occurred since the last time the - * application was launched. - * - * @param aAppChanged - * A tri-state value. Undefined means the current profile was created - * for this session, true means the profile already existed but was - * last used with an application with a different version number, - * false means that the profile was last used by this version of the - * application. - * @param aOldAppVersion - * The version of the application last run with this profile or null - * if it is a new profile or the version is unknown - * @param aOldPlatformVersion - * The version of the platform last run with this profile or null - * if it is a new profile or the version is unknown - * @return true if a change requiring a restart was detected - */ - checkForChanges: function(aAppChanged, aOldAppVersion, - aOldPlatformVersion) { - logger.debug("checkForChanges"); - - // Keep track of whether and why we need to open and update the database at - // startup time. - let updateReasons = []; - if (aAppChanged) { - updateReasons.push("appChanged"); - } - - // Load the list of bootstrapped add-ons first so processFileChanges can - // modify it - // XXX eventually get rid of bootstrappedAddons - try { - this.bootstrappedAddons = JSON.parse(Preferences.get(PREF_BOOTSTRAP_ADDONS, - "{}")); - } catch (e) { - logger.warn("Error parsing enabled bootstrapped extensions cache", e); - } - - // First install any new add-ons into the locations, if there are any - // changes then we must update the database with the information in the - // install locations - let manifests = {}; - let updated = this.processPendingFileChanges(manifests); - if (updated) { - updateReasons.push("pendingFileChanges"); - } - - // This will be true if the previous session made changes that affect the - // active state of add-ons but didn't commit them properly (normally due - // to the application crashing) - let hasPendingChanges = Preferences.get(PREF_PENDING_OPERATIONS); - if (hasPendingChanges) { - updateReasons.push("hasPendingChanges"); - } - - // If the application has changed then check for new distribution add-ons - if (Preferences.get(PREF_INSTALL_DISTRO_ADDONS, true)) - { - updated = this.installDistributionAddons(manifests, aAppChanged); - if (updated) { - updateReasons.push("installDistributionAddons"); - } - } - - // Telemetry probe added around getInstallState() to check perf - let telemetryCaptureTime = Cu.now(); - let installChanged = XPIStates.getInstallState(); - let telemetry = Services.telemetry; - telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Math.round(Cu.now() - telemetryCaptureTime)); - if (installChanged) { - updateReasons.push("directoryState"); - } - - let haveAnyAddons = (XPIStates.size > 0); - - // If the schema appears to have changed then we should update the database - if (DB_SCHEMA != Preferences.get(PREF_DB_SCHEMA, 0)) { - // If we don't have any add-ons, just update the pref, since we don't need to - // write the database - if (!haveAnyAddons) { - logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA); - Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); - } - else { - updateReasons.push("schemaChanged"); - } - } - - // If the database doesn't exist and there are add-ons installed then we - // must update the database however if there are no add-ons then there is - // no need to update the database. - let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); - if (!dbFile.exists() && haveAnyAddons) { - updateReasons.push("needNewDatabase"); - } - - // XXX This will go away when we fold bootstrappedAddons into XPIStates. - if (updateReasons.length == 0) { - let bootstrapDescriptors = new Set(Object.keys(this.bootstrappedAddons) - .map(b => this.bootstrappedAddons[b].descriptor)); - - for (let location of XPIStates.db.values()) { - for (let state of location.values()) { - bootstrapDescriptors.delete(state.descriptor); - } - } - - if (bootstrapDescriptors.size > 0) { - logger.warn("Bootstrap state is invalid (missing add-ons: " - + Array.from(bootstrapDescriptors).join(", ") + ")"); - updateReasons.push("missingBootstrapAddon"); - } - } - - // Catch and log any errors during the main startup - try { - let extensionListChanged = false; - // If the database needs to be updated then open it and then update it - // from the filesystem - if (updateReasons.length > 0) { - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons); - XPIDatabase.syncLoadDB(false); - try { - extensionListChanged = XPIDatabaseReconcile.processFileChanges(manifests, - aAppChanged, - aOldAppVersion, - aOldPlatformVersion, - updateReasons.includes("schemaChanged")); - } - catch (e) { - logger.error("Failed to process extension changes at startup", e); - } - } - - if (aAppChanged) { - // When upgrading the app and using a custom skin make sure it is still - // compatible otherwise switch back the default - if (this.currentSkin != this.defaultSkin) { - let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin); - if (!oldSkin || oldSkin.disabled) - this.enableDefaultTheme(); - } - - // When upgrading remove the old extensions cache to force older - // versions to rescan the entire list of extensions - let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true); - try { - if (oldCache.exists()) - oldCache.remove(true); - } - catch (e) { - logger.warn("Unable to remove old extension cache " + oldCache.path, e); - } - } - - // If the application crashed before completing any pending operations then - // we should perform them now. - if (extensionListChanged || hasPendingChanges) { - logger.debug("Updating database with changes to installed add-ons"); - XPIDatabase.updateActiveAddons(); - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, - !XPIDatabase.writeAddonsList()); - Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, - JSON.stringify(this.bootstrappedAddons)); - return true; - } - - logger.debug("No changes found"); - } - catch (e) { - logger.error("Error during startup file checks", e); - } - - // Check that the add-ons list still exists - let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], - true); - // the addons list file should exist if and only if we have add-ons installed - if (addonsList.exists() != haveAnyAddons) { - logger.debug("Add-ons list is invalid, rebuilding"); - XPIDatabase.writeAddonsList(); - } - - return false; - }, - - /** - * Called to test whether this provider supports installing a particular - * mimetype. - * - * @param aMimetype - * The mimetype to check for - * @return true if the mimetype is application/x-xpinstall - */ - supportsMimetype: function(aMimetype) { - return aMimetype == "application/x-xpinstall"; - }, - - /** - * Called to test whether installing XPI add-ons is enabled. - * - * @return true if installing is enabled - */ - isInstallEnabled: function() { - // Default to enabled if the preference does not exist - return Preferences.get(PREF_XPI_ENABLED, true); - }, - - /** - * Called to test whether installing XPI add-ons by direct URL requests is - * whitelisted. - * - * @return true if installing by direct requests is whitelisted - */ - isDirectRequestWhitelisted: function() { - // Default to whitelisted if the preference does not exist. - return Preferences.get(PREF_XPI_DIRECT_WHITELISTED, true); - }, - - /** - * Called to test whether installing XPI add-ons from file referrers is - * whitelisted. - * - * @return true if installing from file referrers is whitelisted - */ - isFileRequestWhitelisted: function() { - // Default to whitelisted if the preference does not exist. - return Preferences.get(PREF_XPI_FILE_WHITELISTED, true); - }, - - /** - * Called to test whether installing XPI add-ons from a URI is allowed. - * - * @param aInstallingPrincipal - * The nsIPrincipal that initiated the install - * @return true if installing is allowed - */ - isInstallAllowed: function(aInstallingPrincipal) { - if (!this.isInstallEnabled()) - return false; - - let uri = aInstallingPrincipal.URI; - - // Direct requests without a referrer are either whitelisted or blocked. - if (!uri) - return this.isDirectRequestWhitelisted(); - - // Local referrers can be whitelisted. - if (this.isFileRequestWhitelisted() && - (uri.schemeIs("chrome") || uri.schemeIs("file"))) - return true; - - this.importPermissions(); - - let permission = Services.perms.testPermissionFromPrincipal(aInstallingPrincipal, XPI_PERMISSION); - if (permission == Ci.nsIPermissionManager.DENY_ACTION) - return false; - - let requireWhitelist = Preferences.get(PREF_XPI_WHITELIST_REQUIRED, true); - if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION)) - return false; - - let requireSecureOrigin = Preferences.get(PREF_INSTALL_REQUIRESECUREORIGIN, true); - let safeSchemes = ["https", "chrome", "file"]; - if (requireSecureOrigin && safeSchemes.indexOf(uri.scheme) == -1) - return false; - - return true; - }, - - /** - * Called to get an AddonInstall to download and install an add-on from a URL. - * - * @param aUrl - * The URL to be installed - * @param aHash - * A hash for the install - * @param aName - * A name for the install - * @param aIcons - * Icon URLs for the install - * @param aVersion - * A version for the install - * @param aBrowser - * The browser performing the install - * @param aCallback - * A callback to pass the AddonInstall to - */ - getInstallForURL: function(aUrl, aHash, aName, aIcons, aVersion, aBrowser, - aCallback) { - createDownloadInstall(function(aInstall) { - aCallback(aInstall.wrapper); - }, aUrl, aHash, aName, aIcons, aVersion, aBrowser); - }, - - /** - * Called to get an AddonInstall to install an add-on from a local file. - * - * @param aFile - * The file to be installed - * @param aCallback - * A callback to pass the AddonInstall to - */ - getInstallForFile: function(aFile, aCallback) { - createLocalInstall(aFile).then(install => { - aCallback(install ? install.wrapper : null); - }); - }, - - /** - * Temporarily installs add-on from a local XPI file or directory. - * As this is intended for development, the signature is not checked and - * the add-on does not persist on application restart. - * - * @param aFile - * An nsIFile for the unpacked add-on directory or XPI file. - * - * @return See installAddonFromLocation return value. - */ - installTemporaryAddon: function(aFile) { - return this.installAddonFromLocation(aFile, TemporaryInstallLocation); - }, - - /** - * Permanently installs add-on from a local XPI file or directory. - * The signature is checked but the add-on persist on application restart. - * - * @param aFile - * An nsIFile for the unpacked add-on directory or XPI file. - * - * @return See installAddonFromLocation return value. - */ - installAddonFromSources: Task.async(function*(aFile) { - let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; - return this.installAddonFromLocation(aFile, location, "proxy"); - }), - - /** - * Installs add-on from a local XPI file or directory. - * - * @param aFile - * An nsIFile for the unpacked add-on directory or XPI file. - * @param aInstallLocation - * Define a custom install location object to use for the install. - * @param aInstallAction - * Optional action mode to use when installing the addon - * (see MutableDirectoryInstallLocation.installAddon) - * - * @return a Promise that resolves to an Addon object on success, or rejects - * if the add-on is not a valid restartless add-on or if the - * same ID is already installed. - */ - installAddonFromLocation: Task.async(function*(aFile, aInstallLocation, aInstallAction) { - if (aFile.exists() && aFile.isFile()) { - flushJarCache(aFile); - } - let addon = yield loadManifestFromFile(aFile, aInstallLocation); - - aInstallLocation.installAddon({ id: addon.id, source: aFile, action: aInstallAction }); - - if (addon.appDisabled) { - let message = `Add-on ${addon.id} is not compatible with application version.`; - - let app = addon.matchingTargetApplication; - if (app) { - if (app.minVersion) { - message += ` add-on minVersion: ${app.minVersion}.`; - } - if (app.maxVersion) { - message += ` add-on maxVersion: ${app.maxVersion}.`; - } - } - throw new Error(message); - } - - if (!addon.bootstrap) { - throw new Error("Only restartless (bootstrap) add-ons" - + " can be installed from sources:", addon.id); - } - let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL; - let oldAddon = yield new Promise( - resolve => XPIDatabase.getVisibleAddonForID(addon.id, resolve)); - if (oldAddon) { - if (!oldAddon.bootstrap) { - logger.warn("Non-restartless Add-on is already installed", addon.id); - throw new Error("Non-restartless add-on with ID " - + oldAddon.id + " is already installed"); - } - else { - logger.warn("Addon with ID " + oldAddon.id + " already installed," - + " older version will be disabled"); - - let existingAddonID = oldAddon.id; - let existingAddon = oldAddon._sourceBundle; - - // We'll be replacing a currently active bootstrapped add-on so - // call its uninstall method - let newVersion = addon.version; - let oldVersion = oldAddon.version; - if (Services.vc.compare(newVersion, oldVersion) >= 0) { - installReason = BOOTSTRAP_REASONS.ADDON_UPGRADE; - } else { - installReason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE; - } - let uninstallReason = installReason; - - if (oldAddon.active) { - XPIProvider.callBootstrapMethod(oldAddon, existingAddon, - "shutdown", uninstallReason, - { newVersion }); - } - this.callBootstrapMethod(oldAddon, existingAddon, - "uninstall", uninstallReason, { newVersion }); - this.unloadBootstrapScope(existingAddonID); - flushChromeCaches(); - } - } - - let file = addon._sourceBundle; - - XPIProvider._addURIMapping(addon.id, file); - XPIProvider.callBootstrapMethod(addon, file, "install", installReason); - addon.state = AddonManager.STATE_INSTALLED; - logger.debug("Install of temporary addon in " + aFile.path + " completed."); - addon.visible = true; - addon.enabled = true; - addon.active = true; - - addon = XPIDatabase.addAddonMetadata(addon, file.persistentDescriptor); - - XPIStates.addAddon(addon); - XPIDatabase.saveChanges(); - XPIStates.save(); - - AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper, - false); - XPIProvider.callBootstrapMethod(addon, file, "startup", - BOOTSTRAP_REASONS.ADDON_ENABLE); - AddonManagerPrivate.callInstallListeners("onExternalInstall", - null, addon.wrapper, - oldAddon ? oldAddon.wrapper : null, - false); - AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper); - - return addon.wrapper; - }), - - /** - * Returns an Addon corresponding to an instance ID. - * @param aInstanceID - * An Addon Instance ID - * @return {Promise} - * @resolves The found Addon or null if no such add-on exists. - * @rejects Never - * @throws if the aInstanceID argument is not specified - */ - getAddonByInstanceID: function(aInstanceID) { - if (!aInstanceID || typeof aInstanceID != "symbol") - throw Components.Exception("aInstanceID must be a Symbol()", - Cr.NS_ERROR_INVALID_ARG); - - for (let [id, val] of this.activeAddons) { - if (aInstanceID == val.instanceID) { - if (val.safeWrapper) { - return Promise.resolve(val.safeWrapper); - } - - return new Promise(resolve => { - this.getAddonByID(id, function(addon) { - val.safeWrapper = new PrivateWrapper(addon); - resolve(val.safeWrapper); - }); - }); - } - } - - return Promise.resolve(null); - }, - - /** - * Removes an AddonInstall from the list of active installs. - * - * @param install - * The AddonInstall to remove - */ - removeActiveInstall: function(aInstall) { - this.installs.delete(aInstall); - }, - - /** - * Called to get an Addon with a particular ID. - * - * @param aId - * The ID of the add-on to retrieve - * @param aCallback - * A callback to pass the Addon to - */ - getAddonByID: function(aId, aCallback) { - XPIDatabase.getVisibleAddonForID (aId, function(aAddon) { - aCallback(aAddon ? aAddon.wrapper : null); - }); - }, - - /** - * Called to get Addons of a particular type. - * - * @param aTypes - * An array of types to fetch. Can be null to get all types. - * @param aCallback - * A callback to pass an array of Addons to - */ - getAddonsByTypes: function(aTypes, aCallback) { - let typesToGet = getAllAliasesForTypes(aTypes); - - XPIDatabase.getVisibleAddons(typesToGet, function(aAddons) { - aCallback(aAddons.map(a => a.wrapper)); - }); - }, - - /** - * Obtain an Addon having the specified Sync GUID. - * - * @param aGUID - * String GUID of add-on to retrieve - * @param aCallback - * A callback to pass the Addon to. Receives null if not found. - */ - getAddonBySyncGUID: function(aGUID, aCallback) { - XPIDatabase.getAddonBySyncGUID(aGUID, function(aAddon) { - aCallback(aAddon ? aAddon.wrapper : null); - }); - }, - - /** - * Called to get Addons that have pending operations. - * - * @param aTypes - * An array of types to fetch. Can be null to get all types - * @param aCallback - * A callback to pass an array of Addons to - */ - getAddonsWithOperationsByTypes: function(aTypes, aCallback) { - let typesToGet = getAllAliasesForTypes(aTypes); - - XPIDatabase.getVisibleAddonsWithPendingOperations(typesToGet, function(aAddons) { - let results = aAddons.map(a => a.wrapper); - for (let install of XPIProvider.installs) { - if (install.state == AddonManager.STATE_INSTALLED && - !(install.addon.inDatabase)) - results.push(install.addon.wrapper); - } - aCallback(results); - }); - }, - - /** - * Called to get the current AddonInstalls, optionally limiting to a list of - * types. - * - * @param aTypes - * An array of types or null to get all types - * @param aCallback - * A callback to pass the array of AddonInstalls to - */ - getInstallsByTypes: function(aTypes, aCallback) { - let results = [...this.installs]; - if (aTypes) { - results = results.filter(install => { - return aTypes.includes(getExternalType(install.type)); - }); - } - - aCallback(results.map(install => install.wrapper)); - }, - - /** - * Synchronously map a URI to the corresponding Addon ID. - * - * Mappable URIs are limited to in-application resources belonging to the - * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc. - * but do not include URIs from meta data, such as the add-on homepage. - * - * @param aURI - * nsIURI to map or null - * @return string containing the Addon ID - * @see AddonManager.mapURIToAddonID - * @see amIAddonManager.mapURIToAddonID - */ - mapURIToAddonID: function(aURI) { - // Returns `null` instead of empty string if the URI can't be mapped. - return AddonPathService.mapURIToAddonId(aURI) || null; - }, - - /** - * Called when a new add-on has been enabled when only one add-on of that type - * can be enabled. - * - * @param aId - * The ID of the newly enabled add-on - * @param aType - * The type of the newly enabled add-on - * @param aPendingRestart - * true if the newly enabled add-on will only become enabled after a - * restart - */ - addonChanged: function(aId, aType, aPendingRestart) { - // We only care about themes in this provider - if (aType != "theme") - return; - - if (!aId) { - // Fallback to the default theme when no theme was enabled - this.enableDefaultTheme(); - return; - } - - // Look for the previously enabled theme and find the internalName of the - // currently selected theme - let previousTheme = null; - let newSkin = this.defaultSkin; - let addons = XPIDatabase.getAddonsByType("theme"); - for (let theme of addons) { - if (!theme.visible) - return; - if (theme.id == aId) - newSkin = theme.internalName; - else if (theme.userDisabled == false && !theme.pendingUninstall) - previousTheme = theme; - } - - if (aPendingRestart) { - Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true); - Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin); - } - else if (newSkin == this.currentSkin) { - try { - Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); - } - catch (e) { } - try { - Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); - } - catch (e) { } - } - else { - Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin); - this.currentSkin = newSkin; - } - this.selectedSkin = newSkin; - - // Flush the preferences to disk so they don't get out of sync with the - // database - Services.prefs.savePrefFile(null); - - // Mark the previous theme as disabled. This won't cause recursion since - // only enabled calls notifyAddonChanged. - if (previousTheme) - this.updateAddonDisabledState(previousTheme, true); - }, - - /** - * Update the appDisabled property for all add-ons. - */ - updateAddonAppDisabledStates: function() { - let addons = XPIDatabase.getAddons(); - for (let addon of addons) { - this.updateAddonDisabledState(addon); - } - }, - - /** - * Update the repositoryAddon property for all add-ons. - * - * @param aCallback - * Function to call when operation is complete. - */ - updateAddonRepositoryData: function(aCallback) { - XPIDatabase.getVisibleAddons(null, aAddons => { - let pending = aAddons.length; - logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons"); - if (pending == 0) { - aCallback(); - return; - } - - function notifyComplete() { - if (--pending == 0) - aCallback(); - } - - for (let addon of aAddons) { - AddonRepository.getCachedAddonByID(addon.id, aRepoAddon => { - if (aRepoAddon) { - logger.debug("updateAddonRepositoryData got info for " + addon.id); - addon._repositoryAddon = aRepoAddon; - addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; - this.updateAddonDisabledState(addon); - } - - notifyComplete(); - }); - } - }); - }, - - /** - * When the previously selected theme is removed this method will be called - * to enable the default theme. - */ - enableDefaultTheme: function() { - logger.debug("Activating default theme"); - let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin); - if (addon) { - if (addon.userDisabled) { - this.updateAddonDisabledState(addon, false); - } - else if (!this.extensionsActive) { - // During startup we may end up trying to enable the default theme when - // the database thinks it is already enabled (see f.e. bug 638847). In - // this case just force the theme preferences to be correct - Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, - addon.internalName); - this.currentSkin = this.selectedSkin = addon.internalName; - Preferences.reset(PREF_DSS_SKIN_TO_SELECT); - Preferences.reset(PREF_DSS_SWITCHPENDING); - } - else { - logger.warn("Attempting to activate an already active default theme"); - } - } - else { - logger.warn("Unable to activate the default theme"); - } - }, - - onDebugConnectionChange: function(aEvent, aWhat, aConnection) { - if (aWhat != "opened") - return; - - for (let [id, val] of this.activeAddons) { - aConnection.setAddonOptions( - id, { global: val.debugGlobal || val.bootstrapScope }); - } - }, - - /** - * Notified when a preference we're interested in has changed. - * - * @see nsIObserver - */ - observe: function(aSubject, aTopic, aData) { - if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) { - if (!aData || aData == XPI_PERMISSION) { - this.importPermissions(); - } - return; - } - else if (aTopic == NOTIFICATION_TOOLBOXPROCESS_LOADED) { - Services.obs.removeObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false); - this._toolboxProcessLoaded = true; - BrowserToolboxProcess.on("connectionchange", - this.onDebugConnectionChange.bind(this)); - } - - if (aTopic == "nsPref:changed") { - switch (aData) { - case PREF_EM_MIN_COMPAT_APP_VERSION: - this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION, - null); - this.updateAddonAppDisabledStates(); - break; - case PREF_EM_MIN_COMPAT_PLATFORM_VERSION: - this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, - null); - this.updateAddonAppDisabledStates(); - break; - case PREF_XPI_SIGNATURES_REQUIRED: - this.updateAddonAppDisabledStates(); - break; - } - } - }, - - /** - * Tests whether enabling an add-on will require a restart. - * - * @param aAddon - * The add-on to test - * @return true if the operation requires a restart - */ - enableRequiresRestart: function(aAddon) { - // If the platform couldn't have activated extensions then we can make - // changes without any restart. - if (!this.extensionsActive) - return false; - - // If the application is in safe mode then any change can be made without - // restarting - if (Services.appinfo.inSafeMode) - return false; - - // Anything that is active is already enabled - if (aAddon.active) - return false; - - if (aAddon.type == "theme") { - // If dynamic theme switching is enabled then switching themes does not - // require a restart - if (Preferences.get(PREF_EM_DSS_ENABLED)) - return false; - - // If the theme is already the theme in use then no restart is necessary. - // This covers the case where the default theme is in use but a - // lightweight theme is considered active. - return aAddon.internalName != this.currentSkin; - } - - return !aAddon.bootstrap; - }, - - /** - * Tests whether disabling an add-on will require a restart. - * - * @param aAddon - * The add-on to test - * @return true if the operation requires a restart - */ - disableRequiresRestart: function(aAddon) { - // If the platform couldn't have activated up extensions then we can make - // changes without any restart. - if (!this.extensionsActive) - return false; - - // If the application is in safe mode then any change can be made without - // restarting - if (Services.appinfo.inSafeMode) - return false; - - // Anything that isn't active is already disabled - if (!aAddon.active) - return false; - - if (aAddon.type == "theme") { - // If dynamic theme switching is enabled then switching themes does not - // require a restart - if (Preferences.get(PREF_EM_DSS_ENABLED)) - return false; - - // Non-default themes always require a restart to disable since it will - // be switching from one theme to another or to the default theme and a - // lightweight theme. - if (aAddon.internalName != this.defaultSkin) - return true; - - // The default theme requires a restart to disable if we are in the - // process of switching to a different theme. Note that this makes the - // disabled flag of operationsRequiringRestart incorrect for the default - // theme (it will be false most of the time). Bug 520124 would be required - // to fix it. For the UI this isn't a problem since we never try to - // disable or uninstall the default theme. - return this.selectedSkin != this.currentSkin; - } - - return !aAddon.bootstrap; - }, - - /** - * Tests whether installing an add-on will require a restart. - * - * @param aAddon - * The add-on to test - * @return true if the operation requires a restart - */ - installRequiresRestart: function(aAddon) { - // If the platform couldn't have activated up extensions then we can make - // changes without any restart. - if (!this.extensionsActive) - return false; - - // If the application is in safe mode then any change can be made without - // restarting - if (Services.appinfo.inSafeMode) - return false; - - // Add-ons that are already installed don't require a restart to install. - // This wouldn't normally be called for an already installed add-on (except - // for forming the operationsRequiringRestart flags) so is really here as - // a safety measure. - if (aAddon.inDatabase) - return false; - - // If we have an AddonInstall for this add-on then we can see if there is - // an existing installed add-on with the same ID - if ("_install" in aAddon && aAddon._install) { - // If there is an existing installed add-on and uninstalling it would - // require a restart then installing the update will also require a - // restart - let existingAddon = aAddon._install.existingAddon; - if (existingAddon && this.uninstallRequiresRestart(existingAddon)) - return true; - } - - // If the add-on is not going to be active after installation then it - // doesn't require a restart to install. - if (aAddon.disabled) - return false; - - // Themes will require a restart (even if dynamic switching is enabled due - // to some caching issues) and non-bootstrapped add-ons will require a - // restart - return aAddon.type == "theme" || !aAddon.bootstrap; - }, - - /** - * Tests whether uninstalling an add-on will require a restart. - * - * @param aAddon - * The add-on to test - * @return true if the operation requires a restart - */ - uninstallRequiresRestart: function(aAddon) { - // If the platform couldn't have activated up extensions then we can make - // changes without any restart. - if (!this.extensionsActive) - return false; - - // If the application is in safe mode then any change can be made without - // restarting - if (Services.appinfo.inSafeMode) - return false; - - // If the add-on can be disabled without a restart then it can also be - // uninstalled without a restart - return this.disableRequiresRestart(aAddon); - }, - - /** - * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason - * values as constants in the scope. This will also add information about the - * add-on to the bootstrappedAddons dictionary and notify the crash reporter - * that new add-ons have been loaded. - * - * @param aId - * The add-on's ID - * @param aFile - * The nsIFile for the add-on - * @param aVersion - * The add-on's version - * @param aType - * The type for the add-on - * @param aMultiprocessCompatible - * Boolean indicating whether the add-on is compatible with electrolysis. - * @param aRunInSafeMode - * Boolean indicating whether the add-on can run in safe mode. - * @param aDependencies - * An array of add-on IDs on which this add-on depends. - * @param hasEmbeddedWebExtension - * Boolean indicating whether the add-on has an embedded webextension. - * @return a JavaScript scope - */ - loadBootstrapScope: function(aId, aFile, aVersion, aType, - aMultiprocessCompatible, aRunInSafeMode, - aDependencies, hasEmbeddedWebExtension) { - // Mark the add-on as active for the crash reporter before loading - this.bootstrappedAddons[aId] = { - version: aVersion, - type: aType, - descriptor: aFile.persistentDescriptor, - multiprocessCompatible: aMultiprocessCompatible, - runInSafeMode: aRunInSafeMode, - dependencies: aDependencies, - hasEmbeddedWebExtension, - }; - this.persistBootstrappedAddons(); - this.addAddonsToCrashReporter(); - - this.activeAddons.set(aId, { - debugGlobal: null, - safeWrapper: null, - bootstrapScope: null, - // a Symbol passed to this add-on, which it can use to identify itself - instanceID: Symbol(aId), - }); - let activeAddon = this.activeAddons.get(aId); - - // Locales only contain chrome and can't have bootstrap scripts - if (aType == "locale") { - return; - } - - logger.debug("Loading bootstrap scope from " + aFile.path); - - let principal = Cc["@mozilla.org/systemprincipal;1"]. - createInstance(Ci.nsIPrincipal); - if (!aMultiprocessCompatible && Preferences.get(PREF_INTERPOSITION_ENABLED, false)) { - let interposition = Cc["@mozilla.org/addons/multiprocess-shims;1"]. - getService(Ci.nsIAddonInterposition); - Cu.setAddonInterposition(aId, interposition); - Cu.allowCPOWsInAddon(aId, true); - } - - if (!aFile.exists()) { - activeAddon.bootstrapScope = - new Cu.Sandbox(principal, { sandboxName: aFile.path, - wantGlobalProperties: ["indexedDB"], - addonId: aId, - metadata: { addonID: aId } }); - logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path); - return; - } - - let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec; - if (aType == "dictionary") - uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js" - else if (aType == "webextension") - uri = "resource://gre/modules/addons/WebExtensionBootstrap.js" - else if (aType == "apiextension") - uri = "resource://gre/modules/addons/APIExtensionBootstrap.js" - - activeAddon.bootstrapScope = - new Cu.Sandbox(principal, { sandboxName: uri, - wantGlobalProperties: ["indexedDB"], - addonId: aId, - metadata: { addonID: aId, URI: uri } }); - - let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. - createInstance(Ci.mozIJSSubScriptLoader); - - try { - // Copy the reason values from the global object into the bootstrap scope. - for (let name in BOOTSTRAP_REASONS) - activeAddon.bootstrapScope[name] = BOOTSTRAP_REASONS[name]; - - // Add other stuff that extensions want. - const features = [ "Worker", "ChromeWorker" ]; - - for (let feature of features) - activeAddon.bootstrapScope[feature] = gGlobalScope[feature]; - - // Define a console for the add-on - activeAddon.bootstrapScope["console"] = new ConsoleAPI( - { consoleID: "addon/" + aId }); - - // As we don't want our caller to control the JS version used for the - // bootstrap file, we run loadSubScript within the context of the - // sandbox with the latest JS version set explicitly. - activeAddon.bootstrapScope.__SCRIPT_URI_SPEC__ = uri; - Components.utils.evalInSandbox( - "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \ - .createInstance(Components.interfaces.mozIJSSubScriptLoader) \ - .loadSubScript(__SCRIPT_URI_SPEC__);", - activeAddon.bootstrapScope, "ECMAv5"); - } - catch (e) { - logger.warn("Error loading bootstrap.js for " + aId, e); - } - - // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been - // initialized as otherwise, when it will be initialized, all addons' - // globals will be added anyways - if (this._toolboxProcessLoaded) { - BrowserToolboxProcess.setAddonOptions(aId, - { global: activeAddon.bootstrapScope }); - } - }, - - /** - * Unloads a bootstrap scope by dropping all references to it and then - * updating the list of active add-ons with the crash reporter. - * - * @param aId - * The add-on's ID - */ - unloadBootstrapScope: function(aId) { - // In case the add-on was not multiprocess-compatible, deregister - // any interpositions for it. - Cu.setAddonInterposition(aId, null); - Cu.allowCPOWsInAddon(aId, false); - - this.activeAddons.delete(aId); - delete this.bootstrappedAddons[aId]; - this.persistBootstrappedAddons(); - this.addAddonsToCrashReporter(); - - // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been - // initialized as otherwise, there won't be any addon globals added to it - if (this._toolboxProcessLoaded) { - BrowserToolboxProcess.setAddonOptions(aId, { global: null }); - } - }, - - /** - * Calls a bootstrap method for an add-on. - * - * @param aAddon - * An object representing the add-on, with `id`, `type` and `version` - * @param aFile - * The nsIFile for the add-on - * @param aMethod - * The name of the bootstrap method to call - * @param aReason - * The reason flag to pass to the bootstrap's startup method - * @param aExtraParams - * An object of additional key/value pairs to pass to the method in - * the params argument - */ - callBootstrapMethod: function(aAddon, aFile, aMethod, aReason, aExtraParams) { - if (!aAddon.id || !aAddon.version || !aAddon.type) { - throw new Error("aAddon must include an id, version, and type"); - } - - // Only run in safe mode if allowed to - let runInSafeMode = "runInSafeMode" in aAddon ? aAddon.runInSafeMode : canRunInSafeMode(aAddon); - if (Services.appinfo.inSafeMode && !runInSafeMode) - return; - - let timeStart = new Date(); - if (CHROME_TYPES.has(aAddon.type) && aMethod == "startup") { - logger.debug("Registering manifest for " + aFile.path); - Components.manager.addBootstrappedManifestLocation(aFile); - } - - try { - // Load the scope if it hasn't already been loaded - let activeAddon = this.activeAddons.get(aAddon.id); - if (!activeAddon) { - this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type, - aAddon.multiprocessCompatible || false, - runInSafeMode, aAddon.dependencies, - aAddon.hasEmbeddedWebExtension || false); - activeAddon = this.activeAddons.get(aAddon.id); - } - - if (aMethod == "startup" || aMethod == "shutdown") { - if (!aExtraParams) { - aExtraParams = {}; - } - aExtraParams["instanceID"] = this.activeAddons.get(aAddon.id).instanceID; - } - - // Nothing to call for locales - if (aAddon.type == "locale") - return; - - let method = undefined; - try { - method = Components.utils.evalInSandbox(`${aMethod};`, - activeAddon.bootstrapScope, "ECMAv5"); - } - catch (e) { - // An exception will be caught if the expected method is not defined. - // That will be logged below. - } - - if (!method) { - logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod); - return; - } - - // Extensions are automatically deinitialized in the correct order at shutdown. - if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { - activeAddon.disable = true; - for (let addon of this.getDependentAddons(aAddon)) { - if (addon.active) - this.updateAddonDisabledState(addon); - } - } - - let params = { - id: aAddon.id, - version: aAddon.version, - installPath: aFile.clone(), - resourceURI: getURIForResourceInFile(aFile, "") - }; - - if (aExtraParams) { - for (let key in aExtraParams) { - params[key] = aExtraParams[key]; - } - } - - if (aAddon.hasEmbeddedWebExtension) { - if (aMethod == "startup") { - const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor(params); - params.webExtension = { - startup: () => webExtension.startup(), - }; - } else if (aMethod == "shutdown") { - LegacyExtensionsUtils.getEmbeddedExtensionFor(params).shutdown(); - } - } - - logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " + - aAddon.version); - try { - method(params, aReason); - } - catch (e) { - logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e); - } - } - finally { - // Extensions are automatically initialized in the correct order at startup. - if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) { - for (let addon of this.getDependentAddons(aAddon)) - this.updateAddonDisabledState(addon); - } - - if (CHROME_TYPES.has(aAddon.type) && aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { - logger.debug("Removing manifest for " + aFile.path); - Components.manager.removeBootstrappedManifestLocation(aFile); - - let manifest = getURIForResourceInFile(aFile, "chrome.manifest"); - for (let line of ChromeManifestParser.parseSync(manifest)) { - if (line.type == "resource") { - ResProtocolHandler.setSubstitution(line.args[0], null); - } - } - } - this.setTelemetry(aAddon.id, aMethod + "_MS", new Date() - timeStart); - } - }, - - /** - * Updates the disabled state for an add-on. Its appDisabled property will be - * calculated and if the add-on is changed the database will be saved and - * appropriate notifications will be sent out to the registered AddonListeners. - * - * @param aAddon - * The DBAddonInternal to update - * @param aUserDisabled - * Value for the userDisabled property. If undefined the value will - * not change - * @param aSoftDisabled - * Value for the softDisabled property. If undefined the value will - * not change. If true this will force userDisabled to be true - * @return a tri-state indicating the action taken for the add-on: - * - undefined: The add-on did not change state - * - true: The add-on because disabled - * - false: The add-on became enabled - * @throws if addon is not a DBAddonInternal - */ - updateAddonDisabledState: function(aAddon, aUserDisabled, aSoftDisabled) { - if (!(aAddon.inDatabase)) - throw new Error("Can only update addon states for installed addons."); - if (aUserDisabled !== undefined && aSoftDisabled !== undefined) { - throw new Error("Cannot change userDisabled and softDisabled at the " + - "same time"); - } - - if (aUserDisabled === undefined) { - aUserDisabled = aAddon.userDisabled; - } - else if (!aUserDisabled) { - // If enabling the add-on then remove softDisabled - aSoftDisabled = false; - } - - // If not changing softDisabled or the add-on is already userDisabled then - // use the existing value for softDisabled - if (aSoftDisabled === undefined || aUserDisabled) - aSoftDisabled = aAddon.softDisabled; - - let appDisabled = !isUsableAddon(aAddon); - // No change means nothing to do here - if (aAddon.userDisabled == aUserDisabled && - aAddon.appDisabled == appDisabled && - aAddon.softDisabled == aSoftDisabled) - return undefined; - - let wasDisabled = aAddon.disabled; - let isDisabled = aUserDisabled || aSoftDisabled || appDisabled; - - // If appDisabled changes but addon.disabled doesn't, - // no onDisabling/onEnabling is sent - so send a onPropertyChanged. - let appDisabledChanged = aAddon.appDisabled != appDisabled; - - // Update the properties in the database. - XPIDatabase.setAddonProperties(aAddon, { - userDisabled: aUserDisabled, - appDisabled: appDisabled, - softDisabled: aSoftDisabled - }); - - let wrapper = aAddon.wrapper; - - if (appDisabledChanged) { - AddonManagerPrivate.callAddonListeners("onPropertyChanged", - wrapper, - ["appDisabled"]); - } - - // If the add-on is not visible or the add-on is not changing state then - // there is no need to do anything else - if (!aAddon.visible || (wasDisabled == isDisabled)) - return undefined; - - // Flag that active states in the database need to be updated on shutdown - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); - - // Have we just gone back to the current state? - if (isDisabled != aAddon.active) { - AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); - } - else { - if (isDisabled) { - var needsRestart = this.disableRequiresRestart(aAddon); - AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, - needsRestart); - } - else { - needsRestart = this.enableRequiresRestart(aAddon); - AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, - needsRestart); - } - - if (!needsRestart) { - XPIDatabase.updateAddonActive(aAddon, !isDisabled); - - if (isDisabled) { - if (aAddon.bootstrap) { - this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", - BOOTSTRAP_REASONS.ADDON_DISABLE); - this.unloadBootstrapScope(aAddon.id); - } - AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); - } - else { - if (aAddon.bootstrap) { - this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup", - BOOTSTRAP_REASONS.ADDON_ENABLE); - } - AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); - } - } - else if (aAddon.bootstrap) { - // Something blocked the restartless add-on from enabling or disabling - // make sure it happens on the next startup - if (isDisabled) { - this.bootstrappedAddons[aAddon.id].disable = true; - } - else { - this.bootstrappedAddons[aAddon.id] = { - version: aAddon.version, - type: aAddon.type, - descriptor: aAddon._sourceBundle.persistentDescriptor, - multiprocessCompatible: aAddon.multiprocessCompatible, - runInSafeMode: canRunInSafeMode(aAddon), - dependencies: aAddon.dependencies, - hasEmbeddedWebExtension: aAddon.hasEmbeddedWebExtension, - }; - this.persistBootstrappedAddons(); - } - } - } - - // Sync with XPIStates. - let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id); - if (xpiState) { - xpiState.syncWithDB(aAddon); - XPIStates.save(); - } else { - // There should always be an xpiState - logger.warn("No XPIState for ${id} in ${location}", aAddon); - } - - // Notify any other providers that a new theme has been enabled - if (aAddon.type == "theme" && !isDisabled) - AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart); - - return isDisabled; - }, - - /** - * Uninstalls an add-on, immediately if possible or marks it as pending - * uninstall if not. - * - * @param aAddon - * The DBAddonInternal to uninstall - * @param aForcePending - * Force this addon into the pending uninstall state, even if - * it isn't marked as requiring a restart (used e.g. while the - * add-on manager is open and offering an "undo" button) - * @throws if the addon cannot be uninstalled because it is in an install - * location that does not allow it - */ - uninstallAddon: function(aAddon, aForcePending) { - if (!(aAddon.inDatabase)) - throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed"); - - if (aAddon._installLocation.locked) - throw new Error("Cannot uninstall addon " + aAddon.id - + " from locked install location " + aAddon._installLocation.name); - - // Inactive add-ons don't require a restart to uninstall - let requiresRestart = this.uninstallRequiresRestart(aAddon); - - // if makePending is true, we don't actually apply the uninstall, - // we just mark the addon as having a pending uninstall - let makePending = aForcePending || requiresRestart; - - if (makePending && aAddon.pendingUninstall) - throw new Error("Add-on is already marked to be uninstalled"); - - aAddon._hasResourceCache.clear(); - - if (aAddon._updateCheck) { - logger.debug("Cancel in-progress update check for " + aAddon.id); - aAddon._updateCheck.cancel(); - } - - let wasPending = aAddon.pendingUninstall; - - if (makePending) { - // We create an empty directory in the staging directory to indicate - // that an uninstall is necessary on next startup. Temporary add-ons are - // automatically uninstalled on shutdown anyway so there is no need to - // do this for them. - if (aAddon._installLocation.name != KEY_APP_TEMPORARY) { - let stage = aAddon._installLocation.getStagingDir(); - stage.append(aAddon.id); - if (!stage.exists()) - stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - } - - XPIDatabase.setAddonProperties(aAddon, { - pendingUninstall: true - }); - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); - let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id); - if (xpiState) { - xpiState.enabled = false; - XPIStates.save(); - } else { - logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon); - } - } - - // If the add-on is not visible then there is no need to notify listeners. - if (!aAddon.visible) - return; - - let wrapper = aAddon.wrapper; - - // If the add-on wasn't already pending uninstall then notify listeners. - if (!wasPending) { - // Passing makePending as the requiresRestart parameter is a little - // strange as in some cases this operation can complete without a restart - // so really this is now saying that the uninstall isn't going to happen - // immediately but will happen later. - AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, - makePending); - } - - // Reveal the highest priority add-on with the same ID - function revealAddon(aAddon) { - XPIDatabase.makeAddonVisible(aAddon); - - let wrappedAddon = aAddon.wrapper; - AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false); - - if (!aAddon.disabled && !XPIProvider.enableRequiresRestart(aAddon)) { - XPIDatabase.updateAddonActive(aAddon, true); - } - - if (aAddon.bootstrap) { - XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, - "install", BOOTSTRAP_REASONS.ADDON_INSTALL); - - if (aAddon.active) { - XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, - "startup", BOOTSTRAP_REASONS.ADDON_INSTALL); - } - else { - XPIProvider.unloadBootstrapScope(aAddon.id); - } - } - - // We always send onInstalled even if a restart is required to enable - // the revealed add-on - AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon); - } - - function findAddonAndReveal(aId) { - let [locationName, ] = XPIStates.findAddon(aId); - if (locationName) { - XPIDatabase.getAddonInLocation(aId, locationName, revealAddon); - } - } - - if (!makePending) { - if (aAddon.bootstrap) { - if (aAddon.active) { - this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", - BOOTSTRAP_REASONS.ADDON_UNINSTALL); - } - - this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall", - BOOTSTRAP_REASONS.ADDON_UNINSTALL); - this.unloadBootstrapScope(aAddon.id); - flushChromeCaches(); - } - aAddon._installLocation.uninstallAddon(aAddon.id); - XPIDatabase.removeAddonMetadata(aAddon); - XPIStates.removeAddon(aAddon.location, aAddon.id); - AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); - - findAddonAndReveal(aAddon.id); - } - else if (aAddon.bootstrap && aAddon.active && !this.disableRequiresRestart(aAddon)) { - this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", - BOOTSTRAP_REASONS.ADDON_UNINSTALL); - this.unloadBootstrapScope(aAddon.id); - XPIDatabase.updateAddonActive(aAddon, false); - } - - // Notify any other providers that a new theme has been enabled - if (aAddon.type == "theme" && aAddon.active) - AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart); - }, - - /** - * Cancels the pending uninstall of an add-on. - * - * @param aAddon - * The DBAddonInternal to cancel uninstall for - */ - cancelUninstallAddon: function(aAddon) { - if (!(aAddon.inDatabase)) - throw new Error("Can only cancel uninstall for installed addons."); - if (!aAddon.pendingUninstall) - throw new Error("Add-on is not marked to be uninstalled"); - - if (aAddon._installLocation.name != KEY_APP_TEMPORARY) - aAddon._installLocation.cleanStagingDir([aAddon.id]); - - XPIDatabase.setAddonProperties(aAddon, { - pendingUninstall: false - }); - - if (!aAddon.visible) - return; - - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); - - // TODO hide hidden add-ons (bug 557710) - let wrapper = aAddon.wrapper; - AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); - - if (aAddon.bootstrap && !aAddon.disabled && !this.enableRequiresRestart(aAddon)) { - this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup", - BOOTSTRAP_REASONS.ADDON_INSTALL); - XPIDatabase.updateAddonActive(aAddon, true); - } - - // Notify any other providers that this theme is now enabled again. - if (aAddon.type == "theme" && aAddon.active) - AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false); - } -}; - -function getHashStringForCrypto(aCrypto) { - // return the two-digit hexadecimal code for a byte - let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2); - - // convert the binary hash data to a hex string. - let binary = aCrypto.finish(false); - let hash = Array.from(binary, c => toHexString(c.charCodeAt(0))) - return hash.join("").toLowerCase(); -} - -/** - * Base class for objects that manage the installation of an addon. - * This class isn't instantiated directly, see the derived classes below. - */ -class AddonInstall { - /** - * Instantiates an AddonInstall. - * - * @param aInstallLocation - * The install location the add-on will be installed into - * @param aUrl - * The nsIURL to get the add-on from. If this is an nsIFileURL then - * the add-on will not need to be downloaded - * @param aHash - * An optional hash for the add-on - * @param aExistingAddon - * The add-on this install will update if known - */ - constructor(aInstallLocation, aUrl, aHash, aExistingAddon) { - this.wrapper = new AddonInstallWrapper(this); - this.installLocation = aInstallLocation; - this.sourceURI = aUrl; - - if (aHash) { - let hashSplit = aHash.toLowerCase().split(":"); - this.originalHash = { - algorithm: hashSplit[0], - data: hashSplit[1] - }; - } - this.hash = this.originalHash; - this.existingAddon = aExistingAddon; - this.releaseNotesURI = null; - - this.listeners = []; - this.icons = {}; - this.error = 0; - - this.progress = 0; - this.maxProgress = -1; - - // Giving each instance of AddonInstall a reference to the logger. - this.logger = logger; - - this.name = null; - this.type = null; - this.version = null; - - this.file = null; - this.ownsTempFile = null; - this.certificate = null; - this.certName = null; - - this.linkedInstalls = null; - this.addon = null; - this.state = null; - - XPIProvider.installs.add(this); - } - - /** - * Starts installation of this add-on from whatever state it is currently at - * if possible. - * - * Note this method is overridden to handle additional state in - * the subclassses below. - * - * @throws if installation cannot proceed from the current state - */ - install() { - switch (this.state) { - case AddonManager.STATE_DOWNLOADED: - this.startInstall(); - break; - case AddonManager.STATE_POSTPONED: - logger.debug(`Postponing install of ${this.addon.id}`); - break; - case AddonManager.STATE_DOWNLOADING: - case AddonManager.STATE_CHECKING: - case AddonManager.STATE_INSTALLING: - // Installation is already running - return; - default: - throw new Error("Cannot start installing from this state"); - } - } - - /** - * Cancels installation of this add-on. - * - * Note this method is overridden to handle additional state in - * the subclass DownloadAddonInstall. - * - * @throws if installation cannot be cancelled from the current state - */ - cancel() { - switch (this.state) { - case AddonManager.STATE_AVAILABLE: - case AddonManager.STATE_DOWNLOADED: - logger.debug("Cancelling download of " + this.sourceURI.spec); - this.state = AddonManager.STATE_CANCELLED; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onDownloadCancelled", - this.listeners, this.wrapper); - this.removeTemporaryFile(); - break; - case AddonManager.STATE_INSTALLED: - logger.debug("Cancelling install of " + this.addon.id); - let xpi = this.installLocation.getStagingDir(); - xpi.append(this.addon.id + ".xpi"); - flushJarCache(xpi); - this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi", - this.addon.id + ".json"]); - this.state = AddonManager.STATE_CANCELLED; - XPIProvider.removeActiveInstall(this); - - if (this.existingAddon) { - delete this.existingAddon.pendingUpgrade; - this.existingAddon.pendingUpgrade = null; - } - - AddonManagerPrivate.callAddonListeners("onOperationCancelled", this.addon.wrapper); - - AddonManagerPrivate.callInstallListeners("onInstallCancelled", - this.listeners, this.wrapper); - break; - case AddonManager.STATE_POSTPONED: - logger.debug(`Cancelling postponed install of ${this.addon.id}`); - this.state = AddonManager.STATE_CANCELLED; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onInstallCancelled", - this.listeners, this.wrapper); - this.removeTemporaryFile(); - - let stagingDir = this.installLocation.getStagingDir(); - let stagedAddon = stagingDir.clone(); - - this.unstageInstall(stagedAddon); - default: - throw new Error("Cannot cancel install of " + this.sourceURI.spec + - " from this state (" + this.state + ")"); - } - } - - /** - * Adds an InstallListener for this instance if the listener is not already - * registered. - * - * @param aListener - * The InstallListener to add - */ - addListener(aListener) { - if (!this.listeners.some(function(i) { return i == aListener; })) - this.listeners.push(aListener); - } - - /** - * Removes an InstallListener for this instance if it is registered. - * - * @param aListener - * The InstallListener to remove - */ - removeListener(aListener) { - this.listeners = this.listeners.filter(function(i) { - return i != aListener; - }); - } - - /** - * Removes the temporary file owned by this AddonInstall if there is one. - */ - removeTemporaryFile() { - // Only proceed if this AddonInstall owns its XPI file - if (!this.ownsTempFile) { - this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file"); - return; - } - - try { - this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " + - this.file.path); - this.file.remove(true); - this.ownsTempFile = false; - } - catch (e) { - this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " + - this.sourceURI.spec, - e); - } - } - - /** - * Updates the sourceURI and releaseNotesURI values on the Addon being - * installed by this AddonInstall instance. - */ - updateAddonURIs() { - this.addon.sourceURI = this.sourceURI.spec; - if (this.releaseNotesURI) - this.addon.releaseNotesURI = this.releaseNotesURI.spec; - } - - /** - * Fills out linkedInstalls with AddonInstall instances for the other files - * in a multi-package XPI. - * - * @param aFiles - * An array of { entryName, file } for each remaining file from the - * multi-package XPI. - */ - _createLinkedInstalls(aFiles) { - return Task.spawn((function*() { - if (aFiles.length == 0) - return; - - // Create new AddonInstall instances for every remaining file - if (!this.linkedInstalls) - this.linkedInstalls = []; - - for (let { entryName, file } of aFiles) { - logger.debug("Creating linked install from " + entryName); - let install = yield createLocalInstall(file); - - // Make the new install own its temporary file - install.ownsTempFile = true; - - this.linkedInstalls.push(install); - - // If one of the internal XPIs was multipackage then move its linked - // installs to the outer install - if (install.linkedInstalls) { - this.linkedInstalls.push(...install.linkedInstalls); - install.linkedInstalls = null; - } - - install.sourceURI = this.sourceURI; - install.releaseNotesURI = this.releaseNotesURI; - if (install.state != AddonManager.STATE_DOWNLOAD_FAILED) - install.updateAddonURIs(); - } - }).bind(this)); - } - - /** - * Loads add-on manifests from a multi-package XPI file. Each of the - * XPI and JAR files contained in the XPI will be extracted. Any that - * do not contain valid add-ons will be ignored. The first valid add-on will - * be installed by this AddonInstall instance, the rest will have new - * AddonInstall instances created for them. - * - * @param aZipReader - * An open nsIZipReader for the multi-package XPI's files. This will - * be closed before this method returns. - */ - _loadMultipackageManifests(aZipReader) { - return Task.spawn((function*() { - let files = []; - let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])"); - while (entries.hasMore()) { - let entryName = entries.getNext(); - let file = getTemporaryFile(); - try { - aZipReader.extract(entryName, file); - files.push({ entryName, file }); - } - catch (e) { - logger.warn("Failed to extract " + entryName + " from multi-package " + - "XPI", e); - file.remove(false); - } - } - - aZipReader.close(); - - if (files.length == 0) { - return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, - "Multi-package XPI does not contain any packages to install"]); - } - - let addon = null; - - // Find the first file that is a valid install and use it for - // the add-on that this AddonInstall instance will install. - for (let { entryName, file } of files) { - this.removeTemporaryFile(); - try { - yield this.loadManifest(file); - logger.debug("Base multi-package XPI install came from " + entryName); - this.file = file; - this.ownsTempFile = true; - - yield this._createLinkedInstalls(files.filter(f => f.file != file)); - return undefined; - } - catch (e) { - // _createLinkedInstalls will log errors when it tries to process this - // file - } - } - - // No valid add-on was found, delete all the temporary files - for (let { file } of files) { - try { - file.remove(true); - } catch (e) { - this.logger.warn("Could not remove temp file " + file.path); - } - } - - return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, - "Multi-package XPI does not contain any valid packages to install"]); - }).bind(this)); - } - - /** - * Called after the add-on is a local file and the signature and install - * manifest can be read. - * - * @param aCallback - * A function to call when the manifest has been loaded - * @throws if the add-on does not contain a valid install manifest or the - * XPI is incorrectly signed - */ - loadManifest(file) { - return Task.spawn((function*() { - let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"]. - createInstance(Ci.nsIZipReader); - try { - zipreader.open(file); - } - catch (e) { - zipreader.close(); - return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]); - } - - try { - // loadManifestFromZipReader performs the certificate verification for us - this.addon = yield loadManifestFromZipReader(zipreader, this.installLocation); - } - catch (e) { - zipreader.close(); - return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]); - } - - // A multi-package XPI is a container, the add-ons it holds each - // have their own id. Everything else had better have an id here. - if (!this.addon.id && this.addon.type != "multipackage") { - let err = new Error(`Cannot find id for addon ${file.path}`); - return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]); - } - - if (this.existingAddon) { - // Check various conditions related to upgrades - if (this.addon.id != this.existingAddon.id) { - zipreader.close(); - return Promise.reject([AddonManager.ERROR_INCORRECT_ID, - `Refusing to upgrade addon ${this.existingAddon.id} to different ID {this.addon.id}`]); - } - - if (this.addon.type == "multipackage") { - zipreader.close(); - return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, - `Refusing to upgrade addon ${this.existingAddon.id} to a multi-package xpi`]); - } - - if (this.existingAddon.type == "webextension" && this.addon.type != "webextension") { - zipreader.close(); - return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, - "WebExtensions may not be upated to other extension types"]); - } - } - - if (mustSign(this.addon.type)) { - if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) { - // This add-on isn't properly signed by a signature that chains to the - // trusted root. - let state = this.addon.signedState; - this.addon = null; - zipreader.close(); - - if (state == AddonManager.SIGNEDSTATE_MISSING) - return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED, - "signature is required but missing"]) - - return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, - "signature verification failed"]) - } - } - else if (this.addon.signedState == AddonManager.SIGNEDSTATE_UNKNOWN || - this.addon.signedState == AddonManager.SIGNEDSTATE_NOT_REQUIRED) { - // Check object signing certificate, if any - let x509 = zipreader.getSigningCert(null); - if (x509) { - logger.debug("Verifying XPI signature"); - if (verifyZipSigning(zipreader, x509)) { - this.certificate = x509; - if (this.certificate.commonName.length > 0) { - this.certName = this.certificate.commonName; - } else { - this.certName = this.certificate.organization; - } - } else { - zipreader.close(); - return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, - "XPI is incorrectly signed"]); - } - } - } - - if (this.addon.type == "multipackage") - return this._loadMultipackageManifests(zipreader); - - zipreader.close(); - - this.updateAddonURIs(); - - this.addon._install = this; - this.name = this.addon.selectedLocale.name; - this.type = this.addon.type; - this.version = this.addon.version; - - // Setting the iconURL to something inside the XPI locks the XPI and - // makes it impossible to delete on Windows. - - // Try to load from the existing cache first - let repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve)); - - // It wasn't there so try to re-download it - if (!repoAddon) { - yield new Promise(resolve => AddonRepository.cacheAddons([this.addon.id], resolve)); - repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve)); - } - - this.addon._repositoryAddon = repoAddon; - this.name = this.name || this.addon._repositoryAddon.name; - this.addon.compatibilityOverrides = repoAddon ? - repoAddon.compatibilityOverrides : - null; - this.addon.appDisabled = !isUsableAddon(this.addon); - return undefined; - }).bind(this)); - } - - // TODO This relies on the assumption that we are always installing into the - // highest priority install location so the resulting add-on will be visible - // overriding any existing copy in another install location (bug 557710). - /** - * Installs the add-on into the install location. - */ - startInstall() { - this.state = AddonManager.STATE_INSTALLING; - if (!AddonManagerPrivate.callInstallListeners("onInstallStarted", - this.listeners, this.wrapper)) { - this.state = AddonManager.STATE_DOWNLOADED; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onInstallCancelled", - this.listeners, this.wrapper) - return; - } - - // Find and cancel any pending installs for the same add-on in the same - // install location - for (let aInstall of XPIProvider.installs) { - if (aInstall.state == AddonManager.STATE_INSTALLED && - aInstall.installLocation == this.installLocation && - aInstall.addon.id == this.addon.id) { - logger.debug("Cancelling previous pending install of " + aInstall.addon.id); - aInstall.cancel(); - } - } - - let isUpgrade = this.existingAddon && - this.existingAddon._installLocation == this.installLocation; - let requiresRestart = XPIProvider.installRequiresRestart(this.addon); - - logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec); - AddonManagerPrivate.callAddonListeners("onInstalling", - this.addon.wrapper, - requiresRestart); - - let stagedAddon = this.installLocation.getStagingDir(); - - Task.spawn((function*() { - let installedUnpacked = 0; - - yield this.installLocation.requestStagingDir(); - - // remove any previously staged files - yield this.unstageInstall(stagedAddon); - - stagedAddon.append(this.addon.id); - stagedAddon.leafName = this.addon.id + ".xpi"; - - installedUnpacked = yield this.stageInstall(requiresRestart, stagedAddon, isUpgrade); - - if (requiresRestart) { - this.state = AddonManager.STATE_INSTALLED; - AddonManagerPrivate.callInstallListeners("onInstallEnded", - this.listeners, this.wrapper, - this.addon.wrapper); - } - else { - // The install is completed so it should be removed from the active list - XPIProvider.removeActiveInstall(this); - - // Deactivate and remove the old add-on as necessary - let reason = BOOTSTRAP_REASONS.ADDON_INSTALL; - if (this.existingAddon) { - if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0) - reason = BOOTSTRAP_REASONS.ADDON_UPGRADE; - else - reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE; - - if (this.existingAddon.bootstrap) { - let file = this.existingAddon._sourceBundle; - if (this.existingAddon.active) { - XPIProvider.callBootstrapMethod(this.existingAddon, file, - "shutdown", reason, - { newVersion: this.addon.version }); - } - - XPIProvider.callBootstrapMethod(this.existingAddon, file, - "uninstall", reason, - { newVersion: this.addon.version }); - XPIProvider.unloadBootstrapScope(this.existingAddon.id); - flushChromeCaches(); - } - - if (!isUpgrade && this.existingAddon.active) { - XPIDatabase.updateAddonActive(this.existingAddon, false); - } - } - - // Install the new add-on into its final location - let existingAddonID = this.existingAddon ? this.existingAddon.id : null; - let file = this.installLocation.installAddon({ - id: this.addon.id, - source: stagedAddon, - existingAddonID - }); - - // Update the metadata in the database - this.addon._sourceBundle = file; - this.addon.visible = true; - - if (isUpgrade) { - this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon, - file.persistentDescriptor); - let state = XPIStates.getAddon(this.installLocation.name, this.addon.id); - if (state) { - state.syncWithDB(this.addon, true); - } else { - logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon); - } - } - else { - this.addon.active = (this.addon.visible && !this.addon.disabled); - this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor); - XPIStates.addAddon(this.addon); - this.addon.installDate = this.addon.updateDate; - XPIDatabase.saveChanges(); - } - XPIStates.save(); - - let extraParams = {}; - if (this.existingAddon) { - extraParams.oldVersion = this.existingAddon.version; - } - - if (this.addon.bootstrap) { - XPIProvider.callBootstrapMethod(this.addon, file, "install", - reason, extraParams); - } - - AddonManagerPrivate.callAddonListeners("onInstalled", - this.addon.wrapper); - - logger.debug("Install of " + this.sourceURI.spec + " completed."); - this.state = AddonManager.STATE_INSTALLED; - AddonManagerPrivate.callInstallListeners("onInstallEnded", - this.listeners, this.wrapper, - this.addon.wrapper); - - if (this.addon.bootstrap) { - if (this.addon.active) { - XPIProvider.callBootstrapMethod(this.addon, file, "startup", - reason, extraParams); - } - else { - // XXX this makes it dangerous to do some things in onInstallEnded - // listeners because important cleanup hasn't been done yet - XPIProvider.unloadBootstrapScope(this.addon.id); - } - } - XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked); - recordAddonTelemetry(this.addon); - } - }).bind(this)).then(null, (e) => { - logger.warn(`Failed to install ${this.file.path} from ${this.sourceURI.spec} to ${stagedAddon.path}`, e); - - if (stagedAddon.exists()) - recursiveRemove(stagedAddon); - this.state = AddonManager.STATE_INSTALL_FAILED; - this.error = AddonManager.ERROR_FILE_ACCESS; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callAddonListeners("onOperationCancelled", - this.addon.wrapper); - AddonManagerPrivate.callInstallListeners("onInstallFailed", - this.listeners, - this.wrapper); - }).then(() => { - this.removeTemporaryFile(); - return this.installLocation.releaseStagingDir(); - }); - } - - /** - * Stages an upgrade for next application restart. - */ - stageInstall(restartRequired, stagedAddon, isUpgrade) { - return Task.spawn((function*() { - let stagedJSON = stagedAddon.clone(); - stagedJSON.leafName = this.addon.id + ".json"; - - let installedUnpacked = 0; - - // First stage the file regardless of whether restarting is necessary - if (this.addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) { - logger.debug("Addon " + this.addon.id + " will be installed as " + - "an unpacked directory"); - stagedAddon.leafName = this.addon.id; - yield OS.File.makeDir(stagedAddon.path); - yield ZipUtils.extractFilesAsync(this.file, stagedAddon); - installedUnpacked = 1; - } - else { - logger.debug(`Addon ${this.addon.id} will be installed as a packed xpi`); - stagedAddon.leafName = this.addon.id + ".xpi"; - - yield OS.File.copy(this.file.path, stagedAddon.path); - } - - if (restartRequired) { - // Point the add-on to its extracted files as the xpi may get deleted - this.addon._sourceBundle = stagedAddon; - - // Cache the AddonInternal as it may have updated compatibility info - writeStringToFile(stagedJSON, JSON.stringify(this.addon)); - - logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart."); - if (isUpgrade) { - delete this.existingAddon.pendingUpgrade; - this.existingAddon.pendingUpgrade = this.addon; - } - } - - return installedUnpacked; - }).bind(this)); - } - - /** - * Removes any previously staged upgrade. - */ - unstageInstall(stagedAddon) { - return Task.spawn((function*() { - let stagedJSON = stagedAddon.clone(); - let removedAddon = stagedAddon.clone(); - - stagedJSON.append(this.addon.id + ".json"); - - if (stagedJSON.exists()) { - stagedJSON.remove(true); - } - - removedAddon.append(this.addon.id); - yield removeAsync(removedAddon); - removedAddon.leafName = this.addon.id + ".xpi"; - yield removeAsync(removedAddon); - }).bind(this)); - } - - /** - * Postone a pending update, until restart or until the add-on resumes. - * - * @param {Function} resumeFunction - a function for the add-on to run - * when resuming. - */ - postpone(resumeFunction) { - return Task.spawn((function*() { - this.state = AddonManager.STATE_POSTPONED; - - let stagingDir = this.installLocation.getStagingDir(); - let stagedAddon = stagingDir.clone(); - - yield this.installLocation.requestStagingDir(); - yield this.unstageInstall(stagedAddon); - - stagedAddon.append(this.addon.id); - stagedAddon.leafName = this.addon.id + ".xpi"; - - yield this.stageInstall(true, stagedAddon, true); - - AddonManagerPrivate.callInstallListeners("onInstallPostponed", - this.listeners, this.wrapper) - - // upgrade has been staged for restart, provide a way for it to call the - // resume function. - if (resumeFunction) { - let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id); - if (callback) { - callback({ - version: this.version, - install: () => { - switch (this.state) { - case AddonManager.STATE_POSTPONED: - resumeFunction(); - break; - default: - logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`); - break; - } - }, - }); - } - } - this.installLocation.releaseStagingDir(); - }).bind(this)); - } -} - -class LocalAddonInstall extends AddonInstall { - /** - * Initialises this install to be an install from a local file. - * - * @returns Promise - * A Promise that resolves when the object is ready to use. - */ - init() { - return Task.spawn((function*() { - this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file; - - if (!this.file.exists()) { - logger.warn("XPI file " + this.file.path + " does not exist"); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = AddonManager.ERROR_NETWORK_FAILURE; - XPIProvider.removeActiveInstall(this); - return; - } - - this.state = AddonManager.STATE_DOWNLOADED; - this.progress = this.file.fileSize; - this.maxProgress = this.file.fileSize; - - if (this.hash) { - let crypto = Cc["@mozilla.org/security/hash;1"]. - createInstance(Ci.nsICryptoHash); - try { - crypto.initWithString(this.hash.algorithm); - } - catch (e) { - logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = AddonManager.ERROR_INCORRECT_HASH; - XPIProvider.removeActiveInstall(this); - return; - } - - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - fis.init(this.file, -1, -1, false); - crypto.updateFromStream(fis, this.file.fileSize); - let calculatedHash = getHashStringForCrypto(crypto); - if (calculatedHash != this.hash.data) { - logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" + - this.hash.data + ")"); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = AddonManager.ERROR_INCORRECT_HASH; - XPIProvider.removeActiveInstall(this); - return; - } - } - - try { - yield this.loadManifest(this.file); - } catch ([error, message]) { - logger.warn("Invalid XPI", message); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = error; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onNewInstall", - this.listeners, - this.wrapper); - return; - } - - let addon = yield new Promise(resolve => { - XPIDatabase.getVisibleAddonForID(this.addon.id, resolve); - }); - - this.existingAddon = addon; - if (addon) - applyBlocklistChanges(addon, this.addon); - this.addon.updateDate = Date.now(); - this.addon.installDate = addon ? addon.installDate : this.addon.updateDate; - - if (!this.addon.isCompatible) { - this.state = AddonManager.STATE_CHECKING; - - yield new Promise(resolve => { - new UpdateChecker(this.addon, { - onUpdateFinished: aAddon => { - this.state = AddonManager.STATE_DOWNLOADED; - AddonManagerPrivate.callInstallListeners("onNewInstall", - this.listeners, - this.wrapper); - resolve(); - } - }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); - }); - } - else { - AddonManagerPrivate.callInstallListeners("onNewInstall", - this.listeners, - this.wrapper); - - } - }).bind(this)); - } - - install() { - if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) { - // For a local install, this state means that verification of the - // file failed (e.g., the hash or signature or manifest contents - // were invalid). It doesn't make sense to retry anything in this - // case but we have callers who don't know if their AddonInstall - // object is a local file or a download so accomodate them here. - AddonManagerPrivate.callInstallListeners("onDownloadFailed", - this.listeners, this.wrapper); - return; - } - super.install(); - } -} - -class DownloadAddonInstall extends AddonInstall { - /** - * Instantiates a DownloadAddonInstall - * - * @param installLocation - * The InstallLocation the add-on will be installed into - * @param url - * The nsIURL to get the add-on from - * @param name - * An optional name for the add-on - * @param hash - * An optional hash for the add-on - * @param existingAddon - * The add-on this install will update if known - * @param browser - * The browser performing the install, used to display - * authentication prompts. - * @param type - * An optional type for the add-on - * @param icons - * Optional icons for the add-on - * @param version - * An optional version for the add-on - */ - constructor(installLocation, url, hash, existingAddon, browser, - name, type, icons, version) { - super(installLocation, url, hash, existingAddon); - - this.browser = browser; - - this.state = AddonManager.STATE_AVAILABLE; - this.name = name; - this.type = type; - this.version = version; - this.icons = icons; - - this.stream = null; - this.crypto = null; - this.badCertHandler = null; - this.restartDownload = false; - - AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners, - this.wrapper); - } - - install() { - switch (this.state) { - case AddonManager.STATE_AVAILABLE: - this.startDownload(); - break; - case AddonManager.STATE_DOWNLOAD_FAILED: - case AddonManager.STATE_INSTALL_FAILED: - case AddonManager.STATE_CANCELLED: - this.removeTemporaryFile(); - this.state = AddonManager.STATE_AVAILABLE; - this.error = 0; - this.progress = 0; - this.maxProgress = -1; - this.hash = this.originalHash; - this.startDownload(); - break; - default: - super.install(); - } - } - - cancel() { - if (this.state == AddonManager.STATE_DOWNLOADING) { - if (this.channel) { - logger.debug("Cancelling download of " + this.sourceURI.spec); - this.channel.cancel(Cr.NS_BINDING_ABORTED); - } - } else { - super.cancel(); - } - } - - observe(aSubject, aTopic, aData) { - // Network is going offline - this.cancel(); - } - - /** - * Starts downloading the add-on's XPI file. - */ - startDownload() { - this.state = AddonManager.STATE_DOWNLOADING; - if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted", - this.listeners, this.wrapper)) { - logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec); - this.state = AddonManager.STATE_CANCELLED; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onDownloadCancelled", - this.listeners, this.wrapper) - return; - } - - // If a listener changed our state then do not proceed with the download - if (this.state != AddonManager.STATE_DOWNLOADING) - return; - - if (this.channel) { - // A previous download attempt hasn't finished cleaning up yet, signal - // that it should restart when complete - logger.debug("Waiting for previous download to complete"); - this.restartDownload = true; - return; - } - - this.openChannel(); - } - - openChannel() { - this.restartDownload = false; - - try { - this.file = getTemporaryFile(); - this.ownsTempFile = true; - this.stream = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | - FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0); - } - catch (e) { - logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = AddonManager.ERROR_FILE_ACCESS; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onDownloadFailed", - this.listeners, this.wrapper); - return; - } - - let listener = Cc["@mozilla.org/network/stream-listener-tee;1"]. - createInstance(Ci.nsIStreamListenerTee); - listener.init(this, this.stream); - try { - let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true); - this.badCertHandler = new CertUtils.BadCertHandler(!requireBuiltIn); - - this.channel = NetUtil.newChannel({ - uri: this.sourceURI, - loadUsingSystemPrincipal: true - }); - this.channel.notificationCallbacks = this; - if (this.channel instanceof Ci.nsIHttpChannel) { - this.channel.setRequestHeader("Moz-XPI-Update", "1", true); - if (this.channel instanceof Ci.nsIHttpChannelInternal) - this.channel.forceAllowThirdPartyCookie = true; - } - this.channel.asyncOpen2(listener); - - Services.obs.addObserver(this, "network:offline-about-to-go-offline", false); - } - catch (e) { - logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = AddonManager.ERROR_NETWORK_FAILURE; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onDownloadFailed", - this.listeners, this.wrapper); - } - } - - /** - * Update the crypto hasher with the new data and call the progress listeners. - * - * @see nsIStreamListener - */ - onDataAvailable(aRequest, aContext, aInputstream, aOffset, aCount) { - this.crypto.updateFromStream(aInputstream, aCount); - this.progress += aCount; - if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress", - this.listeners, this.wrapper)) { - // TODO cancel the download and make it available again (bug 553024) - } - } - - /** - * Check the redirect response for a hash of the target XPI and verify that - * we don't end up on an insecure channel. - * - * @see nsIChannelEventSink - */ - asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) { - if (!this.hash && aOldChannel.originalURI.schemeIs("https") && - aOldChannel instanceof Ci.nsIHttpChannel) { - try { - let hashStr = aOldChannel.getResponseHeader("X-Target-Digest"); - let hashSplit = hashStr.toLowerCase().split(":"); - this.hash = { - algorithm: hashSplit[0], - data: hashSplit[1] - }; - } - catch (e) { - } - } - - // Verify that we don't end up on an insecure channel if we haven't got a - // hash to verify with (see bug 537761 for discussion) - if (!this.hash) - this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback); - else - aCallback.onRedirectVerifyCallback(Cr.NS_OK); - - this.channel = aNewChannel; - } - - /** - * This is the first chance to get at real headers on the channel. - * - * @see nsIStreamListener - */ - onStartRequest(aRequest, aContext) { - this.crypto = Cc["@mozilla.org/security/hash;1"]. - createInstance(Ci.nsICryptoHash); - if (this.hash) { - try { - this.crypto.initWithString(this.hash.algorithm); - } - catch (e) { - logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = AddonManager.ERROR_INCORRECT_HASH; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onDownloadFailed", - this.listeners, this.wrapper); - aRequest.cancel(Cr.NS_BINDING_ABORTED); - return; - } - } - else { - // We always need something to consume data from the inputstream passed - // to onDataAvailable so just create a dummy cryptohasher to do that. - this.crypto.initWithString("sha1"); - } - - this.progress = 0; - if (aRequest instanceof Ci.nsIChannel) { - try { - this.maxProgress = aRequest.contentLength; - } - catch (e) { - } - logger.debug("Download started for " + this.sourceURI.spec + " to file " + - this.file.path); - } - } - - /** - * The download is complete. - * - * @see nsIStreamListener - */ - onStopRequest(aRequest, aContext, aStatus) { - this.stream.close(); - this.channel = null; - this.badCerthandler = null; - Services.obs.removeObserver(this, "network:offline-about-to-go-offline"); - - // If the download was cancelled then update the state and send events - if (aStatus == Cr.NS_BINDING_ABORTED) { - if (this.state == AddonManager.STATE_DOWNLOADING) { - logger.debug("Cancelled download of " + this.sourceURI.spec); - this.state = AddonManager.STATE_CANCELLED; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onDownloadCancelled", - this.listeners, this.wrapper); - // If a listener restarted the download then there is no need to - // remove the temporary file - if (this.state != AddonManager.STATE_CANCELLED) - return; - } - - this.removeTemporaryFile(); - if (this.restartDownload) - this.openChannel(); - return; - } - - logger.debug("Download of " + this.sourceURI.spec + " completed."); - - if (Components.isSuccessCode(aStatus)) { - if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) { - if (!this.hash && (aRequest instanceof Ci.nsIChannel)) { - try { - CertUtils.checkCert(aRequest, - !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true)); - } - catch (e) { - this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e); - return; - } - } - - // convert the binary hash data to a hex string. - let calculatedHash = getHashStringForCrypto(this.crypto); - this.crypto = null; - if (this.hash && calculatedHash != this.hash.data) { - this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH, - "Downloaded file hash (" + calculatedHash + - ") did not match provided hash (" + this.hash.data + ")"); - return; - } - - this.loadManifest(this.file).then(() => { - if (this.addon.isCompatible) { - this.downloadCompleted(); - } - else { - // TODO Should we send some event here (bug 557716)? - this.state = AddonManager.STATE_CHECKING; - new UpdateChecker(this.addon, { - onUpdateFinished: aAddon => this.downloadCompleted(), - }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); - } - }, ([error, message]) => { - this.removeTemporaryFile(); - this.downloadFailed(error, message); - }); - } - else if (aRequest instanceof Ci.nsIHttpChannel) { - this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, - aRequest.responseStatus + " " + - aRequest.responseStatusText); - } - else { - this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); - } - } - else { - this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); - } - } - - /** - * Notify listeners that the download failed. - * - * @param aReason - * Something to log about the failure - * @param error - * The error code to pass to the listeners - */ - downloadFailed(aReason, aError) { - logger.warn("Download of " + this.sourceURI.spec + " failed", aError); - this.state = AddonManager.STATE_DOWNLOAD_FAILED; - this.error = aReason; - XPIProvider.removeActiveInstall(this); - AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners, - this.wrapper); - - // If the listener hasn't restarted the download then remove any temporary - // file - if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) { - logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec); - this.removeTemporaryFile(); - } - else - logger.debug("downloadFailed: listener changed AddonInstall state for " + - this.sourceURI.spec + " to " + this.state); - } - - /** - * Notify listeners that the download completed. - */ - downloadCompleted() { - XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => { - if (aAddon) - this.existingAddon = aAddon; - - this.state = AddonManager.STATE_DOWNLOADED; - this.addon.updateDate = Date.now(); - - if (this.existingAddon) { - this.addon.existingAddonID = this.existingAddon.id; - this.addon.installDate = this.existingAddon.installDate; - applyBlocklistChanges(this.existingAddon, this.addon); - } - else { - this.addon.installDate = this.addon.updateDate; - } - - if (AddonManagerPrivate.callInstallListeners("onDownloadEnded", - this.listeners, - this.wrapper)) { - // If a listener changed our state then do not proceed with the install - if (this.state != AddonManager.STATE_DOWNLOADED) - return; - - // If an upgrade listener is registered for this add-on, pass control - // over the upgrade to the add-on. - if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) { - logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`); - let resumeFn = () => { - logger.info(`${this.addon.id} has resumed a previously postponed upgrade`); - this.state = AddonManager.STATE_DOWNLOADED; - this.install(); - } - this.postpone(resumeFn); - } else { - // no upgrade listener present, so proceed with normal install - this.install(); - if (this.linkedInstalls) { - for (let install of this.linkedInstalls) { - if (install.state == AddonManager.STATE_DOWNLOADED) - install.install(); - } - } - } - } - }); - } - - getInterface(iid) { - if (iid.equals(Ci.nsIAuthPrompt2)) { - let win = null; - if (this.browser) { - win = this.browser.contentWindow || this.browser.ownerDocument.defaultView; - } - - let factory = Cc["@mozilla.org/prompter;1"]. - getService(Ci.nsIPromptFactory); - let prompt = factory.getPrompt(win, Ci.nsIAuthPrompt2); - - if (this.browser && prompt instanceof Ci.nsILoginManagerPrompter) - prompt.browser = this.browser; - - return prompt; - } - else if (iid.equals(Ci.nsIChannelEventSink)) { - return this; - } - - return this.badCertHandler.getInterface(iid); - } - - /** - * Postone a pending update, until restart or until the add-on resumes. - * - * @param {Function} resumeFn - a function for the add-on to run - * when resuming. - */ - postpone(resumeFn) { - return Task.spawn((function*() { - this.state = AddonManager.STATE_POSTPONED; - - let stagingDir = this.installLocation.getStagingDir(); - let stagedAddon = stagingDir.clone(); - - yield this.installLocation.requestStagingDir(); - yield this.unstageInstall(stagedAddon); - - stagedAddon.append(this.addon.id); - stagedAddon.leafName = this.addon.id + ".xpi"; - - yield this.stageInstall(true, stagedAddon, true); - - AddonManagerPrivate.callInstallListeners("onInstallPostponed", - this.listeners, this.wrapper) - - // upgrade has been staged for restart, provide a way for it to call the - // resume function. - let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id); - if (callback) { - callback({ - version: this.version, - install: () => { - switch (this.state) { - case AddonManager.STATE_POSTPONED: - if (resumeFn) { - resumeFn(); - } - break; - default: - logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`); - break; - } - }, - }); - } - // Release the staging directory lock, but since the staging dir is populated - // it will not be removed until resumed or installed by restart. - // See also cleanStagingDir() - this.installLocation.releaseStagingDir(); - }).bind(this)); - } -} - -/** - * This class exists just for the specific case of staged add-ons that - * fail to install at startup. When that happens, the add-on remains - * staged but we want to keep track of it like other installs so that we - * can clean it up if the same add-on is installed again (see the comment - * about "pending installs for the same add-on" in AddonInstall.startInstall) - */ -class StagedAddonInstall extends AddonInstall { - constructor(installLocation, dir, manifest) { - super(installLocation, dir); - - this.name = manifest.name; - this.type = manifest.type; - this.version = manifest.version; - this.icons = manifest.icons; - this.releaseNotesURI = manifest.releaseNotesURI ? - NetUtil.newURI(manifest.releaseNotesURI) : - null; - this.sourceURI = manifest.sourceURI ? - NetUtil.newURI(manifest.sourceURI) : - null; - this.file = null; - this.addon = manifest; - - this.state = AddonManager.STATE_INSTALLED; - } -} - -/** - * Creates a new AddonInstall to install an add-on from a local file. - * - * @param file - * The file to install - * @param location - * The location to install to - * @returns Promise - * A Promise that resolves with the new install object. - */ -function createLocalInstall(file, location) { - if (!location) { - location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; - } - let url = Services.io.newFileURI(file); - - try { - let install = new LocalAddonInstall(location, url); - return install.init().then(() => install); - } - catch (e) { - logger.error("Error creating install", e); - XPIProvider.removeActiveInstall(this); - return Promise.resolve(null); - } -} - -/** - * Creates a new AddonInstall to download and install a URL. - * - * @param aCallback - * The callback to pass the new AddonInstall to - * @param aUri - * The URI to download - * @param aHash - * A hash for the add-on - * @param aName - * A name for the add-on - * @param aIcons - * An icon URLs for the add-on - * @param aVersion - * A version for the add-on - * @param aBrowser - * The browser performing the install - */ -function createDownloadInstall(aCallback, aUri, aHash, aName, aIcons, - aVersion, aBrowser) { - let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; - let url = NetUtil.newURI(aUri); - - if (url instanceof Ci.nsIFileURL) { - let install = new LocalAddonInstall(location, url, aHash); - install.init().then(() => { aCallback(install); }); - } else { - let install = new DownloadAddonInstall(location, url, aHash, null, - aBrowser, aName, null, aIcons, - aVersion); - aCallback(install); - } -} - -/** - * Creates a new AddonInstall for an update. - * - * @param aCallback - * The callback to pass the new AddonInstall to - * @param aAddon - * The add-on being updated - * @param aUpdate - * The metadata about the new version from the update manifest - */ -function createUpdate(aCallback, aAddon, aUpdate) { - let url = NetUtil.newURI(aUpdate.updateURL); - - Task.spawn(function*() { - let install; - if (url instanceof Ci.nsIFileURL) { - install = new LocalAddonInstall(aAddon._installLocation, url, - aUpdate.updateHash, aAddon); - yield install.init(); - } else { - install = new DownloadAddonInstall(aAddon._installLocation, url, - aUpdate.updateHash, aAddon, null, - aAddon.selectedLocale.name, aAddon.type, - aAddon.icons, aUpdate.version); - } - try { - if (aUpdate.updateInfoURL) - install.releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL)); - } - catch (e) { - // If the releaseNotesURI cannot be parsed then just ignore it. - } - - aCallback(install); - }); -} - -// This map is shared between AddonInstallWrapper and AddonWrapper -const wrapperMap = new WeakMap(); -let installFor = wrapper => wrapperMap.get(wrapper); -let addonFor = installFor; - -/** - * Creates a wrapper for an AddonInstall that only exposes the public API - * - * @param install - * The AddonInstall to create a wrapper for - */ -function AddonInstallWrapper(aInstall) { - wrapperMap.set(this, aInstall); -} - -AddonInstallWrapper.prototype = { - get __AddonInstallInternal__() { - return AppConstants.DEBUG ? installFor(this) : undefined; - }, - - get type() { - return getExternalType(installFor(this).type); - }, - - get iconURL() { - return installFor(this).icons[32]; - }, - - get existingAddon() { - let install = installFor(this); - return install.existingAddon ? install.existingAddon.wrapper : null; - }, - - get addon() { - let install = installFor(this); - return install.addon ? install.addon.wrapper : null; - }, - - get sourceURI() { - return installFor(this).sourceURI; - }, - - get linkedInstalls() { - let install = installFor(this); - if (!install.linkedInstalls) - return null; - return install.linkedInstalls.map(i => i.wrapper); - }, - - install: function() { - installFor(this).install(); - }, - - cancel: function() { - installFor(this).cancel(); - }, - - addListener: function(listener) { - installFor(this).addListener(listener); - }, - - removeListener: function(listener) { - installFor(this).removeListener(listener); - }, -}; - -["name", "version", "icons", "releaseNotesURI", "file", "state", "error", - "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) { - Object.defineProperty(AddonInstallWrapper.prototype, aProp, { - get: function() { - return installFor(this)[aProp]; - }, - enumerable: true, - }); -}); - -/** - * Creates a new update checker. - * - * @param aAddon - * The add-on to check for updates - * @param aListener - * An UpdateListener to notify of updates - * @param aReason - * The reason for the update check - * @param aAppVersion - * An optional application version to check for updates for - * @param aPlatformVersion - * An optional platform version to check for updates for - * @throws if the aListener or aReason arguments are not valid - */ -function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) { - if (!aListener || !aReason) - throw Cr.NS_ERROR_INVALID_ARG; - - Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); - - this.addon = aAddon; - aAddon._updateCheck = this; - XPIProvider.doing(this); - this.listener = aListener; - this.appVersion = aAppVersion; - this.platformVersion = aPlatformVersion; - this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED); - - let updateURL = aAddon.updateURL; - if (!updateURL) { - if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE && - Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) { - updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL); - } else { - updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL); - } - } - - const UPDATE_TYPE_COMPATIBILITY = 32; - const UPDATE_TYPE_NEWVERSION = 64; - - aReason |= UPDATE_TYPE_COMPATIBILITY; - if ("onUpdateAvailable" in this.listener) - aReason |= UPDATE_TYPE_NEWVERSION; - - let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion); - this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey, - url, this); -} - -UpdateChecker.prototype = { - addon: null, - listener: null, - appVersion: null, - platformVersion: null, - syncCompatibility: null, - - /** - * Calls a method on the listener passing any number of arguments and - * consuming any exceptions. - * - * @param aMethod - * The method to call on the listener - */ - callListener: function(aMethod, ...aArgs) { - if (!(aMethod in this.listener)) - return; - - try { - this.listener[aMethod].apply(this.listener, aArgs); - } - catch (e) { - logger.warn("Exception calling UpdateListener method " + aMethod, e); - } - }, - - /** - * Called when AddonUpdateChecker completes the update check - * - * @param updates - * The list of update details for the add-on - */ - onUpdateCheckComplete: function(aUpdates) { - XPIProvider.done(this.addon._updateCheck); - this.addon._updateCheck = null; - let AUC = AddonUpdateChecker; - - let ignoreMaxVersion = false; - let ignoreStrictCompat = false; - if (!AddonManager.checkCompatibility) { - ignoreMaxVersion = true; - ignoreStrictCompat = true; - } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES && - !AddonManager.strictCompatibility && - !this.addon.strictCompatibility && - !this.addon.hasBinaryComponents) { - ignoreMaxVersion = true; - } - - // Always apply any compatibility update for the current version - let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, - this.syncCompatibility, - null, null, - ignoreMaxVersion, - ignoreStrictCompat); - // Apply the compatibility update to the database - if (compatUpdate) - this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility); - - // If the request is for an application or platform version that is - // different to the current application or platform version then look for a - // compatibility update for those versions. - if ((this.appVersion && - Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) || - (this.platformVersion && - Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) { - compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, - false, this.appVersion, - this.platformVersion, - ignoreMaxVersion, - ignoreStrictCompat); - } - - if (compatUpdate) - this.callListener("onCompatibilityUpdateAvailable", this.addon.wrapper); - else - this.callListener("onNoCompatibilityUpdateAvailable", this.addon.wrapper); - - function sendUpdateAvailableMessages(aSelf, aInstall) { - if (aInstall) { - aSelf.callListener("onUpdateAvailable", aSelf.addon.wrapper, - aInstall.wrapper); - } - else { - aSelf.callListener("onNoUpdateAvailable", aSelf.addon.wrapper); - } - aSelf.callListener("onUpdateFinished", aSelf.addon.wrapper, - AddonManager.UPDATE_STATUS_NO_ERROR); - } - - let compatOverrides = AddonManager.strictCompatibility ? - null : - this.addon.compatibilityOverrides; - - let update = AUC.getNewestCompatibleUpdate(aUpdates, - this.appVersion, - this.platformVersion, - ignoreMaxVersion, - ignoreStrictCompat, - compatOverrides); - - if (update && Services.vc.compare(this.addon.version, update.version) < 0 - && !this.addon._installLocation.locked) { - for (let currentInstall of XPIProvider.installs) { - // Skip installs that don't match the available update - if (currentInstall.existingAddon != this.addon || - currentInstall.version != update.version) - continue; - - // If the existing install has not yet started downloading then send an - // available update notification. If it is already downloading then - // don't send any available update notification - if (currentInstall.state == AddonManager.STATE_AVAILABLE) { - logger.debug("Found an existing AddonInstall for " + this.addon.id); - sendUpdateAvailableMessages(this, currentInstall); - } - else - sendUpdateAvailableMessages(this, null); - return; - } - - createUpdate(aInstall => { - sendUpdateAvailableMessages(this, aInstall); - }, this.addon, update); - } - else { - sendUpdateAvailableMessages(this, null); - } - }, - - /** - * Called when AddonUpdateChecker fails the update check - * - * @param aError - * An error status - */ - onUpdateCheckError: function(aError) { - XPIProvider.done(this.addon._updateCheck); - this.addon._updateCheck = null; - this.callListener("onNoCompatibilityUpdateAvailable", this.addon.wrapper); - this.callListener("onNoUpdateAvailable", this.addon.wrapper); - this.callListener("onUpdateFinished", this.addon.wrapper, aError); - }, - - /** - * Called to cancel an in-progress update check - */ - cancel: function() { - let parser = this._parser; - if (parser) { - this._parser = null; - // This will call back to onUpdateCheckError with a CANCELLED error - parser.cancel(); - } - } -}; - -/** - * The AddonInternal is an internal only representation of add-ons. It may - * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm) - * or an install manifest. - */ -function AddonInternal() { - this._hasResourceCache = new Map(); - - XPCOMUtils.defineLazyGetter(this, "wrapper", () => { - return new AddonWrapper(this); - }); -} - -AddonInternal.prototype = { - _selectedLocale: null, - _hasResourceCache: null, - active: false, - visible: false, - userDisabled: false, - appDisabled: false, - softDisabled: false, - sourceURI: null, - releaseNotesURI: null, - foreignInstall: false, - seen: true, - skinnable: false, - - /** - * @property {Array<string>} dependencies - * An array of bootstrapped add-on IDs on which this add-on depends. - * The add-on will remain appDisabled if any of the dependent - * add-ons is not installed and enabled. - */ - dependencies: Object.freeze([]), - hasEmbeddedWebExtension: false, - - get selectedLocale() { - if (this._selectedLocale) - return this._selectedLocale; - let locale = Locale.findClosestLocale(this.locales); - this._selectedLocale = locale ? locale : this.defaultLocale; - return this._selectedLocale; - }, - - get providesUpdatesSecurely() { - return !!(this.updateKey || !this.updateURL || - this.updateURL.substring(0, 6) == "https:"); - }, - - get isCorrectlySigned() { - switch (this._installLocation.name) { - case KEY_APP_SYSTEM_ADDONS: - // System add-ons must be signed by the system key. - return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM - - case KEY_APP_SYSTEM_DEFAULTS: - case KEY_APP_TEMPORARY: - // Temporary and built-in system add-ons do not require signing. - return true; - - case KEY_APP_SYSTEM_SHARE: - case KEY_APP_SYSTEM_LOCAL: - // On UNIX platforms except OSX, an additional location for system - // add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons - // installed there do not require signing. - if (Services.appinfo.OS != "Darwin") - return true; - break; - } - - if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) - return true; - return this.signedState > AddonManager.SIGNEDSTATE_MISSING; - }, - - get isCompatible() { - return this.isCompatibleWith(); - }, - - get disabled() { - return (this.userDisabled || this.appDisabled || this.softDisabled); - }, - - get isPlatformCompatible() { - if (this.targetPlatforms.length == 0) - return true; - - let matchedOS = false; - - // If any targetPlatform matches the OS and contains an ABI then we will - // only match a targetPlatform that contains both the current OS and ABI - let needsABI = false; - - // Some platforms do not specify an ABI, test against null in that case. - let abi = null; - try { - abi = Services.appinfo.XPCOMABI; - } - catch (e) { } - - // Something is causing errors in here - try { - for (let platform of this.targetPlatforms) { - if (platform.os == Services.appinfo.OS) { - if (platform.abi) { - needsABI = true; - if (platform.abi === abi) - return true; - } - else { - matchedOS = true; - } - } - } - } catch (e) { - let message = "Problem with addon " + this.id + " targetPlatforms " - + JSON.stringify(this.targetPlatforms); - logger.error(message, e); - AddonManagerPrivate.recordException("XPI", message, e); - // don't trust this add-on - return false; - } - - return matchedOS && !needsABI; - }, - - isCompatibleWith: function(aAppVersion, aPlatformVersion) { - let app = this.matchingTargetApplication; - if (!app) - return false; - - // set reasonable defaults for minVersion and maxVersion - let minVersion = app.minVersion || "0"; - let maxVersion = app.maxVersion || "*"; - - if (!aAppVersion) - aAppVersion = Services.appinfo.version; - if (!aPlatformVersion) - aPlatformVersion = Services.appinfo.platformVersion; - - let version; - if (app.id == Services.appinfo.ID) - version = aAppVersion; - else if (app.id == TOOLKIT_ID) - version = aPlatformVersion - else if (app.id == WEBEXTENSIONS_ID) - version = WEBEXTENSIONS_VERSION - - // Only extensions and dictionaries can be compatible by default; themes - // and language packs always use strict compatibility checking. - if (this.type in COMPATIBLE_BY_DEFAULT_TYPES && - !AddonManager.strictCompatibility && !this.strictCompatibility && - !this.hasBinaryComponents) { - - // The repository can specify compatibility overrides. - // Note: For now, only blacklisting is supported by overrides. - if (this._repositoryAddon && - this._repositoryAddon.compatibilityOverrides) { - let overrides = this._repositoryAddon.compatibilityOverrides; - let override = AddonRepository.findMatchingCompatOverride(this.version, - overrides); - if (override && override.type == "incompatible") - return false; - } - - // Extremely old extensions should not be compatible by default. - let minCompatVersion; - if (app.id == Services.appinfo.ID) - minCompatVersion = XPIProvider.minCompatibleAppVersion; - else if (app.id == TOOLKIT_ID || app.id == WEBEXTENSIONS_ID) - minCompatVersion = XPIProvider.minCompatiblePlatformVersion; - - if (minCompatVersion && - Services.vc.compare(minCompatVersion, maxVersion) > 0) - return false; - - return Services.vc.compare(version, minVersion) >= 0; - } - - return (Services.vc.compare(version, minVersion) >= 0) && - (Services.vc.compare(version, maxVersion) <= 0) - }, - - get matchingTargetApplication() { - let app = null; - for (let targetApp of this.targetApplications) { - if (targetApp.id == Services.appinfo.ID) - return targetApp; - if (targetApp.id == TOOLKIT_ID || targetApp.id == WEBEXTENSIONS_ID) - app = targetApp; - } - return app; - }, - - get blocklistState() { - let staticItem = findMatchingStaticBlocklistItem(this); - if (staticItem) - return staticItem.level; - - return Blocklist.getAddonBlocklistState(this.wrapper); - }, - - get blocklistURL() { - let staticItem = findMatchingStaticBlocklistItem(this); - if (staticItem) { - let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL"); - return url.replace(/%blockID%/g, staticItem.blockID); - } - - return Blocklist.getAddonBlocklistURL(this.wrapper); - }, - - applyCompatibilityUpdate: function(aUpdate, aSyncCompatibility) { - for (let targetApp of this.targetApplications) { - for (let updateTarget of aUpdate.targetApplications) { - if (targetApp.id == updateTarget.id && (aSyncCompatibility || - Services.vc.compare(targetApp.maxVersion, updateTarget.maxVersion) < 0)) { - targetApp.minVersion = updateTarget.minVersion; - targetApp.maxVersion = updateTarget.maxVersion; - } - } - } - if (aUpdate.multiprocessCompatible !== undefined) - this.multiprocessCompatible = aUpdate.multiprocessCompatible; - this.appDisabled = !isUsableAddon(this); - }, - - /** - * getDataDirectory tries to execute the callback with two arguments: - * 1) the path of the data directory within the profile, - * 2) any exception generated from trying to build it. - */ - getDataDirectory: function(callback) { - let parentPath = OS.Path.join(OS.Constants.Path.profileDir, "extension-data"); - let dirPath = OS.Path.join(parentPath, this.id); - - Task.spawn(function*() { - yield OS.File.makeDir(parentPath, {ignoreExisting: true}); - yield OS.File.makeDir(dirPath, {ignoreExisting: true}); - }).then(() => callback(dirPath, null), - e => callback(dirPath, e)); - }, - - /** - * toJSON is called by JSON.stringify in order to create a filtered version - * of this object to be serialized to a JSON file. A new object is returned - * with copies of all non-private properties. Functions, getters and setters - * are not copied. - * - * @param aKey - * The key that this object is being serialized as in the JSON. - * Unused here since this is always the main object serialized - * - * @return an object containing copies of the properties of this object - * ignoring private properties, functions, getters and setters - */ - toJSON: function(aKey) { - let obj = {}; - for (let prop in this) { - // Ignore the wrapper property - if (prop == "wrapper") - continue; - - // Ignore private properties - if (prop.substring(0, 1) == "_") - continue; - - // Ignore getters - if (this.__lookupGetter__(prop)) - continue; - - // Ignore setters - if (this.__lookupSetter__(prop)) - continue; - - // Ignore functions - if (typeof this[prop] == "function") - continue; - - obj[prop] = this[prop]; - } - - return obj; - }, - - /** - * When an add-on install is pending its metadata will be cached in a file. - * This method reads particular properties of that metadata that may be newer - * than that in the install manifest, like compatibility information. - * - * @param aObj - * A JS object containing the cached metadata - */ - importMetadata: function(aObj) { - for (let prop of PENDING_INSTALL_METADATA) { - if (!(prop in aObj)) - continue; - - this[prop] = aObj[prop]; - } - - // Compatibility info may have changed so update appDisabled - this.appDisabled = !isUsableAddon(this); - }, - - permissions: function() { - let permissions = 0; - - // Add-ons that aren't installed cannot be modified in any way - if (!(this.inDatabase)) - return permissions; - - if (!this.appDisabled) { - if (this.userDisabled || this.softDisabled) { - permissions |= AddonManager.PERM_CAN_ENABLE; - } - else if (this.type != "theme") { - permissions |= AddonManager.PERM_CAN_DISABLE; - } - } - - // Add-ons that are in locked install locations, or are pending uninstall - // cannot be upgraded or uninstalled - if (!this._installLocation.locked && !this.pendingUninstall) { - // Experiments cannot be upgraded. - // System add-on upgrades are triggered through a different mechanism (see updateSystemAddons()) - let isSystem = (this._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || - this._installLocation.name == KEY_APP_SYSTEM_ADDONS); - // Add-ons that are installed by a file link cannot be upgraded. - if (this.type != "experiment" && - !this._installLocation.isLinkedAddon(this.id) && !isSystem) { - permissions |= AddonManager.PERM_CAN_UPGRADE; - } - - permissions |= AddonManager.PERM_CAN_UNINSTALL; - } - - return permissions; - }, -}; - -/** - * The AddonWrapper wraps an Addon to provide the data visible to consumers of - * the public API. - */ -function AddonWrapper(aAddon) { - wrapperMap.set(this, aAddon); -} - -AddonWrapper.prototype = { - get __AddonInternal__() { - return AppConstants.DEBUG ? addonFor(this) : undefined; - }, - - get seen() { - return addonFor(this).seen; - }, - - get hasEmbeddedWebExtension() { - return addonFor(this).hasEmbeddedWebExtension; - }, - - markAsSeen: function() { - addonFor(this).seen = true; - XPIDatabase.saveChanges(); - }, - - get type() { - return getExternalType(addonFor(this).type); - }, - - get isWebExtension() { - return addonFor(this).type == "webextension"; - }, - - get temporarilyInstalled() { - return addonFor(this)._installLocation == TemporaryInstallLocation; - }, - - get aboutURL() { - return this.isActive ? addonFor(this)["aboutURL"] : null; - }, - - get optionsURL() { - if (!this.isActive) { - return null; - } - - let addon = addonFor(this); - if (addon.optionsURL) { - if (this.isWebExtension || this.hasEmbeddedWebExtension) { - // The internal object's optionsURL property comes from the addons - // DB and should be a relative URL. However, extensions with - // options pages installed before bug 1293721 was fixed got absolute - // URLs in the addons db. This code handles both cases. - let base = ExtensionManagement.getURLForExtension(addon.id); - if (!base) { - return null; - } - return new URL(addon.optionsURL, base).href; - } - return addon.optionsURL; - } - - if (this.hasResource("options.xul")) - return this.getResourceURI("options.xul").spec; - - return null; - }, - - get optionsType() { - if (!this.isActive) - return null; - - let addon = addonFor(this); - let hasOptionsXUL = this.hasResource("options.xul"); - let hasOptionsURL = !!this.optionsURL; - - if (addon.optionsType) { - switch (parseInt(addon.optionsType, 10)) { - case AddonManager.OPTIONS_TYPE_DIALOG: - case AddonManager.OPTIONS_TYPE_TAB: - return hasOptionsURL ? addon.optionsType : null; - case AddonManager.OPTIONS_TYPE_INLINE: - case AddonManager.OPTIONS_TYPE_INLINE_INFO: - case AddonManager.OPTIONS_TYPE_INLINE_BROWSER: - return (hasOptionsXUL || hasOptionsURL) ? addon.optionsType : null; - } - return null; - } - - if (hasOptionsXUL) - return AddonManager.OPTIONS_TYPE_INLINE; - - if (hasOptionsURL) - return AddonManager.OPTIONS_TYPE_DIALOG; - - return null; - }, - - get iconURL() { - return AddonManager.getPreferredIconURL(this, 48); - }, - - get icon64URL() { - return AddonManager.getPreferredIconURL(this, 64); - }, - - get icons() { - let addon = addonFor(this); - let icons = {}; - - if (addon._repositoryAddon) { - for (let size in addon._repositoryAddon.icons) { - icons[size] = addon._repositoryAddon.icons[size]; - } - } - - if (addon.icons) { - for (let size in addon.icons) { - icons[size] = this.getResourceURI(addon.icons[size]).spec; - } - } else { - // legacy add-on that did not update its icon data yet - if (this.hasResource("icon.png")) { - icons[32] = icons[48] = this.getResourceURI("icon.png").spec; - } - if (this.hasResource("icon64.png")) { - icons[64] = this.getResourceURI("icon64.png").spec; - } - } - - if (this.isActive && addon.iconURL) { - icons[32] = addon.iconURL; - icons[48] = addon.iconURL; - } - - if (this.isActive && addon.icon64URL) { - icons[64] = addon.icon64URL; - } - - Object.freeze(icons); - return icons; - }, - - get screenshots() { - let addon = addonFor(this); - let repositoryAddon = addon._repositoryAddon; - if (repositoryAddon && ("screenshots" in repositoryAddon)) { - let repositoryScreenshots = repositoryAddon.screenshots; - if (repositoryScreenshots && repositoryScreenshots.length > 0) - return repositoryScreenshots; - } - - if (addon.type == "theme" && this.hasResource("preview.png")) { - let url = this.getResourceURI("preview.png").spec; - return [new AddonManagerPrivate.AddonScreenshot(url)]; - } - - return null; - }, - - get applyBackgroundUpdates() { - return addonFor(this).applyBackgroundUpdates; - }, - set applyBackgroundUpdates(val) { - let addon = addonFor(this); - if (this.type == "experiment") { - logger.warn("Setting applyBackgroundUpdates on an experiment is not supported."); - return addon.applyBackgroundUpdates; - } - - if (val != AddonManager.AUTOUPDATE_DEFAULT && - val != AddonManager.AUTOUPDATE_DISABLE && - val != AddonManager.AUTOUPDATE_ENABLE) { - val = val ? AddonManager.AUTOUPDATE_DEFAULT : - AddonManager.AUTOUPDATE_DISABLE; - } - - if (val == addon.applyBackgroundUpdates) - return val; - - XPIDatabase.setAddonProperties(addon, { - applyBackgroundUpdates: val - }); - AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]); - - return val; - }, - - set syncGUID(val) { - let addon = addonFor(this); - if (addon.syncGUID == val) - return val; - - if (addon.inDatabase) - XPIDatabase.setAddonSyncGUID(addon, val); - - addon.syncGUID = val; - - return val; - }, - - get install() { - let addon = addonFor(this); - if (!("_install" in addon) || !addon._install) - return null; - return addon._install.wrapper; - }, - - get pendingUpgrade() { - let addon = addonFor(this); - return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null; - }, - - get scope() { - let addon = addonFor(this); - if (addon._installLocation) - return addon._installLocation.scope; - - return AddonManager.SCOPE_PROFILE; - }, - - get pendingOperations() { - let addon = addonFor(this); - let pending = 0; - if (!(addon.inDatabase)) { - // Add-on is pending install if there is no associated install (shouldn't - // happen here) or if the install is in the process of or has successfully - // completed the install. If an add-on is pending install then we ignore - // any other pending operations. - if (!addon._install || addon._install.state == AddonManager.STATE_INSTALLING || - addon._install.state == AddonManager.STATE_INSTALLED) - return AddonManager.PENDING_INSTALL; - } - else if (addon.pendingUninstall) { - // If an add-on is pending uninstall then we ignore any other pending - // operations - return AddonManager.PENDING_UNINSTALL; - } - - if (addon.active && addon.disabled) - pending |= AddonManager.PENDING_DISABLE; - else if (!addon.active && !addon.disabled) - pending |= AddonManager.PENDING_ENABLE; - - if (addon.pendingUpgrade) - pending |= AddonManager.PENDING_UPGRADE; - - return pending; - }, - - get operationsRequiringRestart() { - let addon = addonFor(this); - let ops = 0; - if (XPIProvider.installRequiresRestart(addon)) - ops |= AddonManager.OP_NEEDS_RESTART_INSTALL; - if (XPIProvider.uninstallRequiresRestart(addon)) - ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL; - if (XPIProvider.enableRequiresRestart(addon)) - ops |= AddonManager.OP_NEEDS_RESTART_ENABLE; - if (XPIProvider.disableRequiresRestart(addon)) - ops |= AddonManager.OP_NEEDS_RESTART_DISABLE; - - return ops; - }, - - get isDebuggable() { - return this.isActive && addonFor(this).bootstrap; - }, - - get permissions() { - return addonFor(this).permissions(); - }, - - get isActive() { - let addon = addonFor(this); - if (!addon.active) - return false; - if (!Services.appinfo.inSafeMode) - return true; - return addon.bootstrap && canRunInSafeMode(addon); - }, - - get userDisabled() { - let addon = addonFor(this); - return addon.softDisabled || addon.userDisabled; - }, - set userDisabled(val) { - let addon = addonFor(this); - if (val == this.userDisabled) { - return val; - } - - if (addon.inDatabase) { - if (addon.type == "theme" && val) { - if (addon.internalName == XPIProvider.defaultSkin) - throw new Error("Cannot disable the default theme"); - XPIProvider.enableDefaultTheme(); - } - else { - // hidden and system add-ons should not be user disasbled, - // as there is no UI to re-enable them. - if (this.hidden) { - throw new Error(`Cannot disable hidden add-on ${addon.id}`); - } - XPIProvider.updateAddonDisabledState(addon, val); - } - } - else { - addon.userDisabled = val; - // When enabling remove the softDisabled flag - if (!val) - addon.softDisabled = false; - } - - return val; - }, - - set softDisabled(val) { - let addon = addonFor(this); - if (val == addon.softDisabled) - return val; - - if (addon.inDatabase) { - // When softDisabling a theme just enable the active theme - if (addon.type == "theme" && val && !addon.userDisabled) { - if (addon.internalName == XPIProvider.defaultSkin) - throw new Error("Cannot disable the default theme"); - XPIProvider.enableDefaultTheme(); - } - else { - XPIProvider.updateAddonDisabledState(addon, undefined, val); - } - } - else if (!addon.userDisabled) { - // Only set softDisabled if not already disabled - addon.softDisabled = val; - } - - return val; - }, - - get hidden() { - let addon = addonFor(this); - if (addon._installLocation.name == KEY_APP_TEMPORARY) - return false; - - return (addon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || - addon._installLocation.name == KEY_APP_SYSTEM_ADDONS); - }, - - get isSystem() { - let addon = addonFor(this); - return (addon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || - addon._installLocation.name == KEY_APP_SYSTEM_ADDONS); - }, - - // Returns true if Firefox Sync should sync this addon. Only non-hotfixes - // directly in the profile are considered syncable. - get isSyncable() { - let addon = addonFor(this); - let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined); - if (hotfixID && hotfixID == addon.id) { - return false; - } - return (addon._installLocation.name == KEY_APP_PROFILE); - }, - - isCompatibleWith: function(aAppVersion, aPlatformVersion) { - return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion); - }, - - uninstall: function(alwaysAllowUndo) { - let addon = addonFor(this); - XPIProvider.uninstallAddon(addon, alwaysAllowUndo); - }, - - cancelUninstall: function() { - let addon = addonFor(this); - XPIProvider.cancelUninstallAddon(addon); - }, - - findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) { - // Short-circuit updates for experiments because updates are handled - // through the Experiments Manager. - if (this.type == "experiment") { - AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason, - aAppVersion, aPlatformVersion); - return; - } - - new UpdateChecker(addonFor(this), aListener, aReason, aAppVersion, aPlatformVersion); - }, - - // Returns true if there was an update in progress, false if there was no update to cancel - cancelUpdate: function() { - let addon = addonFor(this); - if (addon._updateCheck) { - addon._updateCheck.cancel(); - return true; - } - return false; - }, - - hasResource: function(aPath) { - let addon = addonFor(this); - if (addon._hasResourceCache.has(aPath)) - return addon._hasResourceCache.get(aPath); - - let bundle = addon._sourceBundle.clone(); - - // Bundle may not exist any more if the addon has just been uninstalled, - // but explicitly first checking .exists() results in unneeded file I/O. - try { - var isDir = bundle.isDirectory(); - } catch (e) { - addon._hasResourceCache.set(aPath, false); - return false; - } - - if (isDir) { - if (aPath) - aPath.split("/").forEach(part => bundle.append(part)); - let result = bundle.exists(); - addon._hasResourceCache.set(aPath, result); - return result; - } - - let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. - createInstance(Ci.nsIZipReader); - try { - zipReader.open(bundle); - let result = zipReader.hasEntry(aPath); - addon._hasResourceCache.set(aPath, result); - return result; - } - catch (e) { - addon._hasResourceCache.set(aPath, false); - return false; - } - finally { - zipReader.close(); - } - }, - - /** - * Reloads the add-on. - * - * For temporarily installed add-ons, this uninstalls and re-installs the - * add-on. Otherwise, the addon is disabled and then re-enabled, and the cache - * is flushed. - * - * @return Promise - */ - reload: function() { - return new Promise((resolve) => { - const addon = addonFor(this); - - logger.debug(`reloading add-on ${addon.id}`); - - if (!this.temporarilyInstalled) { - let addonFile = addon.getResourceURI; - XPIProvider.updateAddonDisabledState(addon, true); - Services.obs.notifyObservers(addonFile, "flush-cache-entry", null); - XPIProvider.updateAddonDisabledState(addon, false) - resolve(); - } else { - // This function supports re-installing an existing add-on. - resolve(AddonManager.installTemporaryAddon(addon._sourceBundle)); - } - }); - }, - - /** - * Returns a URI to the selected resource or to the add-on bundle if aPath - * is null. URIs to the bundle will always be file: URIs. URIs to resources - * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is - * still an XPI file. - * - * @param aPath - * The path in the add-on to get the URI for or null to get a URI to - * the file or directory the add-on is installed as. - * @return an nsIURI - */ - getResourceURI: function(aPath) { - let addon = addonFor(this); - if (!aPath) - return NetUtil.newURI(addon._sourceBundle); - - return getURIForResourceInFile(addon._sourceBundle, aPath); - } -}; - -/** - * The PrivateWrapper is used to expose certain functionality only when being - * called with the add-on instanceID, disallowing other add-ons to access it. - */ -function PrivateWrapper(aAddon) { - AddonWrapper.call(this, aAddon); -} - -PrivateWrapper.prototype = Object.create(AddonWrapper.prototype); -Object.assign(PrivateWrapper.prototype, { - addonId() { - return this.id; - }, - - /** - * Retrieves the preferred global context to be used from the - * add-on debugging window. - * - * @returns global - * The object set as global context. Must be a window object. - */ - getDebugGlobal(global) { - let activeAddon = XPIProvider.activeAddons.get(this.id); - if (activeAddon) { - return activeAddon.debugGlobal; - } - - return null; - }, - - /** - * Defines a global context to be used in the console - * of the add-on debugging window. - * - * @param global - * The object to set as global context. Must be a window object. - */ - setDebugGlobal(global) { - if (!global) { - // If the new global is null, notify the listeners regardless - // from the current state of the addon. - // NOTE: this happen after the addon has been disabled and - // the global will never be set to null otherwise. - AddonManagerPrivate.callAddonListeners("onPropertyChanged", - addonFor(this), - ["debugGlobal"]); - } else { - let activeAddon = XPIProvider.activeAddons.get(this.id); - if (activeAddon) { - let globalChanged = activeAddon.debugGlobal != global; - activeAddon.debugGlobal = global; - - if (globalChanged) { - AddonManagerPrivate.callAddonListeners("onPropertyChanged", - addonFor(this), - ["debugGlobal"]); - } - } - } - } -}); - -function chooseValue(aAddon, aObj, aProp) { - let repositoryAddon = aAddon._repositoryAddon; - let objValue = aObj[aProp]; - - if (repositoryAddon && (aProp in repositoryAddon) && - (objValue === undefined || objValue === null)) { - return [repositoryAddon[aProp], true]; - } - - return [objValue, false]; -} - -function defineAddonWrapperProperty(name, getter) { - Object.defineProperty(AddonWrapper.prototype, name, { - get: getter, - enumerable: true, - }); -} - -["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible", - "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled", - "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents", - "strictCompatibility", "compatibilityOverrides", "updateURL", "dependencies", - "getDataDirectory", "multiprocessCompatible", "signedState", "mpcOptedOut", - "isCorrectlySigned"].forEach(function(aProp) { - defineAddonWrapperProperty(aProp, function() { - let addon = addonFor(this); - return (aProp in addon) ? addon[aProp] : undefined; - }); -}); - -["fullDescription", "developerComments", "eula", "supportURL", - "contributionURL", "contributionAmount", "averageRating", "reviewCount", - "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers", - "repositoryStatus"].forEach(function(aProp) { - defineAddonWrapperProperty(aProp, function() { - let addon = addonFor(this); - if (addon._repositoryAddon) - return addon._repositoryAddon[aProp]; - - return null; - }); -}); - -["installDate", "updateDate"].forEach(function(aProp) { - defineAddonWrapperProperty(aProp, function() { - return new Date(addonFor(this)[aProp]); - }); -}); - -["sourceURI", "releaseNotesURI"].forEach(function(aProp) { - defineAddonWrapperProperty(aProp, function() { - let addon = addonFor(this); - - // Temporary Installed Addons do not have a "sourceURI", - // But we can use the "_sourceBundle" as an alternative, - // which points to the path of the addon xpi installed - // or its source dir (if it has been installed from a - // directory). - if (aProp == "sourceURI" && this.temporarilyInstalled) { - return Services.io.newFileURI(addon._sourceBundle); - } - - let [target, fromRepo] = chooseValue(addon, addon, aProp); - if (!target) - return null; - if (fromRepo) - return target; - return NetUtil.newURI(target); - }); -}); - -PROP_LOCALE_SINGLE.forEach(function(aProp) { - defineAddonWrapperProperty(aProp, function() { - let addon = addonFor(this); - // Override XPI creator if repository creator is defined - if (aProp == "creator" && - addon._repositoryAddon && addon._repositoryAddon.creator) { - return addon._repositoryAddon.creator; - } - - let result = null; - - if (addon.active) { - try { - let pref = PREF_EM_EXTENSION_FORMAT + addon.id + "." + aProp; - let value = Preferences.get(pref, null, Ci.nsIPrefLocalizedString); - if (value) - result = value; - } - catch (e) { - } - } - - let rest; - if (result == null) - [result, ...rest] = chooseValue(addon, addon.selectedLocale, aProp); - - if (aProp == "creator") - return result ? new AddonManagerPrivate.AddonAuthor(result) : null; - - return result; - }); -}); - -PROP_LOCALE_MULTI.forEach(function(aProp) { - defineAddonWrapperProperty(aProp, function() { - let addon = addonFor(this); - let results = null; - let usedRepository = false; - - if (addon.active) { - let pref = PREF_EM_EXTENSION_FORMAT + addon.id + "." + - aProp.substring(0, aProp.length - 1); - let list = Services.prefs.getChildList(pref, {}); - if (list.length > 0) { - list.sort(); - results = []; - for (let childPref of list) { - let value = Preferences.get(childPref, null, Ci.nsIPrefLocalizedString); - if (value) - results.push(value); - } - } - } - - if (results == null) - [results, usedRepository] = chooseValue(addon, addon.selectedLocale, aProp); - - if (results && !usedRepository) { - results = results.map(function(aResult) { - return new AddonManagerPrivate.AddonAuthor(aResult); - }); - } - - return results; - }); -}); - -/** - * An object which identifies a directory install location for add-ons. The - * location consists of a directory which contains the add-ons installed in the - * location. - * - * Each add-on installed in the location is either a directory containing the - * add-on's files or a text file containing an absolute path to the directory - * containing the add-ons files. The directory or text file must have the same - * name as the add-on's ID. - * - * @param aName - * The string identifier for the install location - * @param aDirectory - * The nsIFile directory for the install location - * @param aScope - * The scope of add-ons installed in this location - */ -function DirectoryInstallLocation(aName, aDirectory, aScope) { - this._name = aName; - this.locked = true; - this._directory = aDirectory; - this._scope = aScope - this._IDToFileMap = {}; - this._linkedAddons = []; - - if (!aDirectory || !aDirectory.exists()) - return; - if (!aDirectory.isDirectory()) - throw new Error("Location must be a directory."); - - this._readAddons(); -} - -DirectoryInstallLocation.prototype = { - _name : "", - _directory : null, - _IDToFileMap : null, // mapping from add-on ID to nsIFile - - /** - * Reads a directory linked to in a file. - * - * @param file - * The file containing the directory path - * @return An nsIFile object representing the linked directory. - */ - _readDirectoryFromFile: function(aFile) { - let linkedDirectory; - if (aFile.isSymlink()) { - linkedDirectory = aFile.clone(); - try { - linkedDirectory.normalize(); - } catch (e) { - logger.warn("Symbolic link " + aFile.path + " points to a path" + - " which does not exist"); - return null; - } - } - else { - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - fis.init(aFile, -1, -1, false); - let line = { value: "" }; - if (fis instanceof Ci.nsILineInputStream) - fis.readLine(line); - fis.close(); - if (line.value) { - linkedDirectory = Cc["@mozilla.org/file/local;1"]. - createInstance(Ci.nsIFile); - - try { - linkedDirectory.initWithPath(line.value); - } - catch (e) { - linkedDirectory.setRelativeDescriptor(aFile.parent, line.value); - } - } - } - - if (linkedDirectory) { - if (!linkedDirectory.exists()) { - logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + - " which does not exist"); - return null; - } - - if (!linkedDirectory.isDirectory()) { - logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + - " which is not a directory"); - return null; - } - - return linkedDirectory; - } - - logger.warn("File pointer " + aFile.path + " does not contain a path"); - return null; - }, - - /** - * Finds all the add-ons installed in this location. - */ - _readAddons: function() { - // Use a snapshot of the directory contents to avoid possible issues with - // iterating over a directory while removing files from it (the YAFFS2 - // embedded filesystem has this issue, see bug 772238). - let entries = getDirectoryEntries(this._directory); - for (let entry of entries) { - let id = entry.leafName; - - if (id == DIR_STAGE || id == DIR_TRASH) - continue; - - let directLoad = false; - if (entry.isFile() && - id.substring(id.length - 4).toLowerCase() == ".xpi") { - directLoad = true; - id = id.substring(0, id.length - 4); - } - - if (!gIDTest.test(id)) { - logger.debug("Ignoring file entry whose name is not a valid add-on ID: " + - entry.path); - continue; - } - - if (!directLoad && (entry.isFile() || entry.isSymlink())) { - let newEntry = this._readDirectoryFromFile(entry); - if (!newEntry) { - logger.debug("Deleting stale pointer file " + entry.path); - try { - entry.remove(true); - } - catch (e) { - logger.warn("Failed to remove stale pointer file " + entry.path, e); - // Failing to remove the stale pointer file is ignorable - } - continue; - } - - entry = newEntry; - this._linkedAddons.push(id); - } - - this._IDToFileMap[id] = entry; - XPIProvider._addURIMapping(id, entry); - } - }, - - /** - * Gets the name of this install location. - */ - get name() { - return this._name; - }, - - /** - * Gets the scope of this install location. - */ - get scope() { - return this._scope; - }, - - /** - * Gets an array of nsIFiles for add-ons installed in this location. - */ - getAddonLocations: function() { - let locations = new Map(); - for (let id in this._IDToFileMap) { - locations.set(id, this._IDToFileMap[id].clone()); - } - return locations; - }, - - /** - * Gets the directory that the add-on with the given ID is installed in. - * - * @param aId - * The ID of the add-on - * @return The nsIFile - * @throws if the ID does not match any of the add-ons installed - */ - getLocationForID: function(aId) { - if (aId in this._IDToFileMap) - return this._IDToFileMap[aId].clone(); - throw new Error("Unknown add-on ID " + aId); - }, - - /** - * Returns true if the given addon was installed in this location by a text - * file pointing to its real path. - * - * @param aId - * The ID of the addon - */ - isLinkedAddon: function(aId) { - return this._linkedAddons.indexOf(aId) != -1; - } -}; - -/** - * An extension of DirectoryInstallLocation which adds methods to installing - * and removing add-ons from the directory at runtime. - * - * @param aName - * The string identifier for the install location - * @param aDirectory - * The nsIFile directory for the install location - * @param aScope - * The scope of add-ons installed in this location - */ -function MutableDirectoryInstallLocation(aName, aDirectory, aScope) { - DirectoryInstallLocation.call(this, aName, aDirectory, aScope); - this.locked = false; - this._stagingDirLock = 0; -} - -MutableDirectoryInstallLocation.prototype = Object.create(DirectoryInstallLocation.prototype); -Object.assign(MutableDirectoryInstallLocation.prototype, { - /** - * Gets the staging directory to put add-ons that are pending install and - * uninstall into. - * - * @return an nsIFile - */ - getStagingDir: function() { - let dir = this._directory.clone(); - dir.append(DIR_STAGE); - return dir; - }, - - requestStagingDir: function() { - this._stagingDirLock++; - - if (this._stagingDirPromise) - return this._stagingDirPromise; - - OS.File.makeDir(this._directory.path); - let stagepath = OS.Path.join(this._directory.path, DIR_STAGE); - return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => { - if (e instanceof OS.File.Error && e.becauseExists) - return; - logger.error("Failed to create staging directory", e); - throw e; - }); - }, - - releaseStagingDir: function() { - this._stagingDirLock--; - - if (this._stagingDirLock == 0) { - this._stagingDirPromise = null; - this.cleanStagingDir(); - } - - return Promise.resolve(); - }, - - /** - * Removes the specified files or directories in the staging directory and - * then if the staging directory is empty attempts to remove it. - * - * @param aLeafNames - * An array of file or directory to remove from the directory, the - * array may be empty - */ - cleanStagingDir: function(aLeafNames = []) { - let dir = this.getStagingDir(); - - for (let name of aLeafNames) { - let file = dir.clone(); - file.append(name); - recursiveRemove(file); - } - - if (this._stagingDirLock > 0) - return; - - let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); - try { - if (dirEntries.nextFile) - return; - } - finally { - dirEntries.close(); - } - - try { - setFilePermissions(dir, FileUtils.PERMS_DIRECTORY); - dir.remove(false); - } - catch (e) { - logger.warn("Failed to remove staging dir", e); - // Failing to remove the staging directory is ignorable - } - }, - - /** - * Returns a directory that is normally on the same filesystem as the rest of - * the install location and can be used for temporarily storing files during - * safe move operations. Calling this method will delete the existing trash - * directory and its contents. - * - * @return an nsIFile - */ - getTrashDir: function() { - let trashDir = this._directory.clone(); - trashDir.append(DIR_TRASH); - let trashDirExists = trashDir.exists(); - try { - if (trashDirExists) - recursiveRemove(trashDir); - trashDirExists = false; - } catch (e) { - logger.warn("Failed to remove trash directory", e); - } - if (!trashDirExists) - trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - - return trashDir; - }, - - /** - * Installs an add-on into the install location. - * - * @param id - * The ID of the add-on to install - * @param source - * The source nsIFile to install from - * @param existingAddonID - * The ID of an existing add-on to uninstall at the same time - * @param action - * What to we do with the given source file: - * "move" - * Default action, the source files will be moved to the new - * location, - * "copy" - * The source files will be copied, - * "proxy" - * A "proxy file" is going to refer to the source file path - * @return an nsIFile indicating where the add-on was installed to - */ - installAddon: function({ id, source, existingAddonID, action = "move" }) { - let trashDir = this.getTrashDir(); - - let transaction = new SafeInstallOperation(); - - let moveOldAddon = aId => { - let file = this._directory.clone(); - file.append(aId); - - if (file.exists()) - transaction.moveUnder(file, trashDir); - - file = this._directory.clone(); - file.append(aId + ".xpi"); - if (file.exists()) { - flushJarCache(file); - transaction.moveUnder(file, trashDir); - } - } - - // If any of these operations fails the finally block will clean up the - // temporary directory - try { - moveOldAddon(id); - if (existingAddonID && existingAddonID != id) { - moveOldAddon(existingAddonID); - - { - // Move the data directories. - /* XXX ajvincent We can't use OS.File: installAddon isn't compatible - * with Promises, nor is SafeInstallOperation. Bug 945540 has been filed - * for porting to OS.File. - */ - let oldDataDir = FileUtils.getDir( - KEY_PROFILEDIR, ["extension-data", existingAddonID], false, true - ); - - if (oldDataDir.exists()) { - let newDataDir = FileUtils.getDir( - KEY_PROFILEDIR, ["extension-data", id], false, true - ); - if (newDataDir.exists()) { - let trashData = trashDir.clone(); - trashData.append("data-directory"); - transaction.moveUnder(newDataDir, trashData); - } - - transaction.moveTo(oldDataDir, newDataDir); - } - } - } - - if (action == "copy") { - transaction.copy(source, this._directory); - } - else if (action == "move") { - if (source.isFile()) - flushJarCache(source); - - transaction.moveUnder(source, this._directory); - } - // Do nothing for the proxy file as we sideload an addon permanently - } - finally { - // It isn't ideal if this cleanup fails but it isn't worth rolling back - // the install because of it. - try { - recursiveRemove(trashDir); - } - catch (e) { - logger.warn("Failed to remove trash directory when installing " + id, e); - } - } - - let newFile = this._directory.clone(); - - if (action == "proxy") { - // When permanently installing sideloaded addon, we just put a proxy file - // refering to the addon sources - newFile.append(id); - - writeStringToFile(newFile, source.path); - } else { - newFile.append(source.leafName); - } - - try { - newFile.lastModifiedTime = Date.now(); - } catch (e) { - logger.warn("failed to set lastModifiedTime on " + newFile.path, e); - } - this._IDToFileMap[id] = newFile; - XPIProvider._addURIMapping(id, newFile); - - if (existingAddonID && existingAddonID != id && - existingAddonID in this._IDToFileMap) { - delete this._IDToFileMap[existingAddonID]; - } - - return newFile; - }, - - /** - * Uninstalls an add-on from this location. - * - * @param aId - * The ID of the add-on to uninstall - * @throws if the ID does not match any of the add-ons installed - */ - uninstallAddon: function(aId) { - let file = this._IDToFileMap[aId]; - if (!file) { - logger.warn("Attempted to remove " + aId + " from " + - this._name + " but it was already gone"); - return; - } - - file = this._directory.clone(); - file.append(aId); - if (!file.exists()) - file.leafName += ".xpi"; - - if (!file.exists()) { - logger.warn("Attempted to remove " + aId + " from " + - this._name + " but it was already gone"); - - delete this._IDToFileMap[aId]; - return; - } - - let trashDir = this.getTrashDir(); - - if (file.leafName != aId) { - logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId); - flushJarCache(file); - } - - let transaction = new SafeInstallOperation(); - - try { - transaction.moveUnder(file, trashDir); - } - finally { - // It isn't ideal if this cleanup fails, but it is probably better than - // rolling back the uninstall at this point - try { - recursiveRemove(trashDir); - } - catch (e) { - logger.warn("Failed to remove trash directory when uninstalling " + aId, e); - } - } - - delete this._IDToFileMap[aId]; - }, -}); - -/** - * An object which identifies a directory install location for system add-ons - * upgrades. - * - * The location consists of a directory which contains the add-ons installed. - * - * @param aName - * The string identifier for the install location - * @param aDirectory - * The nsIFile directory for the install location - * @param aScope - * The scope of add-ons installed in this location - * @param aResetSet - * True to throw away the current add-on set - */ -function SystemAddonInstallLocation(aName, aDirectory, aScope, aResetSet) { - this._baseDir = aDirectory; - this._nextDir = null; - - this._stagingDirLock = 0; - - if (aResetSet) - this.resetAddonSet(); - - this._addonSet = this._loadAddonSet(); - - this._directory = null; - if (this._addonSet.directory) { - this._directory = aDirectory.clone(); - this._directory.append(this._addonSet.directory); - logger.info("SystemAddonInstallLocation scanning directory " + this._directory.path); - } - else { - logger.info("SystemAddonInstallLocation directory is missing"); - } - - DirectoryInstallLocation.call(this, aName, this._directory, aScope); - this.locked = false; -} - -SystemAddonInstallLocation.prototype = Object.create(DirectoryInstallLocation.prototype); -Object.assign(SystemAddonInstallLocation.prototype, { - /** - * Removes the specified files or directories in the staging directory and - * then if the staging directory is empty attempts to remove it. - * - * @param aLeafNames - * An array of file or directory to remove from the directory, the - * array may be empty - */ - cleanStagingDir: function(aLeafNames = []) { - let dir = this.getStagingDir(); - - for (let name of aLeafNames) { - let file = dir.clone(); - file.append(name); - recursiveRemove(file); - } - - if (this._stagingDirLock > 0) - return; - - let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); - try { - if (dirEntries.nextFile) - return; - } - finally { - dirEntries.close(); - } - - try { - setFilePermissions(dir, FileUtils.PERMS_DIRECTORY); - dir.remove(false); - } - catch (e) { - logger.warn("Failed to remove staging dir", e); - // Failing to remove the staging directory is ignorable - } - }, - - /** - * Gets the staging directory to put add-ons that are pending install and - * uninstall into. - * - * @return {nsIFile} - staging directory for system add-on upgrades. - */ - getStagingDir: function() { - this._addonSet = this._loadAddonSet(); - let dir = null; - if (this._addonSet.directory) { - this._directory = this._baseDir.clone(); - this._directory.append(this._addonSet.directory); - dir = this._directory.clone(); - dir.append(DIR_STAGE); - } - else { - logger.info("SystemAddonInstallLocation directory is missing"); - } - - return dir; - }, - - requestStagingDir: function() { - this._stagingDirLock++; - if (this._stagingDirPromise) - return this._stagingDirPromise; - - this._addonSet = this._loadAddonSet(); - if (this._addonSet.directory) { - this._directory = this._baseDir.clone(); - this._directory.append(this._addonSet.directory); - } - - OS.File.makeDir(this._directory.path); - let stagepath = OS.Path.join(this._directory.path, DIR_STAGE); - return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => { - if (e instanceof OS.File.Error && e.becauseExists) - return; - logger.error("Failed to create staging directory", e); - throw e; - }); - }, - - releaseStagingDir: function() { - this._stagingDirLock--; - - if (this._stagingDirLock == 0) { - this._stagingDirPromise = null; - this.cleanStagingDir(); - } - - return Promise.resolve(); - }, - - /** - * Reads the current set of system add-ons - */ - _loadAddonSet: function() { - try { - let setStr = Preferences.get(PREF_SYSTEM_ADDON_SET, null); - if (setStr) { - let addonSet = JSON.parse(setStr); - if ((typeof addonSet == "object") && addonSet.schema == 1) - return addonSet; - } - } - catch (e) { - logger.error("Malformed system add-on set, resetting."); - } - - return { schema: 1, addons: {} }; - }, - - /** - * Saves the current set of system add-ons - * - * @param {Object} aAddonSet - object containing schema, directory and set - * of system add-on IDs and versions. - */ - _saveAddonSet: function(aAddonSet) { - Preferences.set(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet)); - }, - - getAddonLocations: function() { - // Updated system add-ons are ignored in safe mode - if (Services.appinfo.inSafeMode) - return new Map(); - - let addons = DirectoryInstallLocation.prototype.getAddonLocations.call(this); - - // Strip out any unexpected add-ons from the list - for (let id of addons.keys()) { - if (!(id in this._addonSet.addons)) - addons.delete(id); - } - - return addons; - }, - - /** - * Tests whether updated system add-ons are expected. - */ - isActive: function() { - return this._directory != null; - }, - - isValidAddon: function(aAddon) { - if (aAddon.appDisabled) { - logger.warn(`System add-on ${aAddon.id} isn't compatible with the application.`); - return false; - } - - if (aAddon.unpack) { - logger.warn(`System add-on ${aAddon.id} isn't a packed add-on.`); - return false; - } - - if (!aAddon.bootstrap) { - logger.warn(`System add-on ${aAddon.id} isn't restartless.`); - return false; - } - - if (!aAddon.multiprocessCompatible) { - logger.warn(`System add-on ${aAddon.id} isn't multiprocess compatible.`); - return false; - } - - return true; - }, - - /** - * Tests whether the loaded add-on information matches what is expected. - */ - isValid: function(aAddons) { - for (let id of Object.keys(this._addonSet.addons)) { - if (!aAddons.has(id)) { - logger.warn(`Expected add-on ${id} is missing from the system add-on location.`); - return false; - } - - let addon = aAddons.get(id); - if (addon.version != this._addonSet.addons[id].version) { - logger.warn(`Expected system add-on ${id} to be version ${this._addonSet.addons[id].version} but was ${addon.version}.`); - return false; - } - - if (!this.isValidAddon(addon)) - return false; - } - - return true; - }, - - /** - * Resets the add-on set so on the next startup the default set will be used. - */ - resetAddonSet: function() { - - if (this._addonSet) { - logger.info("Removing all system add-on upgrades."); - - // remove everything from the pref first, if uninstall - // fails then at least they will not be re-activated on - // next restart. - this._saveAddonSet({ schema: 1, addons: {} }); - - for (let id of Object.keys(this._addonSet.addons)) { - AddonManager.getAddonByID(id, addon => { - if (addon) { - addon.uninstall(); - } - }); - } - } - }, - - /** - * Removes any directories not currently in use or pending use after a - * restart. Any errors that happen here don't really matter as we'll attempt - * to cleanup again next time. - */ - cleanDirectories: Task.async(function*() { - - // System add-ons directory does not exist - if (!(yield OS.File.exists(this._baseDir.path))) { - return; - } - - let iterator; - try { - iterator = new OS.File.DirectoryIterator(this._baseDir.path); - } - catch (e) { - logger.error("Failed to clean updated system add-ons directories.", e); - return; - } - - try { - let entries = []; - - yield iterator.forEach(entry => { - // Skip the directory currently in use - if (this._directory && this._directory.path == entry.path) - return; - - // Skip the next directory - if (this._nextDir && this._nextDir.path == entry.path) - return; - - entries.push(entry); - }); - - for (let entry of entries) { - if (entry.isDir) { - yield OS.File.removeDir(entry.path, { - ignoreAbsent: true, - ignorePermissions: true, - }); - } - else { - yield OS.File.remove(entry.path, { - ignoreAbsent: true, - }); - } - } - } - catch (e) { - logger.error("Failed to clean updated system add-ons directories.", e); - } - finally { - iterator.close(); - } - }), - - /** - * Installs a new set of system add-ons into the location and updates the - * add-on set in prefs. - * - * @param {Array} aAddons - An array of addons to install. - */ - installAddonSet: Task.async(function*(aAddons) { - // Make sure the base dir exists - yield OS.File.makeDir(this._baseDir.path, { ignoreExisting: true }); - - let addonSet = this._loadAddonSet(); - - // Remove any add-ons that are no longer part of the set. - for (let addonID of Object.keys(addonSet.addons)) { - if (!aAddons.includes(addonID)) { - AddonManager.getAddonByID(addonID, a => a.uninstall()); - } - } - - let newDir = this._baseDir.clone(); - - let uuidGen = Cc["@mozilla.org/uuid-generator;1"]. - getService(Ci.nsIUUIDGenerator); - newDir.append("blank"); - - while (true) { - newDir.leafName = uuidGen.generateUUID().toString(); - - try { - yield OS.File.makeDir(newDir.path, { ignoreExisting: false }); - break; - } - catch (e) { - logger.debug("Could not create new system add-on updates dir, retrying", e); - } - } - - // Record the new upgrade directory. - let state = { schema: 1, directory: newDir.leafName, addons: {} }; - this._saveAddonSet(state); - - this._nextDir = newDir; - let location = this; - - let installs = []; - for (let addon of aAddons) { - let install = yield createLocalInstall(addon._sourceBundle, location); - installs.push(install); - } - - let installAddon = Task.async(function*(install) { - // Make the new install own its temporary file. - install.ownsTempFile = true; - install.install(); - }); - - let postponeAddon = Task.async(function*(install) { - let resumeFn; - if (AddonManagerPrivate.hasUpgradeListener(install.addon.id)) { - logger.info(`system add-on ${install.addon.id} has an upgrade listener, postponing upgrade set until restart`); - resumeFn = () => { - logger.info(`${install.addon.id} has resumed a previously postponed addon set`); - install.installLocation.resumeAddonSet(installs); - } - } - yield install.postpone(resumeFn); - }); - - let previousState; - - try { - // All add-ons in position, create the new state and store it in prefs - state = { schema: 1, directory: newDir.leafName, addons: {} }; - for (let addon of aAddons) { - state.addons[addon.id] = { - version: addon.version - } - } - - previousState = this._loadAddonSet(); - this._saveAddonSet(state); - - let blockers = aAddons.filter( - addon => AddonManagerPrivate.hasUpgradeListener(addon.id) - ); - - if (blockers.length > 0) { - yield waitForAllPromises(installs.map(postponeAddon)); - } else { - yield waitForAllPromises(installs.map(installAddon)); - } - } - catch (e) { - // Roll back to previous upgrade set (if present) on restart. - if (previousState) { - this._saveAddonSet(previousState); - } - // Otherwise, roll back to built-in set on restart. - // TODO try to do these restartlessly - this.resetAddonSet(); - - try { - yield OS.File.removeDir(newDir.path, { ignorePermissions: true }); - } - catch (e) { - logger.warn(`Failed to remove failed system add-on directory ${newDir.path}.`, e); - } - throw e; - } - }), - - /** - * Resumes upgrade of a previously-delayed add-on set. - */ - resumeAddonSet: Task.async(function*(installs) { - let resumeAddon = Task.async(function*(install) { - install.state = AddonManager.STATE_DOWNLOADED; - install.installLocation.releaseStagingDir(); - install.install(); - }); - - let addonSet = this._loadAddonSet(); - let addonIDs = Object.keys(addonSet.addons); - - let blockers = installs.filter( - install => AddonManagerPrivate.hasUpgradeListener(install.addon.id) - ); - - if (blockers.length > 1) { - logger.warn("Attempted to resume system add-on install but upgrade blockers are still present"); - } else { - yield waitForAllPromises(installs.map(resumeAddon)); - } - }), - - /** - * Returns a directory that is normally on the same filesystem as the rest of - * the install location and can be used for temporarily storing files during - * safe move operations. Calling this method will delete the existing trash - * directory and its contents. - * - * @return an nsIFile - */ - getTrashDir: function() { - let trashDir = this._directory.clone(); - trashDir.append(DIR_TRASH); - let trashDirExists = trashDir.exists(); - try { - if (trashDirExists) - recursiveRemove(trashDir); - trashDirExists = false; - } catch (e) { - logger.warn("Failed to remove trash directory", e); - } - if (!trashDirExists) - trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); - - return trashDir; - }, - - /** - * Installs an add-on into the install location. - * - * @param id - * The ID of the add-on to install - * @param source - * The source nsIFile to install from - * @return an nsIFile indicating where the add-on was installed to - */ - installAddon: function({id, source}) { - let trashDir = this.getTrashDir(); - let transaction = new SafeInstallOperation(); - - // If any of these operations fails the finally block will clean up the - // temporary directory - try { - if (source.isFile()) { - flushJarCache(source); - } - - transaction.moveUnder(source, this._directory); - } - finally { - // It isn't ideal if this cleanup fails but it isn't worth rolling back - // the install because of it. - try { - recursiveRemove(trashDir); - } - catch (e) { - logger.warn("Failed to remove trash directory when installing " + id, e); - } - } - - let newFile = this._directory.clone(); - newFile.append(source.leafName); - - try { - newFile.lastModifiedTime = Date.now(); - } catch (e) { - logger.warn("failed to set lastModifiedTime on " + newFile.path, e); - } - this._IDToFileMap[id] = newFile; - XPIProvider._addURIMapping(id, newFile); - - return newFile; - }, - - // old system add-on upgrade dirs get automatically removed - uninstallAddon: (aAddon) => {}, -}); - -/** - * An object which identifies an install location for temporary add-ons. - */ -const TemporaryInstallLocation = { - locked: false, - name: KEY_APP_TEMPORARY, - scope: AddonManager.SCOPE_TEMPORARY, - getAddonLocations: () => [], - isLinkedAddon: () => false, - installAddon: () => {}, - uninstallAddon: (aAddon) => {}, - getStagingDir: () => {}, -} - -/** - * An object that identifies a registry install location for add-ons. The location - * consists of a registry key which contains string values mapping ID to the - * path where an add-on is installed - * - * @param aName - * The string identifier of this Install Location. - * @param aRootKey - * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey). - * @param scope - * The scope of add-ons installed in this location - */ -function WinRegInstallLocation(aName, aRootKey, aScope) { - this.locked = true; - this._name = aName; - this._rootKey = aRootKey; - this._scope = aScope; - this._IDToFileMap = {}; - - let path = this._appKeyPath + "\\Extensions"; - let key = Cc["@mozilla.org/windows-registry-key;1"]. - createInstance(Ci.nsIWindowsRegKey); - - // Reading the registry may throw an exception, and that's ok. In error - // cases, we just leave ourselves in the empty state. - try { - key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ); - } - catch (e) { - return; - } - - this._readAddons(key); - key.close(); -} - -WinRegInstallLocation.prototype = { - _name : "", - _rootKey : null, - _scope : null, - _IDToFileMap : null, // mapping from ID to nsIFile - - /** - * Retrieves the path of this Application's data key in the registry. - */ - get _appKeyPath() { - let appVendor = Services.appinfo.vendor; - let appName = Services.appinfo.name; - - // XXX Thunderbird doesn't specify a vendor string - if (AppConstants.MOZ_APP_NAME == "thunderbird" && appVendor == "") - appVendor = "Mozilla"; - - // XULRunner-based apps may intentionally not specify a vendor - if (appVendor != "") - appVendor += "\\"; - - return "SOFTWARE\\" + appVendor + appName; - }, - - /** - * Read the registry and build a mapping between ID and path for each - * installed add-on. - * - * @param key - * The key that contains the ID to path mapping - */ - _readAddons: function(aKey) { - let count = aKey.valueCount; - for (let i = 0; i < count; ++i) { - let id = aKey.getValueName(i); - - let file = new nsIFile(aKey.readStringValue(id)); - - if (!file.exists()) { - logger.warn("Ignoring missing add-on in " + file.path); - continue; - } - - this._IDToFileMap[id] = file; - XPIProvider._addURIMapping(id, file); - } - }, - - /** - * Gets the name of this install location. - */ - get name() { - return this._name; - }, - - /** - * Gets the scope of this install location. - */ - get scope() { - return this._scope; - }, - - /** - * Gets an array of nsIFiles for add-ons installed in this location. - */ - getAddonLocations: function() { - let locations = new Map(); - for (let id in this._IDToFileMap) { - locations.set(id, this._IDToFileMap[id].clone()); - } - return locations; - }, - - /** - * @see DirectoryInstallLocation - */ - isLinkedAddon: function(aId) { - return true; - } -}; - -var addonTypes = [ - new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 4000, - AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL), - new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 5000), - new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 7000, - AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL), - new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 8000, - AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL), -]; - -// We only register experiments support if the application supports them. -// Ideally, we would install an observer to watch the pref. Installing -// an observer for this pref is not necessary here and may be buggy with -// regards to registering this XPIProvider twice. -if (Preferences.get("experiments.supported", false)) { - addonTypes.push( - new AddonManagerPrivate.AddonType("experiment", - URI_EXTENSION_STRINGS, - STRING_TYPE_NAME, - AddonManager.VIEW_TYPE_LIST, 11000, - AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL)); -} - -AddonManagerPrivate.registerProvider(XPIProvider, addonTypes); diff --git a/toolkit/mozapps/webextensions/internal/XPIProviderUtils.js b/toolkit/mozapps/webextensions/internal/XPIProviderUtils.js deleted file mode 100644 index 63ff6d8c8..000000000 --- a/toolkit/mozapps/webextensions/internal/XPIProviderUtils.js +++ /dev/null @@ -1,2239 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -// These are injected from XPIProvider.jsm -/* globals ADDON_SIGNING, SIGNED_TYPES, BOOTSTRAP_REASONS, DB_SCHEMA, - AddonInternal, XPIProvider, XPIStates, syncLoadManifestFromFile, - isUsableAddon, recordAddonTelemetry, applyBlocklistChanges, - flushChromeCaches, canRunInSafeMode*/ - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); -/* globals AddonManagerPrivate*/ -Cu.import("resource://gre/modules/Preferences.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", - "resource://gre/modules/addons/AddonRepository.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave", - "resource://gre/modules/DeferredSave.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "Blocklist", - "@mozilla.org/extensions/blocklist;1", - Ci.nsIBlocklistService); - -Cu.import("resource://gre/modules/Log.jsm"); -const LOGGER_ID = "addons.xpi-utils"; - -const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile"); - -// Create a new logger for use by the Addons XPI Provider Utils -// (Requires AddonManager.jsm) -var logger = Log.repository.getLogger(LOGGER_ID); - -const KEY_PROFILEDIR = "ProfD"; -const FILE_DATABASE = "extensions.sqlite"; -const FILE_JSON_DB = "extensions.json"; -const FILE_OLD_DATABASE = "extensions.rdf"; -const FILE_XPI_ADDONS_LIST = "extensions.ini"; - -// The last version of DB_SCHEMA implemented in SQLITE -const LAST_SQLITE_DB_SCHEMA = 14; -const PREF_DB_SCHEMA = "extensions.databaseSchema"; -const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; -const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; -const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; -const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes"; -const PREF_E10S_BLOCKED_BY_ADDONS = "extensions.e10sBlockedByAddons"; -const PREF_E10S_HAS_NONEXEMPT_ADDON = "extensions.e10s.rollout.hasAddon"; - -const KEY_APP_PROFILE = "app-profile"; -const KEY_APP_SYSTEM_ADDONS = "app-system-addons"; -const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults"; -const KEY_APP_GLOBAL = "app-global"; - -// Properties that only exist in the database -const DB_METADATA = ["syncGUID", - "installDate", - "updateDate", - "size", - "sourceURI", - "releaseNotesURI", - "applyBackgroundUpdates"]; -const DB_BOOL_METADATA = ["visible", "active", "userDisabled", "appDisabled", - "pendingUninstall", "bootstrap", "skinnable", - "softDisabled", "isForeignInstall", - "hasBinaryComponents", "strictCompatibility"]; - -// Properties to save in JSON file -const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type", - "internalName", "updateURL", "updateKey", "optionsURL", - "optionsType", "aboutURL", "icons", "iconURL", "icon64URL", - "defaultLocale", "visible", "active", "userDisabled", - "appDisabled", "pendingUninstall", "descriptor", "installDate", - "updateDate", "applyBackgroundUpdates", "bootstrap", - "skinnable", "size", "sourceURI", "releaseNotesURI", - "softDisabled", "foreignInstall", "hasBinaryComponents", - "strictCompatibility", "locales", "targetApplications", - "targetPlatforms", "multiprocessCompatible", "signedState", - "seen", "dependencies", "hasEmbeddedWebExtension", "mpcOptedOut"]; - -// Properties that should be migrated where possible from an old database. These -// shouldn't include properties that can be read directly from install.rdf files -// or calculated -const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled", - "sourceURI", "applyBackgroundUpdates", - "releaseNotesURI", "foreignInstall", "syncGUID"]; - -// Time to wait before async save of XPI JSON database, in milliseconds -const ASYNC_SAVE_DELAY_MS = 20; - -const PREFIX_ITEM_URI = "urn:mozilla:item:"; -const RDFURI_ITEM_ROOT = "urn:mozilla:item:root" -const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; - -XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", - Ci.nsIRDFService); - -function EM_R(aProperty) { - return gRDF.GetResource(PREFIX_NS_EM + aProperty); -} - -/** - * Converts an RDF literal, resource or integer into a string. - * - * @param aLiteral - * The RDF object to convert - * @return a string if the object could be converted or null - */ -function getRDFValue(aLiteral) { - if (aLiteral instanceof Ci.nsIRDFLiteral) - return aLiteral.Value; - if (aLiteral instanceof Ci.nsIRDFResource) - return aLiteral.Value; - if (aLiteral instanceof Ci.nsIRDFInt) - return aLiteral.Value; - return null; -} - -/** - * Gets an RDF property as a string - * - * @param aDs - * The RDF datasource to read the property from - * @param aResource - * The RDF resource to read the property from - * @param aProperty - * The property to read - * @return a string if the property existed or null - */ -function getRDFProperty(aDs, aResource, aProperty) { - return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); -} - -/** - * Asynchronously fill in the _repositoryAddon field for one addon - */ -function getRepositoryAddon(aAddon, aCallback) { - if (!aAddon) { - aCallback(aAddon); - return; - } - function completeAddon(aRepositoryAddon) { - aAddon._repositoryAddon = aRepositoryAddon; - aAddon.compatibilityOverrides = aRepositoryAddon ? - aRepositoryAddon.compatibilityOverrides : - null; - aCallback(aAddon); - } - AddonRepository.getCachedAddonByID(aAddon.id, completeAddon); -} - -/** - * Wrap an API-supplied function in an exception handler to make it safe to call - */ -function makeSafe(aCallback) { - return function(...aArgs) { - try { - aCallback(...aArgs); - } - catch (ex) { - logger.warn("XPI Database callback failed", ex); - } - } -} - -/** - * A helper method to asynchronously call a function on an array - * of objects, calling a callback when function(x) has been gathered - * for every element of the array. - * WARNING: not currently error-safe; if the async function does not call - * our internal callback for any of the array elements, asyncMap will not - * call the callback parameter. - * - * @param aObjects - * The array of objects to process asynchronously - * @param aMethod - * Function with signature function(object, function(f_of_object)) - * @param aCallback - * Function with signature f([aMethod(object)]), called when all values - * are available - */ -function asyncMap(aObjects, aMethod, aCallback) { - var resultsPending = aObjects.length; - var results = [] - if (resultsPending == 0) { - aCallback(results); - return; - } - - function asyncMap_gotValue(aIndex, aValue) { - results[aIndex] = aValue; - if (--resultsPending == 0) { - aCallback(results); - } - } - - aObjects.map(function(aObject, aIndex, aArray) { - try { - aMethod(aObject, function(aResult) { - asyncMap_gotValue(aIndex, aResult); - }); - } - catch (e) { - logger.warn("Async map function failed", e); - asyncMap_gotValue(aIndex, undefined); - } - }); -} - -/** - * A generator to synchronously return result rows from an mozIStorageStatement. - * - * @param aStatement - * The statement to execute - */ -function* resultRows(aStatement) { - try { - while (stepStatement(aStatement)) - yield aStatement.row; - } - finally { - aStatement.reset(); - } -} - -/** - * A helper function to log an SQL error. - * - * @param aError - * The storage error code associated with the error - * @param aErrorString - * An error message - */ -function logSQLError(aError, aErrorString) { - logger.error("SQL error " + aError + ": " + aErrorString); -} - -/** - * A helper function to log any errors that occur during async statements. - * - * @param aError - * A mozIStorageError to log - */ -function asyncErrorLogger(aError) { - logSQLError(aError.result, aError.message); -} - -/** - * A helper function to step a statement synchronously and log any error that - * occurs. - * - * @param aStatement - * A mozIStorageStatement to execute - */ -function stepStatement(aStatement) { - try { - return aStatement.executeStep(); - } - catch (e) { - logSQLError(XPIDatabase.connection.lastError, - XPIDatabase.connection.lastErrorString); - throw e; - } -} - -/** - * Copies properties from one object to another. If no target object is passed - * a new object will be created and returned. - * - * @param aObject - * An object to copy from - * @param aProperties - * An array of properties to be copied - * @param aTarget - * An optional target object to copy the properties to - * @return the object that the properties were copied onto - */ -function copyProperties(aObject, aProperties, aTarget) { - if (!aTarget) - aTarget = {}; - aProperties.forEach(function(aProp) { - if (aProp in aObject) - aTarget[aProp] = aObject[aProp]; - }); - return aTarget; -} - -/** - * Copies properties from a mozIStorageRow to an object. If no target object is - * passed a new object will be created and returned. - * - * @param aRow - * A mozIStorageRow to copy from - * @param aProperties - * An array of properties to be copied - * @param aTarget - * An optional target object to copy the properties to - * @return the object that the properties were copied onto - */ -function copyRowProperties(aRow, aProperties, aTarget) { - if (!aTarget) - aTarget = {}; - aProperties.forEach(function(aProp) { - aTarget[aProp] = aRow.getResultByName(aProp); - }); - return aTarget; -} - -/** - * The DBAddonInternal is a special AddonInternal that has been retrieved from - * the database. The constructor will initialize the DBAddonInternal with a set - * of fields, which could come from either the JSON store or as an - * XPIProvider.AddonInternal created from an addon's manifest - * @constructor - * @param aLoaded - * Addon data fields loaded from JSON or the addon manifest. - */ -function DBAddonInternal(aLoaded) { - AddonInternal.call(this); - - copyProperties(aLoaded, PROP_JSON_FIELDS, this); - - if (!this.dependencies) - this.dependencies = []; - Object.freeze(this.dependencies); - - if (aLoaded._installLocation) { - this._installLocation = aLoaded._installLocation; - this.location = aLoaded._installLocation.name; - } - else if (aLoaded.location) { - this._installLocation = XPIProvider.installLocationsByName[this.location]; - } - - this._key = this.location + ":" + this.id; - - if (!aLoaded._sourceBundle) { - throw new Error("Expected passed argument to contain a descriptor"); - } - - this._sourceBundle = aLoaded._sourceBundle; - - XPCOMUtils.defineLazyGetter(this, "pendingUpgrade", function() { - for (let install of XPIProvider.installs) { - if (install.state == AddonManager.STATE_INSTALLED && - !(install.addon.inDatabase) && - install.addon.id == this.id && - install.installLocation == this._installLocation) { - delete this.pendingUpgrade; - return this.pendingUpgrade = install.addon; - } - } - return null; - }); -} - -DBAddonInternal.prototype = Object.create(AddonInternal.prototype); -Object.assign(DBAddonInternal.prototype, { - applyCompatibilityUpdate: function(aUpdate, aSyncCompatibility) { - let wasCompatible = this.isCompatible; - - this.targetApplications.forEach(function(aTargetApp) { - aUpdate.targetApplications.forEach(function(aUpdateTarget) { - if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility || - Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) { - aTargetApp.minVersion = aUpdateTarget.minVersion; - aTargetApp.maxVersion = aUpdateTarget.maxVersion; - XPIDatabase.saveChanges(); - } - }); - }); - if (aUpdate.multiprocessCompatible !== undefined && - aUpdate.multiprocessCompatible != this.multiprocessCompatible) { - this.multiprocessCompatible = aUpdate.multiprocessCompatible; - XPIDatabase.saveChanges(); - } - - if (wasCompatible != this.isCompatible) - XPIProvider.updateAddonDisabledState(this); - }, - - toJSON: function() { - let jsonData = copyProperties(this, PROP_JSON_FIELDS); - - // Experiments are serialized as disabled so they aren't run on the next - // startup. - if (this.type == "experiment") { - jsonData.userDisabled = true; - jsonData.active = false; - } - - return jsonData; - }, - - get inDatabase() { - return true; - } -}); - -/** - * Internal interface: find an addon from an already loaded addonDB - */ -function _findAddon(addonDB, aFilter) { - for (let addon of addonDB.values()) { - if (aFilter(addon)) { - return addon; - } - } - return null; -} - -/** - * Internal interface to get a filtered list of addons from a loaded addonDB - */ -function _filterDB(addonDB, aFilter) { - return Array.from(addonDB.values()).filter(aFilter); -} - -this.XPIDatabase = { - // true if the database connection has been opened - initialized: false, - // The database file - jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true), - // Migration data loaded from an old version of the database. - migrateData: null, - // Active add-on directories loaded from extensions.ini and prefs at startup. - activeBundles: null, - - // Saved error object if we fail to read an existing database - _loadError: null, - - // Error reported by our most recent attempt to read or write the database, if any - get lastError() { - if (this._loadError) - return this._loadError; - if (this._deferredSave) - return this._deferredSave.lastError; - return null; - }, - - /** - * Mark the current stored data dirty, and schedule a flush to disk - */ - saveChanges: function() { - if (!this.initialized) { - throw new Error("Attempt to use XPI database when it is not initialized"); - } - - if (XPIProvider._closing) { - // use an Error here so we get a stack trace. - let err = new Error("XPI database modified after shutdown began"); - logger.warn(err); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_late_stack", Log.stackTrace(err)); - } - - if (!this._deferredSave) { - this._deferredSave = new DeferredSave(this.jsonFile.path, - () => JSON.stringify(this), - ASYNC_SAVE_DELAY_MS); - } - - let promise = this._deferredSave.saveChanges(); - if (!this._schemaVersionSet) { - this._schemaVersionSet = true; - promise = promise.then( - count => { - // Update the XPIDB schema version preference the first time we successfully - // save the database. - logger.debug("XPI Database saved, setting schema version preference to " + DB_SCHEMA); - Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); - // Reading the DB worked once, so we don't need the load error - this._loadError = null; - }, - error => { - // Need to try setting the schema version again later - this._schemaVersionSet = false; - // this._deferredSave.lastError has the most recent error so we don't - // need this any more - this._loadError = null; - - throw error; - }); - } - - promise.catch(error => { - logger.warn("Failed to save XPI database", error); - }); - }, - - flush: function() { - // handle the "in memory only" and "saveChanges never called" cases - if (!this._deferredSave) { - return Promise.resolve(0); - } - - return this._deferredSave.flush(); - }, - - /** - * Converts the current internal state of the XPI addon database to - * a JSON.stringify()-ready structure - */ - toJSON: function() { - if (!this.addonDB) { - // We never loaded the database? - throw new Error("Attempt to save database without loading it first"); - } - - let toSave = { - schemaVersion: DB_SCHEMA, - addons: [...this.addonDB.values()] - }; - return toSave; - }, - - /** - * Pull upgrade information from an existing SQLITE database - * - * @return false if there is no SQLITE database - * true and sets this.migrateData to null if the SQLITE DB exists - * but does not contain useful information - * true and sets this.migrateData to - * {location: {id1:{addon1}, id2:{addon2}}, location2:{...}, ...} - * if there is useful information - */ - getMigrateDataFromSQLITE: function() { - let connection = null; - let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); - // Attempt to open the database - try { - connection = Services.storage.openUnsharedDatabase(dbfile); - } - catch (e) { - logger.warn("Failed to open sqlite database " + dbfile.path + " for upgrade", e); - return null; - } - logger.debug("Migrating data from sqlite"); - let migrateData = this.getMigrateDataFromDatabase(connection); - connection.close(); - return migrateData; - }, - - /** - * Synchronously opens and reads the database file, upgrading from old - * databases or making a new DB if needed. - * - * The possibilities, in order of priority, are: - * 1) Perfectly good, up to date database - * 2) Out of date JSON database needs to be upgraded => upgrade - * 3) JSON database exists but is mangled somehow => build new JSON - * 4) no JSON DB, but a useable SQLITE db we can upgrade from => upgrade - * 5) useless SQLITE DB => build new JSON - * 6) useable RDF DB => upgrade - * 7) useless RDF DB => build new JSON - * 8) Nothing at all => build new JSON - * @param aRebuildOnError - * A boolean indicating whether add-on information should be loaded - * from the install locations if the database needs to be rebuilt. - * (if false, caller is XPIProvider.checkForChanges() which will rebuild) - */ - syncLoadDB: function(aRebuildOnError) { - this.migrateData = null; - let fstream = null; - let data = ""; - try { - let readTimer = AddonManagerPrivate.simpleTimer("XPIDB_syncRead_MS"); - logger.debug("Opening XPI database " + this.jsonFile.path); - fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. - createInstance(Components.interfaces.nsIFileInputStream); - fstream.init(this.jsonFile, -1, 0, 0); - let cstream = null; - try { - cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. - createInstance(Components.interfaces.nsIConverterInputStream); - cstream.init(fstream, "UTF-8", 0, 0); - - let str = {}; - let read = 0; - do { - read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value - data += str.value; - } while (read != 0); - - readTimer.done(); - this.parseDB(data, aRebuildOnError); - } - catch (e) { - logger.error("Failed to load XPI JSON data from profile", e); - let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); - this.rebuildDatabase(aRebuildOnError); - rebuildTimer.done(); - } - finally { - if (cstream) - cstream.close(); - } - } - catch (e) { - if (e.result === Cr.NS_ERROR_FILE_NOT_FOUND) { - this.upgradeDB(aRebuildOnError); - } - else { - this.rebuildUnreadableDB(e, aRebuildOnError); - } - } - finally { - if (fstream) - fstream.close(); - } - // If an async load was also in progress, resolve that promise with our DB; - // otherwise create a resolved promise - if (this._dbPromise) { - AddonManagerPrivate.recordSimpleMeasure("XPIDB_overlapped_load", 1); - this._dbPromise.resolve(this.addonDB); - } - else - this._dbPromise = Promise.resolve(this.addonDB); - }, - - /** - * Parse loaded data, reconstructing the database if the loaded data is not valid - * @param aRebuildOnError - * If true, synchronously reconstruct the database from installed add-ons - */ - parseDB: function(aData, aRebuildOnError) { - let parseTimer = AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS"); - try { - // dump("Loaded JSON:\n" + aData + "\n"); - let inputAddons = JSON.parse(aData); - // Now do some sanity checks on our JSON db - if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) { - parseTimer.done(); - // Content of JSON file is bad, need to rebuild from scratch - logger.error("bad JSON file contents"); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "badJSON"); - let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildBadJSON_MS"); - this.rebuildDatabase(aRebuildOnError); - rebuildTimer.done(); - return; - } - if (inputAddons.schemaVersion != DB_SCHEMA) { - // Handle mismatched JSON schema version. For now, we assume - // compatibility for JSON data, though we throw away any fields we - // don't know about (bug 902956) - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", - "schemaMismatch-" + inputAddons.schemaVersion); - logger.debug("JSON schema mismatch: expected " + DB_SCHEMA + - ", actual " + inputAddons.schemaVersion); - // When we rev the schema of the JSON database, we need to make sure we - // force the DB to save so that the DB_SCHEMA value in the JSON file and - // the preference are updated. - } - // If we got here, we probably have good data - // Make AddonInternal instances from the loaded data and save them - let addonDB = new Map(); - for (let loadedAddon of inputAddons.addons) { - loadedAddon._sourceBundle = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - try { - loadedAddon._sourceBundle.persistentDescriptor = loadedAddon.descriptor; - } - catch (e) { - // We can fail here when the descriptor is invalid, usually from the - // wrong OS - logger.warn("Could not find source bundle for add-on " + loadedAddon.id, e); - } - - let newAddon = new DBAddonInternal(loadedAddon); - addonDB.set(newAddon._key, newAddon); - } - parseTimer.done(); - this.addonDB = addonDB; - logger.debug("Successfully read XPI database"); - this.initialized = true; - } - catch (e) { - // If we catch and log a SyntaxError from the JSON - // parser, the xpcshell test harness fails the test for us: bug 870828 - parseTimer.done(); - if (e.name == "SyntaxError") { - logger.error("Syntax error parsing saved XPI JSON data"); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "syntax"); - } - else { - logger.error("Failed to load XPI JSON data from profile", e); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "other"); - } - let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); - this.rebuildDatabase(aRebuildOnError); - rebuildTimer.done(); - } - }, - - /** - * Upgrade database from earlier (sqlite or RDF) version if available - */ - upgradeDB: function(aRebuildOnError) { - let upgradeTimer = AddonManagerPrivate.simpleTimer("XPIDB_upgradeDB_MS"); - try { - let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA); - if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) { - // we should have an older SQLITE database - logger.debug("Attempting to upgrade from SQLITE database"); - this.migrateData = this.getMigrateDataFromSQLITE(); - } - else { - // we've upgraded before but the JSON file is gone, fall through - // and rebuild from scratch - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "dbMissing"); - } - } - catch (e) { - // No schema version pref means either a really old upgrade (RDF) or - // a new profile - this.migrateData = this.getMigrateDataFromRDF(); - } - - this.rebuildDatabase(aRebuildOnError); - upgradeTimer.done(); - }, - - /** - * Reconstruct when the DB file exists but is unreadable - * (for example because read permission is denied) - */ - rebuildUnreadableDB: function(aError, aRebuildOnError) { - let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildUnreadableDB_MS"); - logger.warn("Extensions database " + this.jsonFile.path + - " exists but is not readable; rebuilding", aError); - // Remember the error message until we try and write at least once, so - // we know at shutdown time that there was a problem - this._loadError = aError; - AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "unreadable"); - this.rebuildDatabase(aRebuildOnError); - rebuildTimer.done(); - }, - - /** - * Open and read the XPI database asynchronously, upgrading if - * necessary. If any DB load operation fails, we need to - * synchronously rebuild the DB from the installed extensions. - * - * @return Promise<Map> resolves to the Map of loaded JSON data stored - * in this.addonDB; never rejects. - */ - asyncLoadDB: function() { - // Already started (and possibly finished) loading - if (this._dbPromise) { - return this._dbPromise; - } - - logger.debug("Starting async load of XPI database " + this.jsonFile.path); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_async_load", XPIProvider.runPhase); - let readOptions = { - outExecutionDuration: 0 - }; - return this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then( - byteArray => { - logger.debug("Async JSON file read took " + readOptions.outExecutionDuration + " MS"); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_asyncRead_MS", - readOptions.outExecutionDuration); - if (this._addonDB) { - logger.debug("Synchronous load completed while waiting for async load"); - return this.addonDB; - } - logger.debug("Finished async read of XPI database, parsing..."); - let decodeTimer = AddonManagerPrivate.simpleTimer("XPIDB_decode_MS"); - let decoder = new TextDecoder(); - let data = decoder.decode(byteArray); - decodeTimer.done(); - this.parseDB(data, true); - return this.addonDB; - }) - .then(null, - error => { - if (this._addonDB) { - logger.debug("Synchronous load completed while waiting for async load"); - return this.addonDB; - } - if (error.becauseNoSuchFile) { - this.upgradeDB(true); - } - else { - // it's there but unreadable - this.rebuildUnreadableDB(error, true); - } - return this.addonDB; - }); - }, - - /** - * Rebuild the database from addon install directories. If this.migrateData - * is available, uses migrated information for settings on the addons found - * during rebuild - * @param aRebuildOnError - * A boolean indicating whether add-on information should be loaded - * from the install locations if the database needs to be rebuilt. - * (if false, caller is XPIProvider.checkForChanges() which will rebuild) - */ - rebuildDatabase: function(aRebuildOnError) { - this.addonDB = new Map(); - this.initialized = true; - - if (XPIStates.size == 0) { - // No extensions installed, so we're done - logger.debug("Rebuilding XPI database with no extensions"); - return; - } - - // If there is no migration data then load the list of add-on directories - // that were active during the last run - if (!this.migrateData) - this.activeBundles = this.getActiveBundles(); - - if (aRebuildOnError) { - logger.warn("Rebuilding add-ons database from installed extensions."); - try { - XPIDatabaseReconcile.processFileChanges({}, false); - } - catch (e) { - logger.error("Failed to rebuild XPI database from installed extensions", e); - } - // Make sure to update the active add-ons and add-ons list on shutdown - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); - } - }, - - /** - * Gets the list of file descriptors of active extension directories or XPI - * files from the add-ons list. This must be loaded from disk since the - * directory service gives no easy way to get both directly. This list doesn't - * include themes as preferences already say which theme is currently active - * - * @return an array of persistent descriptors for the directories - */ - getActiveBundles: function() { - let bundles = []; - - // non-bootstrapped extensions - let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], - true); - - if (!addonsList.exists()) - // XXX Irving believes this is broken in the case where there is no - // extensions.ini but there are bootstrap extensions (e.g. Android) - return null; - - try { - let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] - .getService(Ci.nsIINIParserFactory); - let parser = iniFactory.createINIParser(addonsList); - let keys = parser.getKeys("ExtensionDirs"); - - while (keys.hasMore()) - bundles.push(parser.getString("ExtensionDirs", keys.getNext())); - } - catch (e) { - logger.warn("Failed to parse extensions.ini", e); - return null; - } - - // Also include the list of active bootstrapped extensions - for (let id in XPIProvider.bootstrappedAddons) - bundles.push(XPIProvider.bootstrappedAddons[id].descriptor); - - return bundles; - }, - - /** - * Retrieves migration data from the old extensions.rdf database. - * - * @return an object holding information about what add-ons were previously - * userDisabled and any updated compatibility information - */ - getMigrateDataFromRDF: function(aDbWasMissing) { - - // Migrate data from extensions.rdf - let rdffile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_DATABASE], true); - if (!rdffile.exists()) - return null; - - logger.debug("Migrating data from " + FILE_OLD_DATABASE); - let migrateData = {}; - - try { - let ds = gRDF.GetDataSourceBlocking(Services.io.newFileURI(rdffile).spec); - let root = Cc["@mozilla.org/rdf/container;1"]. - createInstance(Ci.nsIRDFContainer); - root.Init(ds, gRDF.GetResource(RDFURI_ITEM_ROOT)); - let elements = root.GetElements(); - - while (elements.hasMoreElements()) { - let source = elements.getNext().QueryInterface(Ci.nsIRDFResource); - - let location = getRDFProperty(ds, source, "installLocation"); - if (location) { - if (!(location in migrateData)) - migrateData[location] = {}; - let id = source.ValueUTF8.substring(PREFIX_ITEM_URI.length); - migrateData[location][id] = { - version: getRDFProperty(ds, source, "version"), - userDisabled: false, - targetApplications: [] - } - - let disabled = getRDFProperty(ds, source, "userDisabled"); - if (disabled == "true" || disabled == "needs-disable") - migrateData[location][id].userDisabled = true; - - let targetApps = ds.GetTargets(source, EM_R("targetApplication"), - true); - while (targetApps.hasMoreElements()) { - let targetApp = targetApps.getNext() - .QueryInterface(Ci.nsIRDFResource); - let appInfo = { - id: getRDFProperty(ds, targetApp, "id") - }; - - let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion"); - if (minVersion) { - appInfo.minVersion = minVersion; - appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion"); - } - else { - appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion"); - appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion"); - } - migrateData[location][id].targetApplications.push(appInfo); - } - } - } - } - catch (e) { - logger.warn("Error reading " + FILE_OLD_DATABASE, e); - migrateData = null; - } - - return migrateData; - }, - - /** - * Retrieves migration data from a database that has an older or newer schema. - * - * @return an object holding information about what add-ons were previously - * userDisabled and any updated compatibility information - */ - getMigrateDataFromDatabase: function(aConnection) { - let migrateData = {}; - - // Attempt to migrate data from a different (even future!) version of the - // database - try { - var stmt = aConnection.createStatement("PRAGMA table_info(addon)"); - - const REQUIRED = ["internal_id", "id", "location", "userDisabled", - "installDate", "version"]; - - let reqCount = 0; - let props = []; - for (let row of resultRows(stmt)) { - if (REQUIRED.indexOf(row.name) != -1) { - reqCount++; - props.push(row.name); - } - else if (DB_METADATA.indexOf(row.name) != -1) { - props.push(row.name); - } - else if (DB_BOOL_METADATA.indexOf(row.name) != -1) { - props.push(row.name); - } - } - - if (reqCount < REQUIRED.length) { - logger.error("Unable to read anything useful from the database"); - return null; - } - stmt.finalize(); - - stmt = aConnection.createStatement("SELECT " + props.join(",") + " FROM addon"); - for (let row of resultRows(stmt)) { - if (!(row.location in migrateData)) - migrateData[row.location] = {}; - let addonData = { - targetApplications: [] - } - migrateData[row.location][row.id] = addonData; - - props.forEach(function(aProp) { - if (aProp == "isForeignInstall") - addonData.foreignInstall = (row[aProp] == 1); - if (DB_BOOL_METADATA.indexOf(aProp) != -1) - addonData[aProp] = row[aProp] == 1; - else - addonData[aProp] = row[aProp]; - }) - } - - var taStmt = aConnection.createStatement("SELECT id, minVersion, " + - "maxVersion FROM " + - "targetApplication WHERE " + - "addon_internal_id=:internal_id"); - - for (let location in migrateData) { - for (let id in migrateData[location]) { - taStmt.params.internal_id = migrateData[location][id].internal_id; - delete migrateData[location][id].internal_id; - for (let row of resultRows(taStmt)) { - migrateData[location][id].targetApplications.push({ - id: row.id, - minVersion: row.minVersion, - maxVersion: row.maxVersion - }); - } - } - } - } - catch (e) { - // An error here means the schema is too different to read - logger.error("Error migrating data", e); - return null; - } - finally { - if (taStmt) - taStmt.finalize(); - if (stmt) - stmt.finalize(); - } - - return migrateData; - }, - - /** - * Shuts down the database connection and releases all cached objects. - * Return: Promise{integer} resolves / rejects with the result of the DB - * flush after the database is flushed and - * all cleanup is done - */ - shutdown: function() { - logger.debug("shutdown"); - if (this.initialized) { - // If our last database I/O had an error, try one last time to save. - if (this.lastError) - this.saveChanges(); - - this.initialized = false; - - if (this._deferredSave) { - AddonManagerPrivate.recordSimpleMeasure( - "XPIDB_saves_total", this._deferredSave.totalSaves); - AddonManagerPrivate.recordSimpleMeasure( - "XPIDB_saves_overlapped", this._deferredSave.overlappedSaves); - AddonManagerPrivate.recordSimpleMeasure( - "XPIDB_saves_late", this._deferredSave.dirty ? 1 : 0); - } - - // Return a promise that any pending writes of the DB are complete and we - // are finished cleaning up - let flushPromise = this.flush(); - flushPromise.then(null, error => { - logger.error("Flush of XPI database failed", error); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_shutdownFlush_failed", 1); - // If our last attempt to read or write the DB failed, force a new - // extensions.ini to be written to disk on the next startup - Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); - }) - .then(count => { - // Clear out the cached addons data loaded from JSON - delete this.addonDB; - delete this._dbPromise; - // same for the deferred save - delete this._deferredSave; - // re-enable the schema version setter - delete this._schemaVersionSet; - }); - return flushPromise; - } - return Promise.resolve(0); - }, - - /** - * Asynchronously list all addons that match the filter function - * @param aFilter - * Function that takes an addon instance and returns - * true if that addon should be included in the selected array - * @param aCallback - * Called back with an array of addons matching aFilter - * or an empty array if none match - */ - getAddonList: function(aFilter, aCallback) { - this.asyncLoadDB().then( - addonDB => { - let addonList = _filterDB(addonDB, aFilter); - asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback)); - }) - .then(null, - error => { - logger.error("getAddonList failed", error); - makeSafe(aCallback)([]); - }); - }, - - /** - * (Possibly asynchronously) get the first addon that matches the filter function - * @param aFilter - * Function that takes an addon instance and returns - * true if that addon should be selected - * @param aCallback - * Called back with the addon, or null if no matching addon is found - */ - getAddon: function(aFilter, aCallback) { - return this.asyncLoadDB().then( - addonDB => { - getRepositoryAddon(_findAddon(addonDB, aFilter), makeSafe(aCallback)); - }) - .then(null, - error => { - logger.error("getAddon failed", error); - makeSafe(aCallback)(null); - }); - }, - - /** - * Asynchronously gets an add-on with a particular ID in a particular - * install location. - * - * @param aId - * The ID of the add-on to retrieve - * @param aLocation - * The name of the install location - * @param aCallback - * A callback to pass the DBAddonInternal to - */ - getAddonInLocation: function(aId, aLocation, aCallback) { - this.asyncLoadDB().then( - addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId), - makeSafe(aCallback))); - }, - - /** - * Asynchronously get all the add-ons in a particular install location. - * - * @param aLocation - * The name of the install location - * @param aCallback - * A callback to pass the array of DBAddonInternals to - */ - getAddonsInLocation: function(aLocation, aCallback) { - this.getAddonList(aAddon => aAddon._installLocation.name == aLocation, aCallback); - }, - - /** - * Asynchronously gets the add-on with the specified ID that is visible. - * - * @param aId - * The ID of the add-on to retrieve - * @param aCallback - * A callback to pass the DBAddonInternal to - */ - getVisibleAddonForID: function(aId, aCallback) { - this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible), - aCallback); - }, - - /** - * Asynchronously gets the visible add-ons, optionally restricting by type. - * - * @param aTypes - * An array of types to include or null to include all types - * @param aCallback - * A callback to pass the array of DBAddonInternals to - */ - getVisibleAddons: function(aTypes, aCallback) { - this.getAddonList(aAddon => (aAddon.visible && - (!aTypes || (aTypes.length == 0) || - (aTypes.indexOf(aAddon.type) > -1))), - aCallback); - }, - - /** - * Synchronously gets all add-ons of a particular type. - * - * @param aType - * The type of add-on to retrieve - * @return an array of DBAddonInternals - */ - getAddonsByType: function(aType) { - if (!this.addonDB) { - // jank-tastic! Must synchronously load DB if the theme switches from - // an XPI theme to a lightweight theme before the DB has loaded, - // because we're called from sync XPIProvider.addonChanged - logger.warn("Synchronous load of XPI database due to getAddonsByType(" + aType + ")"); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_byType", XPIProvider.runPhase); - this.syncLoadDB(true); - } - return _filterDB(this.addonDB, aAddon => (aAddon.type == aType)); - }, - - /** - * Synchronously gets an add-on with a particular internalName. - * - * @param aInternalName - * The internalName of the add-on to retrieve - * @return a DBAddonInternal - */ - getVisibleAddonForInternalName: function(aInternalName) { - if (!this.addonDB) { - // This may be called when the DB hasn't otherwise been loaded - logger.warn("Synchronous load of XPI database due to getVisibleAddonForInternalName"); - AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_forInternalName", - XPIProvider.runPhase); - this.syncLoadDB(true); - } - - return _findAddon(this.addonDB, - aAddon => aAddon.visible && - (aAddon.internalName == aInternalName)); - }, - - /** - * Asynchronously gets all add-ons with pending operations. - * - * @param aTypes - * The types of add-ons to retrieve or null to get all types - * @param aCallback - * A callback to pass the array of DBAddonInternal to - */ - getVisibleAddonsWithPendingOperations: function(aTypes, aCallback) { - this.getAddonList( - aAddon => (aAddon.visible && - (aAddon.pendingUninstall || - // Logic here is tricky. If we're active but disabled, - // we're pending disable; !active && !disabled, we're pending enable - (aAddon.active == aAddon.disabled)) && - (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))), - aCallback); - }, - - /** - * Asynchronously get an add-on by its Sync GUID. - * - * @param aGUID - * Sync GUID of add-on to fetch - * @param aCallback - * A callback to pass the DBAddonInternal record to. Receives null - * if no add-on with that GUID is found. - * - */ - getAddonBySyncGUID: function(aGUID, aCallback) { - this.getAddon(aAddon => aAddon.syncGUID == aGUID, - aCallback); - }, - - /** - * Synchronously gets all add-ons in the database. - * This is only called from the preference observer for the default - * compatibility version preference, so we can return an empty list if - * we haven't loaded the database yet. - * - * @return an array of DBAddonInternals - */ - getAddons: function() { - if (!this.addonDB) { - return []; - } - return _filterDB(this.addonDB, aAddon => true); - }, - - /** - * Synchronously adds an AddonInternal's metadata to the database. - * - * @param aAddon - * AddonInternal to add - * @param aDescriptor - * The file descriptor of the add-on - * @return The DBAddonInternal that was added to the database - */ - addAddonMetadata: function(aAddon, aDescriptor) { - if (!this.addonDB) { - AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_addMetadata", - XPIProvider.runPhase); - this.syncLoadDB(false); - } - - let newAddon = new DBAddonInternal(aAddon); - newAddon.descriptor = aDescriptor; - this.addonDB.set(newAddon._key, newAddon); - if (newAddon.visible) { - this.makeAddonVisible(newAddon); - } - - this.saveChanges(); - return newAddon; - }, - - /** - * Synchronously updates an add-on's metadata in the database. Currently just - * removes and recreates. - * - * @param aOldAddon - * The DBAddonInternal to be replaced - * @param aNewAddon - * The new AddonInternal to add - * @param aDescriptor - * The file descriptor of the add-on - * @return The DBAddonInternal that was added to the database - */ - updateAddonMetadata: function(aOldAddon, aNewAddon, aDescriptor) { - this.removeAddonMetadata(aOldAddon); - aNewAddon.syncGUID = aOldAddon.syncGUID; - aNewAddon.installDate = aOldAddon.installDate; - aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates; - aNewAddon.foreignInstall = aOldAddon.foreignInstall; - aNewAddon.seen = aOldAddon.seen; - aNewAddon.active = (aNewAddon.visible && !aNewAddon.disabled && !aNewAddon.pendingUninstall); - - // addAddonMetadata does a saveChanges() - return this.addAddonMetadata(aNewAddon, aDescriptor); - }, - - /** - * Synchronously removes an add-on from the database. - * - * @param aAddon - * The DBAddonInternal being removed - */ - removeAddonMetadata: function(aAddon) { - this.addonDB.delete(aAddon._key); - this.saveChanges(); - }, - - /** - * Synchronously marks a DBAddonInternal as visible marking all other - * instances with the same ID as not visible. - * - * @param aAddon - * The DBAddonInternal to make visible - */ - makeAddonVisible: function(aAddon) { - logger.debug("Make addon " + aAddon._key + " visible"); - for (let [, otherAddon] of this.addonDB) { - if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) { - logger.debug("Hide addon " + otherAddon._key); - otherAddon.visible = false; - otherAddon.active = false; - } - } - aAddon.visible = true; - this.saveChanges(); - }, - - /** - * Synchronously sets properties for an add-on. - * - * @param aAddon - * The DBAddonInternal being updated - * @param aProperties - * A dictionary of properties to set - */ - setAddonProperties: function(aAddon, aProperties) { - for (let key in aProperties) { - aAddon[key] = aProperties[key]; - } - this.saveChanges(); - }, - - /** - * Synchronously sets the Sync GUID for an add-on. - * Only called when the database is already loaded. - * - * @param aAddon - * The DBAddonInternal being updated - * @param aGUID - * GUID string to set the value to - * @throws if another addon already has the specified GUID - */ - setAddonSyncGUID: function(aAddon, aGUID) { - // Need to make sure no other addon has this GUID - function excludeSyncGUID(otherAddon) { - return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID); - } - let otherAddon = _findAddon(this.addonDB, excludeSyncGUID); - if (otherAddon) { - throw new Error("Addon sync GUID conflict for addon " + aAddon._key + - ": " + otherAddon._key + " already has GUID " + aGUID); - } - aAddon.syncGUID = aGUID; - this.saveChanges(); - }, - - /** - * Synchronously updates an add-on's active flag in the database. - * - * @param aAddon - * The DBAddonInternal to update - */ - updateAddonActive: function(aAddon, aActive) { - logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive); - - aAddon.active = aActive; - this.saveChanges(); - }, - - /** - * Synchronously calculates and updates all the active flags in the database. - */ - updateActiveAddons: function() { - if (!this.addonDB) { - logger.warn("updateActiveAddons called when DB isn't loaded"); - // force the DB to load - AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_updateActive", - XPIProvider.runPhase); - this.syncLoadDB(true); - } - logger.debug("Updating add-on states"); - for (let [, addon] of this.addonDB) { - let newActive = (addon.visible && !addon.disabled && !addon.pendingUninstall); - if (newActive != addon.active) { - addon.active = newActive; - this.saveChanges(); - } - } - }, - - /** - * Writes out the XPI add-ons list for the platform to read. - * @return true if the file was successfully updated, false otherwise - */ - writeAddonsList: function() { - if (!this.addonDB) { - // force the DB to load - AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_writeList", - XPIProvider.runPhase); - this.syncLoadDB(true); - } - Services.appinfo.invalidateCachesOnRestart(); - - let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], - true); - let enabledAddons = []; - let text = "[ExtensionDirs]\r\n"; - let count = 0; - let fullCount = 0; - - let activeAddons = _filterDB( - this.addonDB, - aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme")); - - for (let row of activeAddons) { - text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; - enabledAddons.push(encodeURIComponent(row.id) + ":" + - encodeURIComponent(row.version)); - } - fullCount += count; - - // The selected skin may come from an inactive theme (the default theme - // when a lightweight theme is applied for example) - text += "\r\n[ThemeDirs]\r\n"; - - let dssEnabled = false; - try { - dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED); - } catch (e) {} - - let themes = []; - if (dssEnabled) { - themes = _filterDB(this.addonDB, aAddon => aAddon.type == "theme"); - } - else { - let activeTheme = _findAddon( - this.addonDB, - aAddon => (aAddon.type == "theme") && - (aAddon.internalName == XPIProvider.selectedSkin)); - if (activeTheme) { - themes.push(activeTheme); - } - } - - if (themes.length > 0) { - count = 0; - for (let row of themes) { - text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; - enabledAddons.push(encodeURIComponent(row.id) + ":" + - encodeURIComponent(row.version)); - } - fullCount += count; - } - - text += "\r\n[MultiprocessIncompatibleExtensions]\r\n"; - - count = 0; - for (let row of activeAddons) { - if (!row.multiprocessCompatible) { - text += "Extension" + (count++) + "=" + row.id + "\r\n"; - } - } - - if (fullCount > 0) { - logger.debug("Writing add-ons list"); - - try { - let addonsListTmp = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST + ".tmp"], - true); - var fos = FileUtils.openFileOutputStream(addonsListTmp); - fos.write(text, text.length); - fos.close(); - addonsListTmp.moveTo(addonsListTmp.parent, FILE_XPI_ADDONS_LIST); - - Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(",")); - } - catch (e) { - logger.error("Failed to write add-ons list to profile directory", e); - return false; - } - } - else { - if (addonsList.exists()) { - logger.debug("Deleting add-ons list"); - try { - addonsList.remove(false); - } - catch (e) { - logger.error("Failed to remove " + addonsList.path, e); - return false; - } - } - - Services.prefs.clearUserPref(PREF_EM_ENABLED_ADDONS); - } - return true; - } -}; - -this.XPIDatabaseReconcile = { - /** - * Returns a map of ID -> add-on. When the same add-on ID exists in multiple - * install locations the highest priority location is chosen. - */ - flattenByID(addonMap, hideLocation) { - let map = new Map(); - - for (let installLocation of XPIProvider.installLocations) { - if (installLocation.name == hideLocation) - continue; - - let locationMap = addonMap.get(installLocation.name); - if (!locationMap) - continue; - - for (let [id, addon] of locationMap) { - if (!map.has(id)) - map.set(id, addon); - } - } - - return map; - }, - - /** - * Finds the visible add-ons from the map. - */ - getVisibleAddons(addonMap) { - let map = new Map(); - - for (let [location, addons] of addonMap) { - for (let [id, addon] of addons) { - if (!addon.visible) - continue; - - if (map.has(id)) { - logger.warn("Previous database listed more than one visible add-on with id " + id); - continue; - } - - map.set(id, addon); - } - } - - return map; - }, - - /** - * Called to add the metadata for an add-on in one of the install locations - * to the database. This can be called in three different cases. Either an - * add-on has been dropped into the location from outside of Firefox, or - * an add-on has been installed through the application, or the database - * has been upgraded or become corrupt and add-on data has to be reloaded - * into it. - * - * @param aInstallLocation - * The install location containing the add-on - * @param aId - * The ID of the add-on - * @param aAddonState - * The new state of the add-on - * @param aNewAddon - * The manifest for the new add-on if it has already been loaded - * @param aOldAppVersion - * The version of the application last run with this profile or null - * if it is a new profile or the version is unknown - * @param aOldPlatformVersion - * The version of the platform last run with this profile or null - * if it is a new profile or the version is unknown - * @param aMigrateData - * If during startup the database had to be upgraded this will - * contain data that used to be held about this add-on - * @return a boolean indicating if flushing caches is required to complete - * changing this add-on - */ - addMetadata(aInstallLocation, aId, aAddonState, aNewAddon, aOldAppVersion, - aOldPlatformVersion, aMigrateData) { - logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name); - - // If we had staged data for this add-on or we aren't recovering from a - // corrupt database and we don't have migration data for this add-on then - // this must be a new install. - let isNewInstall = (!!aNewAddon) || (!XPIDatabase.activeBundles && !aMigrateData); - - // If it's a new install and we haven't yet loaded the manifest then it - // must be something dropped directly into the install location - let isDetectedInstall = isNewInstall && !aNewAddon; - - // Load the manifest if necessary and sanity check the add-on ID - try { - if (!aNewAddon) { - // Load the manifest from the add-on. - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.persistentDescriptor = aAddonState.descriptor; - aNewAddon = syncLoadManifestFromFile(file, aInstallLocation); - } - // The add-on in the manifest should match the add-on ID. - if (aNewAddon.id != aId) { - throw new Error("Invalid addon ID: expected addon ID " + aId + - ", found " + aNewAddon.id + " in manifest"); - } - } - catch (e) { - logger.warn("addMetadata: Add-on " + aId + " is invalid", e); - - // Remove the invalid add-on from the install location if the install - // location isn't locked, no restart will be necessary - if (aInstallLocation.isLinkedAddon(aId)) - logger.warn("Not uninstalling invalid item because it is a proxy file"); - else if (aInstallLocation.locked) - logger.warn("Could not uninstall invalid item from locked install location"); - else - aInstallLocation.uninstallAddon(aId); - return null; - } - - // Update the AddonInternal properties. - aNewAddon.installDate = aAddonState.mtime; - aNewAddon.updateDate = aAddonState.mtime; - - // Assume that add-ons in the system add-ons install location aren't - // foreign and should default to enabled. - aNewAddon.foreignInstall = isDetectedInstall && - aInstallLocation.name != KEY_APP_SYSTEM_ADDONS && - aInstallLocation.name != KEY_APP_SYSTEM_DEFAULTS; - - // appDisabled depends on whether the add-on is a foreignInstall so update - aNewAddon.appDisabled = !isUsableAddon(aNewAddon); - - if (aMigrateData) { - // If there is migration data then apply it. - logger.debug("Migrating data from old database"); - - DB_MIGRATE_METADATA.forEach(function(aProp) { - // A theme's disabled state is determined by the selected theme - // preference which is read in loadManifestFromRDF - if (aProp == "userDisabled" && aNewAddon.type == "theme") - return; - - if (aProp in aMigrateData) - aNewAddon[aProp] = aMigrateData[aProp]; - }); - - // Force all non-profile add-ons to be foreignInstalls since they can't - // have been installed through the API - aNewAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE; - - // Some properties should only be migrated if the add-on hasn't changed. - // The version property isn't a perfect check for this but covers the - // vast majority of cases. - if (aMigrateData.version == aNewAddon.version) { - logger.debug("Migrating compatibility info"); - if ("targetApplications" in aMigrateData) - aNewAddon.applyCompatibilityUpdate(aMigrateData, true); - } - - // Since the DB schema has changed make sure softDisabled is correct - applyBlocklistChanges(aNewAddon, aNewAddon, aOldAppVersion, - aOldPlatformVersion); - } - - // The default theme is never a foreign install - if (aNewAddon.type == "theme" && aNewAddon.internalName == XPIProvider.defaultSkin) - aNewAddon.foreignInstall = false; - - if (isDetectedInstall && aNewAddon.foreignInstall) { - // If the add-on is a foreign install and is in a scope where add-ons - // that were dropped in should default to disabled then disable it - let disablingScopes = Preferences.get(PREF_EM_AUTO_DISABLED_SCOPES, 0); - if (aInstallLocation.scope & disablingScopes) { - logger.warn("Disabling foreign installed add-on " + aNewAddon.id + " in " - + aInstallLocation.name); - aNewAddon.userDisabled = true; - - // If we don't have an old app version then this is a new profile in - // which case just mark any sideloaded add-ons as already seen. - aNewAddon.seen = !aOldAppVersion; - } - } - - return XPIDatabase.addAddonMetadata(aNewAddon, aAddonState.descriptor); - }, - - /** - * Called when an add-on has been removed. - * - * @param aOldAddon - * The AddonInternal as it appeared the last time the application - * ran - * @return a boolean indicating if flushing caches is required to complete - * changing this add-on - */ - removeMetadata(aOldAddon) { - // This add-on has disappeared - logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location); - XPIDatabase.removeAddonMetadata(aOldAddon); - }, - - /** - * Updates an add-on's metadata and determines if a restart of the - * application is necessary. This is called when either the add-on's - * install directory path or last modified time has changed. - * - * @param aInstallLocation - * The install location containing the add-on - * @param aOldAddon - * The AddonInternal as it appeared the last time the application - * ran - * @param aAddonState - * The new state of the add-on - * @param aNewAddon - * The manifest for the new add-on if it has already been loaded - * @return a boolean indicating if flushing caches is required to complete - * changing this add-on - */ - updateMetadata(aInstallLocation, aOldAddon, aAddonState, aNewAddon) { - logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name); - - try { - // If there isn't an updated install manifest for this add-on then load it. - if (!aNewAddon) { - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.persistentDescriptor = aAddonState.descriptor; - aNewAddon = syncLoadManifestFromFile(file, aInstallLocation); - applyBlocklistChanges(aOldAddon, aNewAddon); - - // Carry over any pendingUninstall state to add-ons modified directly - // in the profile. This is important when the attempt to remove the - // add-on in processPendingFileChanges failed and caused an mtime - // change to the add-ons files. - aNewAddon.pendingUninstall = aOldAddon.pendingUninstall; - } - - // The ID in the manifest that was loaded must match the ID of the old - // add-on. - if (aNewAddon.id != aOldAddon.id) - throw new Error("Incorrect id in install manifest for existing add-on " + aOldAddon.id); - } - catch (e) { - logger.warn("updateMetadata: Add-on " + aOldAddon.id + " is invalid", e); - XPIDatabase.removeAddonMetadata(aOldAddon); - XPIStates.removeAddon(aOldAddon.location, aOldAddon.id); - if (!aInstallLocation.locked) - aInstallLocation.uninstallAddon(aOldAddon.id); - else - logger.warn("Could not uninstall invalid item from locked install location"); - - return null; - } - - // Set the additional properties on the new AddonInternal - aNewAddon.updateDate = aAddonState.mtime; - - // Update the database - return XPIDatabase.updateAddonMetadata(aOldAddon, aNewAddon, aAddonState.descriptor); - }, - - /** - * Updates an add-on's descriptor for when the add-on has moved in the - * filesystem but hasn't changed in any other way. - * - * @param aInstallLocation - * The install location containing the add-on - * @param aOldAddon - * The AddonInternal as it appeared the last time the application - * ran - * @param aAddonState - * The new state of the add-on - * @return a boolean indicating if flushing caches is required to complete - * changing this add-on - */ - updateDescriptor(aInstallLocation, aOldAddon, aAddonState) { - logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor); - aOldAddon.descriptor = aAddonState.descriptor; - aOldAddon._sourceBundle.persistentDescriptor = aAddonState.descriptor; - - return aOldAddon; - }, - - /** - * Called when no change has been detected for an add-on's metadata but the - * application has changed so compatibility may have changed. - * - * @param aInstallLocation - * The install location containing the add-on - * @param aOldAddon - * The AddonInternal as it appeared the last time the application - * ran - * @param aAddonState - * The new state of the add-on - * @param aOldAppVersion - * The version of the application last run with this profile or null - * if it is a new profile or the version is unknown - * @param aOldPlatformVersion - * The version of the platform last run with this profile or null - * if it is a new profile or the version is unknown - * @param aReloadMetadata - * A boolean which indicates whether metadata should be reloaded from - * the addon manifests. Default to false. - * @return the new addon. - */ - updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aOldAppVersion, - aOldPlatformVersion, aReloadMetadata) { - logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name); - - // If updating from a version of the app that didn't support signedState - // then fetch that property now - if (aOldAddon.signedState === undefined && ADDON_SIGNING && - SIGNED_TYPES.has(aOldAddon.type)) { - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.persistentDescriptor = aAddonState.descriptor; - let manifest = syncLoadManifestFromFile(file, aInstallLocation); - aOldAddon.signedState = manifest.signedState; - } - - // May be updating from a version of the app that didn't support all the - // properties of the currently-installed add-ons. - if (aReloadMetadata) { - let file = new nsIFile() - file.persistentDescriptor = aAddonState.descriptor; - let manifest = syncLoadManifestFromFile(file, aInstallLocation); - - // Avoid re-reading these properties from manifest, - // use existing addon instead. - // TODO - consider re-scanning for targetApplications. - let remove = ["syncGUID", "foreignInstall", "visible", "active", - "userDisabled", "applyBackgroundUpdates", "sourceURI", - "releaseNotesURI", "targetApplications"]; - - let props = PROP_JSON_FIELDS.filter(a => !remove.includes(a)); - copyProperties(manifest, props, aOldAddon); - } - - // This updates the addon's JSON cached data in place - applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion, - aOldPlatformVersion); - aOldAddon.appDisabled = !isUsableAddon(aOldAddon); - - return aOldAddon; - }, - - /** - * Compares the add-ons that are currently installed to those that were - * known to be installed when the application last ran and applies any - * changes found to the database. Also sends "startupcache-invalidate" signal to - * observerservice if it detects that data may have changed. - * Always called after XPIProviderUtils.js and extensions.json have been loaded. - * - * @param aManifests - * A dictionary of cached AddonInstalls for add-ons that have been - * installed - * @param aUpdateCompatibility - * true to update add-ons appDisabled property when the application - * version has changed - * @param aOldAppVersion - * The version of the application last run with this profile or null - * if it is a new profile or the version is unknown - * @param aOldPlatformVersion - * The version of the platform last run with this profile or null - * if it is a new profile or the version is unknown - * @param aSchemaChange - * The schema has changed and all add-on manifests should be re-read. - * @return a boolean indicating if a change requiring flushing the caches was - * detected - */ - processFileChanges(aManifests, aUpdateCompatibility, aOldAppVersion, aOldPlatformVersion, - aSchemaChange) { - let loadedManifest = (aInstallLocation, aId) => { - if (!(aInstallLocation.name in aManifests)) - return null; - if (!(aId in aManifests[aInstallLocation.name])) - return null; - return aManifests[aInstallLocation.name][aId]; - }; - - // Add-ons loaded from the database can have an uninitialized _sourceBundle - // if the descriptor was invalid. Swallow that error and say they don't exist. - let exists = (aAddon) => { - try { - return aAddon._sourceBundle.exists(); - } - catch (e) { - if (e.result == Cr.NS_ERROR_NOT_INITIALIZED) - return false; - throw e; - } - }; - - // Get the previous add-ons from the database and put them into maps by location - let previousAddons = new Map(); - for (let a of XPIDatabase.getAddons()) { - let locationAddonMap = previousAddons.get(a.location); - if (!locationAddonMap) { - locationAddonMap = new Map(); - previousAddons.set(a.location, locationAddonMap); - } - locationAddonMap.set(a.id, a); - } - - // Build the list of current add-ons into similar maps. When add-ons are still - // present we re-use the add-on objects from the database and update their - // details directly - let currentAddons = new Map(); - for (let installLocation of XPIProvider.installLocations) { - let locationAddonMap = new Map(); - currentAddons.set(installLocation.name, locationAddonMap); - - // Get all the on-disk XPI states for this location, and keep track of which - // ones we see in the database. - let states = XPIStates.getLocation(installLocation.name); - - // Iterate through the add-ons installed the last time the application - // ran - let dbAddons = previousAddons.get(installLocation.name); - if (dbAddons) { - for (let [id, oldAddon] of dbAddons) { - // Check if the add-on is still installed - let xpiState = states && states.get(id); - if (xpiState) { - // Here the add-on was present in the database and on disk - recordAddonTelemetry(oldAddon); - - // Check if the add-on has been changed outside the XPI provider - if (oldAddon.updateDate != xpiState.mtime) { - // Did time change in the wrong direction? - if (xpiState.mtime < oldAddon.updateDate) { - XPIProvider.setTelemetry(oldAddon.id, "olderFile", { - name: XPIProvider._mostRecentlyModifiedFile[id], - mtime: xpiState.mtime, - oldtime: oldAddon.updateDate - }); - } else { - XPIProvider.setTelemetry(oldAddon.id, "modifiedFile", - XPIProvider._mostRecentlyModifiedFile[id]); - } - } - - // The add-on has changed if the modification time has changed, if - // we have an updated manifest for it, or if the schema version has - // changed. - // - // Also reload the metadata for add-ons in the application directory - // when the application version has changed. - let newAddon = loadedManifest(installLocation, id); - if (newAddon || oldAddon.updateDate != xpiState.mtime || - (aUpdateCompatibility && (installLocation.name == KEY_APP_GLOBAL || - installLocation.name == KEY_APP_SYSTEM_DEFAULTS))) { - newAddon = this.updateMetadata(installLocation, oldAddon, xpiState, newAddon); - } - else if (oldAddon.descriptor != xpiState.descriptor) { - newAddon = this.updateDescriptor(installLocation, oldAddon, xpiState); - } - // Check compatility when the application version and/or schema - // version has changed. A schema change also reloads metadata from - // the manifests. - else if (aUpdateCompatibility || aSchemaChange) { - newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState, - aOldAppVersion, aOldPlatformVersion, - aSchemaChange); - } - else { - // No change - newAddon = oldAddon; - } - - if (newAddon) - locationAddonMap.set(newAddon.id, newAddon); - } - else { - // The add-on is in the DB, but not in xpiState (and thus not on disk). - this.removeMetadata(oldAddon); - } - } - } - - // Any add-on in our current location that we haven't seen needs to - // be added to the database. - // Get the migration data for this install location so we can include that as - // we add, in case this is a database upgrade or rebuild. - let locMigrateData = {}; - if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData) - locMigrateData = XPIDatabase.migrateData[installLocation.name]; - - if (states) { - for (let [id, xpiState] of states) { - if (locationAddonMap.has(id)) - continue; - let migrateData = id in locMigrateData ? locMigrateData[id] : null; - let newAddon = loadedManifest(installLocation, id); - let addon = this.addMetadata(installLocation, id, xpiState, newAddon, - aOldAppVersion, aOldPlatformVersion, migrateData); - if (addon) - locationAddonMap.set(addon.id, addon); - } - } - } - - // previousAddons may contain locations where the database contains add-ons - // but the browser is no longer configured to use that location. The metadata - // for those add-ons must be removed from the database. - for (let [locationName, addons] of previousAddons) { - if (!currentAddons.has(locationName)) { - for (let [id, oldAddon] of addons) - this.removeMetadata(oldAddon); - } - } - - // Validate the updated system add-ons - let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS]; - let addons = currentAddons.get(KEY_APP_SYSTEM_ADDONS) || new Map(); - - let hideLocation; - - if (!systemAddonLocation.isValid(addons)) { - // Hide the system add-on updates if any are invalid. - logger.info("One or more updated system add-ons invalid, falling back to defaults."); - hideLocation = KEY_APP_SYSTEM_ADDONS; - } - - let previousVisible = this.getVisibleAddons(previousAddons); - let currentVisible = this.flattenByID(currentAddons, hideLocation); - let sawActiveTheme = false; - XPIProvider.bootstrappedAddons = {}; - - // Pass over the new set of visible add-ons, record any changes that occured - // during startup and call bootstrap install/uninstall scripts as necessary - for (let [id, currentAddon] of currentVisible) { - let previousAddon = previousVisible.get(id); - - // Note if any visible add-on is not in the application install location - if (currentAddon._installLocation.name != KEY_APP_GLOBAL) - XPIProvider.allAppGlobal = false; - - let isActive = !currentAddon.disabled; - let wasActive = previousAddon ? previousAddon.active : currentAddon.active - - if (!previousAddon) { - // If we had a manifest for this add-on it was a staged install and - // so wasn't something recovered from a corrupt database - let wasStaged = !!loadedManifest(currentAddon._installLocation, id); - - // We might be recovering from a corrupt database, if so use the - // list of known active add-ons to update the new add-on - if (!wasStaged && XPIDatabase.activeBundles) { - // For themes we know which is active by the current skin setting - if (currentAddon.type == "theme") - isActive = currentAddon.internalName == XPIProvider.currentSkin; - else - isActive = XPIDatabase.activeBundles.indexOf(currentAddon.descriptor) != -1; - - // If the add-on wasn't active and it isn't already disabled in some way - // then it was probably either softDisabled or userDisabled - if (!isActive && !currentAddon.disabled) { - // If the add-on is softblocked then assume it is softDisabled - if (currentAddon.blocklistState == Blocklist.STATE_SOFTBLOCKED) - currentAddon.softDisabled = true; - else - currentAddon.userDisabled = true; - } - } - else { - // This is a new install - if (currentAddon.foreignInstall) - AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, id); - - if (currentAddon.bootstrap) { - AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, id); - // Visible bootstrapped add-ons need to have their install method called - XPIProvider.callBootstrapMethod(currentAddon, currentAddon._sourceBundle, - "install", BOOTSTRAP_REASONS.ADDON_INSTALL); - if (!isActive) - XPIProvider.unloadBootstrapScope(currentAddon.id); - } - } - } - else { - if (previousAddon !== currentAddon) { - // This is an add-on that has changed, either the metadata was reloaded - // or the version in a different location has become visible - AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, id); - - let installReason = Services.vc.compare(previousAddon.version, currentAddon.version) < 0 ? - BOOTSTRAP_REASONS.ADDON_UPGRADE : - BOOTSTRAP_REASONS.ADDON_DOWNGRADE; - - // If the previous add-on was in a different path, bootstrapped - // and still exists then call its uninstall method. - if (previousAddon.bootstrap && previousAddon._installLocation && - exists(previousAddon) && - currentAddon._sourceBundle.path != previousAddon._sourceBundle.path) { - - XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle, - "uninstall", installReason, - { newVersion: currentAddon.version }); - XPIProvider.unloadBootstrapScope(previousAddon.id); - } - - // Make sure to flush the cache when an old add-on has gone away - flushChromeCaches(); - - if (currentAddon.bootstrap) { - // Visible bootstrapped add-ons need to have their install method called - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - file.persistentDescriptor = currentAddon._sourceBundle.persistentDescriptor; - XPIProvider.callBootstrapMethod(currentAddon, file, - "install", installReason, - { oldVersion: previousAddon.version }); - if (currentAddon.disabled) - XPIProvider.unloadBootstrapScope(currentAddon.id); - } - } - - if (isActive != wasActive) { - let change = isActive ? AddonManager.STARTUP_CHANGE_ENABLED - : AddonManager.STARTUP_CHANGE_DISABLED; - AddonManagerPrivate.addStartupChange(change, id); - } - } - - XPIDatabase.makeAddonVisible(currentAddon); - currentAddon.active = isActive; - - // Make sure the bootstrap information is up to date for this ID - if (currentAddon.bootstrap && currentAddon.active) { - XPIProvider.bootstrappedAddons[id] = { - version: currentAddon.version, - type: currentAddon.type, - descriptor: currentAddon._sourceBundle.persistentDescriptor, - multiprocessCompatible: currentAddon.multiprocessCompatible, - runInSafeMode: canRunInSafeMode(currentAddon), - dependencies: currentAddon.dependencies, - hasEmbeddedWebExtension: currentAddon.hasEmbeddedWebExtension, - }; - } - - if (currentAddon.active && currentAddon.internalName == XPIProvider.selectedSkin) - sawActiveTheme = true; - } - - // Pass over the set of previously visible add-ons that have now gone away - // and record the change. - for (let [id, previousAddon] of previousVisible) { - if (currentVisible.has(id)) - continue; - - // This add-on vanished - - // If the previous add-on was bootstrapped and still exists then call its - // uninstall method. - if (previousAddon.bootstrap && exists(previousAddon)) { - XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle, - "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL); - XPIProvider.unloadBootstrapScope(previousAddon.id); - } - AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id); - - // Make sure to flush the cache when an old add-on has gone away - flushChromeCaches(); - } - - // Make sure add-ons from hidden locations are marked invisible and inactive - let locationAddonMap = currentAddons.get(hideLocation); - if (locationAddonMap) { - for (let addon of locationAddonMap.values()) { - addon.visible = false; - addon.active = false; - } - } - - // If a custom theme is selected and it wasn't seen in the new list of - // active add-ons then enable the default theme - if (XPIProvider.selectedSkin != XPIProvider.defaultSkin && !sawActiveTheme) { - logger.info("Didn't see selected skin " + XPIProvider.selectedSkin); - XPIProvider.enableDefaultTheme(); - } - - // Finally update XPIStates to match everything - for (let [locationName, locationAddonMap] of currentAddons) { - for (let [id, addon] of locationAddonMap) { - let xpiState = XPIStates.getAddon(locationName, id); - xpiState.syncWithDB(addon); - } - } - XPIStates.save(); - - XPIProvider.persistBootstrappedAddons(); - - // Clear out any cached migration data. - XPIDatabase.migrateData = null; - XPIDatabase.saveChanges(); - - return true; - }, -} diff --git a/toolkit/mozapps/webextensions/internal/moz.build b/toolkit/mozapps/webextensions/internal/moz.build deleted file mode 100644 index 8f1e4fea6..000000000 --- a/toolkit/mozapps/webextensions/internal/moz.build +++ /dev/null @@ -1,31 +0,0 @@ -# -*- 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/. - -EXTRA_JS_MODULES.addons += [ - '../../extensions/internal/AddonLogging.jsm', - '../../extensions/internal/Content.js', - '../../extensions/internal/ProductAddonChecker.jsm', - '../../extensions/internal/SpellCheckDictionaryBootstrap.js', - 'AddonRepository.jsm', - 'AddonRepository_SQLiteMigrator.jsm', - 'APIExtensionBootstrap.js', - 'GMPProvider.jsm', - 'LightweightThemeImageOptimizer.jsm', - 'WebExtensionBootstrap.js', - 'XPIProvider.jsm', - 'XPIProviderUtils.js', -] - -# Don't ship unused providers on Android -if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android': - EXTRA_JS_MODULES.addons += [ - 'PluginProvider.jsm', - ] - -EXTRA_PP_JS_MODULES.addons += [ - '../../extensions/internal/AddonUpdateChecker.jsm', - 'AddonConstants.jsm', -] diff --git a/toolkit/mozapps/webextensions/jar.mn b/toolkit/mozapps/webextensions/jar.mn deleted file mode 100644 index 0c63396cc..000000000 --- a/toolkit/mozapps/webextensions/jar.mn +++ /dev/null @@ -1,35 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -toolkit.jar: -#ifndef MOZ_FENNEC -% content mozapps %content/mozapps/ -* content/mozapps/extensions/extensions.xul (content/extensions.xul) - content/mozapps/extensions/extensions.css (content/extensions.css) -* content/mozapps/extensions/extensions.js (content/extensions.js) -* content/mozapps/extensions/extensions.xml (content/extensions.xml) - content/mozapps/extensions/updateinfo.xsl (../extensions/content/updateinfo.xsl) - content/mozapps/extensions/about.xul (../extensions/content/about.xul) - content/mozapps/extensions/about.js (content/about.js) - content/mozapps/extensions/list.xul (../extensions/content/list.xul) - content/mozapps/extensions/list.js (../extensions/content/list.js) - content/mozapps/extensions/blocklist.xul (../extensions/content/blocklist.xul) - content/mozapps/extensions/blocklist.js (../extensions/content/blocklist.js) - content/mozapps/extensions/blocklist.css (../extensions/content/blocklist.css) - content/mozapps/extensions/blocklist.xml (../extensions/content/blocklist.xml) -* content/mozapps/extensions/update.xul (content/update.xul) - content/mozapps/extensions/update.js (content/update.js) - content/mozapps/extensions/eula.xul (../extensions/content/eula.xul) - content/mozapps/extensions/eula.js (content/eula.js) - content/mozapps/extensions/newaddon.xul (content/newaddon.xul) -* content/mozapps/extensions/newaddon.js (../extensions/content/newaddon.js) - content/mozapps/extensions/pluginPrefs.xul (../extensions/content/pluginPrefs.xul) - content/mozapps/extensions/gmpPrefs.xul (../extensions/content/gmpPrefs.xul) - content/mozapps/extensions/OpenH264-license.txt (../extensions/content/OpenH264-license.txt) -#endif - content/mozapps/extensions/setting.xml (content/setting.xml) - content/mozapps/xpinstall/xpinstallConfirm.xul (../extensions/content/xpinstallConfirm.xul) - content/mozapps/xpinstall/xpinstallConfirm.js (../extensions/content/xpinstallConfirm.js) - content/mozapps/xpinstall/xpinstallConfirm.css (../extensions/content/xpinstallConfirm.css) - content/mozapps/xpinstall/xpinstallItem.xml (../extensions/content/xpinstallItem.xml) diff --git a/toolkit/mozapps/webextensions/moz.build b/toolkit/mozapps/webextensions/moz.build deleted file mode 100644 index e703125e6..000000000 --- a/toolkit/mozapps/webextensions/moz.build +++ /dev/null @@ -1,55 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -DIRS += ['internal'] - -XPIDL_SOURCES += [ - '../extensions/amIAddonManager.idl', - '../extensions/amIAddonPathService.idl', - '../extensions/amIWebInstaller.idl', - '../extensions/amIWebInstallListener.idl', -] - -XPIDL_MODULE = 'extensions' - -EXTRA_COMPONENTS += [ - '../extensions/amContentHandler.js', - 'addonManager.js', - 'amInstallTrigger.js', - 'amWebAPI.js', - 'amWebInstallListener.js', -] - -EXTRA_PP_COMPONENTS += [ - 'extensions.manifest', -] - -EXTRA_JS_MODULES += [ - '../extensions/ChromeManifestParser.jsm', - '../extensions/DeferredSave.jsm', - 'AddonManager.jsm', - 'LightweightThemeManager.jsm', -] - -JAR_MANIFESTS += ['jar.mn'] - -EXPORTS.mozilla += [ - 'AddonContentPolicy.h', - 'AddonManagerWebAPI.h', - 'AddonPathService.h', -] - -UNIFIED_SOURCES += [ - 'AddonContentPolicy.cpp', - 'AddonManagerWebAPI.cpp', - 'AddonPathService.cpp', -] - -LOCAL_INCLUDES += [ - '/dom/base', -] - -FINAL_LIBRARY = 'xul' diff --git a/toolkit/themes/linux/mozapps/jar.mn b/toolkit/themes/linux/mozapps/jar.mn index 37325c0be..0931d1823 100644 --- a/toolkit/themes/linux/mozapps/jar.mn +++ b/toolkit/themes/linux/mozapps/jar.mn @@ -6,23 +6,6 @@ toolkit.jar: #include ../../shared/mozapps.inc.mn skin/classic/mozapps/downloads/downloadIcon.png (downloads/downloadIcon.png) skin/classic/mozapps/downloads/downloads.css (downloads/downloads.css) -#ifdef MOZ_WEBEXTENSIONS -* skin/classic/mozapps/extensions/extensions.css (webextensions/extensions.css) - skin/classic/mozapps/extensions/category-search.png (webextensions/category-search.png) - skin/classic/mozapps/extensions/category-discover.png (webextensions/category-discover.png) - skin/classic/mozapps/extensions/category-plugins.png (webextensions/category-plugins.png) - skin/classic/mozapps/extensions/category-service.png (webextensions/category-service.png) - skin/classic/mozapps/extensions/category-recent.png (webextensions/category-recent.png) - skin/classic/mozapps/extensions/category-available.png (webextensions/category-available.png) - skin/classic/mozapps/extensions/extensionGeneric-16.png (webextensions/extensionGeneric-16.png) - skin/classic/mozapps/extensions/dictionaryGeneric.png (webextensions/dictionaryGeneric.png) - skin/classic/mozapps/extensions/dictionaryGeneric-16.png (webextensions/dictionaryGeneric-16.png) - skin/classic/mozapps/extensions/themeGeneric.png (webextensions/themeGeneric.png) - skin/classic/mozapps/extensions/themeGeneric-16.png (webextensions/themeGeneric-16.png) - skin/classic/mozapps/extensions/localeGeneric.png (webextensions/localeGeneric.png) -* skin/classic/mozapps/extensions/newaddon.css (webextensions/newaddon.css) - skin/classic/mozapps/extensions/heart.png (webextensions/heart.png) -#else skin/classic/mozapps/extensions/extensions.css (extensions/extensions.css) skin/classic/mozapps/extensions/category-search.png (extensions/category-search.png) skin/classic/mozapps/extensions/category-discover.png (extensions/category-discover.png) @@ -46,7 +29,6 @@ toolkit.jar: skin/classic/mozapps/extensions/newaddon.css (extensions/newaddon.css) skin/classic/mozapps/extensions/selectAddons.css (extensions/selectAddons.css) skin/classic/mozapps/xpinstall/xpinstallItemGeneric.png (extensions/extensionGeneric.png) -#endif skin/classic/mozapps/passwordmgr/key.png (passwordmgr/key.png) skin/classic/mozapps/passwordmgr/key-16.png (passwordmgr/key-16.png) skin/classic/mozapps/passwordmgr/key-64.png (passwordmgr/key-64.png) diff --git a/toolkit/themes/linux/mozapps/webextensions/category-available.png b/toolkit/themes/linux/mozapps/webextensions/category-available.png Binary files differdeleted file mode 100644 index 689d526c9..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/category-available.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/category-discover.png b/toolkit/themes/linux/mozapps/webextensions/category-discover.png Binary files differdeleted file mode 100644 index ccea27524..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/category-discover.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/category-plugins.png b/toolkit/themes/linux/mozapps/webextensions/category-plugins.png Binary files differdeleted file mode 100644 index b253dd08f..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/category-plugins.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/category-recent.png b/toolkit/themes/linux/mozapps/webextensions/category-recent.png Binary files differdeleted file mode 100644 index 9039b27aa..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/category-recent.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/category-search.png b/toolkit/themes/linux/mozapps/webextensions/category-search.png Binary files differdeleted file mode 100644 index 52e91a7ce..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/category-search.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/category-service.png b/toolkit/themes/linux/mozapps/webextensions/category-service.png Binary files differdeleted file mode 100644 index 997c8541c..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/category-service.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/dictionaryGeneric-16.png b/toolkit/themes/linux/mozapps/webextensions/dictionaryGeneric-16.png Binary files differdeleted file mode 100644 index 08a0447a4..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/dictionaryGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/dictionaryGeneric.png b/toolkit/themes/linux/mozapps/webextensions/dictionaryGeneric.png Binary files differdeleted file mode 100644 index a1e0d5359..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/dictionaryGeneric.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/extensionGeneric-16.png b/toolkit/themes/linux/mozapps/webextensions/extensionGeneric-16.png Binary files differdeleted file mode 100644 index b1a2f3652..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/extensionGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/extensions.css b/toolkit/themes/linux/mozapps/webextensions/extensions.css deleted file mode 100644 index 5a11df119..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/extensions.css +++ /dev/null @@ -1,42 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -%include ../../../shared/webextensions/extensions.inc.css - -#header-utils-btn .toolbarbutton-icon { - list-style-image: url("chrome://mozapps/skin/extensions/utilities.svg#utilities-native"); -} - -.sorter[checkState="1"] .button-icon { - display: -moz-box; - list-style-image: url("moz-icon://stock/gtk-sort-descending?size=16"); -} - -.sorter[checkState="2"] .button-icon { - display: -moz-box; - list-style-image: url("moz-icon://stock/gtk-sort-ascending?size=16"); -} - -.addon .relnotes-toggle { - list-style-image: url("moz-icon://stock/gtk-go-down?size=16"); -} - -.addon .relnotes-toggle > .button-box > .button-icon { - display: -moz-box; -} - -.addon[show-relnotes] .relnotes-toggle { - list-style-image: url("moz-icon://stock/gtk-go-up?size=16"); -} - -.meta-rating[showrating="average"] > .star { - list-style-image: url("chrome://mozapps/skin/extensions/rating-not-won.png"); - padding: 0 1px; -} - -.meta-rating > .star[on="true"], -.meta-rating[showrating="user"] > .star[hover] { - list-style-image: url("chrome://mozapps/skin/extensions/rating-won.png"); - padding: 0 1px; -} diff --git a/toolkit/themes/linux/mozapps/webextensions/heart.png b/toolkit/themes/linux/mozapps/webextensions/heart.png Binary files differdeleted file mode 100644 index 655f4c4be..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/heart.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/localeGeneric.png b/toolkit/themes/linux/mozapps/webextensions/localeGeneric.png Binary files differdeleted file mode 100644 index c72115906..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/localeGeneric.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/newaddon.css b/toolkit/themes/linux/mozapps/webextensions/newaddon.css deleted file mode 100644 index 5856c08b5..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/newaddon.css +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -%include ../../../shared/webextensions/newaddon.inc.css diff --git a/toolkit/themes/linux/mozapps/webextensions/themeGeneric-16.png b/toolkit/themes/linux/mozapps/webextensions/themeGeneric-16.png Binary files differdeleted file mode 100644 index 019886fea..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/themeGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/linux/mozapps/webextensions/themeGeneric.png b/toolkit/themes/linux/mozapps/webextensions/themeGeneric.png Binary files differdeleted file mode 100644 index cde1c7834..000000000 --- a/toolkit/themes/linux/mozapps/webextensions/themeGeneric.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/jar.mn b/toolkit/themes/osx/mozapps/jar.mn index f8ade11d9..eabd2eddb 100644 --- a/toolkit/themes/osx/mozapps/jar.mn +++ b/toolkit/themes/osx/mozapps/jar.mn @@ -8,36 +8,6 @@ toolkit.jar: skin/classic/mozapps/downloads/downloadIcon.png (downloads/downloadIcon.png) * skin/classic/mozapps/downloads/downloads.css (downloads/downloads.css) skin/classic/mozapps/downloads/unknownContentType.css (downloads/unknownContentType.css) -#ifdef MOZ_WEBEXTENSIONS - skin/classic/mozapps/extensions/category-search.png (webextensions/category-search.png) - skin/classic/mozapps/extensions/category-discover.png (webextensions/category-discover.png) - skin/classic/mozapps/extensions/category-plugins.png (webextensions/category-plugins.png) - skin/classic/mozapps/extensions/category-service.png (webextensions/category-service.png) - skin/classic/mozapps/extensions/category-dictionaries.png (webextensions/category-dictionaries.png) - skin/classic/mozapps/extensions/category-experiments.png (webextensions/category-experiments.png) - skin/classic/mozapps/extensions/category-recent.png (webextensions/category-recent.png) - skin/classic/mozapps/extensions/category-available.png (webextensions/category-available.png) - skin/classic/mozapps/extensions/discover-logo.png (webextensions/discover-logo.png) - skin/classic/mozapps/extensions/extensionGeneric-16.png (webextensions/extensionGeneric-16.png) - skin/classic/mozapps/extensions/themeGeneric.png (webextensions/themeGeneric.png) - skin/classic/mozapps/extensions/themeGeneric-16.png (webextensions/themeGeneric-16.png) - skin/classic/mozapps/extensions/dictionaryGeneric.png (webextensions/dictionaryGeneric.png) - skin/classic/mozapps/extensions/dictionaryGeneric-16.png (webextensions/dictionaryGeneric-16.png) - skin/classic/mozapps/extensions/experimentGeneric.png (webextensions/experimentGeneric.png) - skin/classic/mozapps/extensions/localeGeneric.png (webextensions/localeGeneric.png) - skin/classic/mozapps/extensions/rating-won.png (webextensions/rating-won.png) - skin/classic/mozapps/extensions/rating-not-won.png (webextensions/rating-not-won.png) - skin/classic/mozapps/extensions/cancel.png (webextensions/cancel.png) - skin/classic/mozapps/extensions/toolbarbutton-dropmarker.png (webextensions/toolbarbutton-dropmarker.png) - skin/classic/mozapps/extensions/heart.png (webextensions/heart.png) - skin/classic/mozapps/extensions/search.png (webextensions/search.png) - skin/classic/mozapps/extensions/about.css (webextensions/about.css) -* skin/classic/mozapps/extensions/extensions.css (webextensions/extensions.css) - skin/classic/mozapps/extensions/update.css (webextensions/update.css) - skin/classic/mozapps/extensions/eula.css (webextensions/eula.css) - skin/classic/mozapps/extensions/blocklist.css (webextensions/blocklist.css) -* skin/classic/mozapps/extensions/newaddon.css (webextensions/newaddon.css) -#else skin/classic/mozapps/extensions/category-search.png (extensions/category-search.png) skin/classic/mozapps/extensions/category-discover.png (extensions/category-discover.png) skin/classic/mozapps/extensions/category-languages.png (extensions/localeGeneric.png) @@ -82,7 +52,6 @@ toolkit.jar: skin/classic/mozapps/extensions/eula.css (extensions/eula.css) skin/classic/mozapps/extensions/blocklist.css (extensions/blocklist.css) * skin/classic/mozapps/extensions/newaddon.css (extensions/newaddon.css) -#endif skin/classic/mozapps/passwordmgr/key.png (passwordmgr/key.png) skin/classic/mozapps/passwordmgr/key-16.png (passwordmgr/key-16.png) skin/classic/mozapps/passwordmgr/key-64.png (passwordmgr/key-64.png) @@ -98,12 +67,8 @@ toolkit.jar: skin/classic/mozapps/update/buttons.png (update/buttons.png) * skin/classic/mozapps/update/updates.css (update/updates.css) skin/classic/mozapps/viewsource/viewsource.css (viewsource/viewsource.css) -#ifdef MOZ_WEBEXTENSIONS - skin/classic/mozapps/xpinstall/xpinstallConfirm.css (webextensions/xpinstallConfirm.css) -#else skin/classic/mozapps/xpinstall/xpinstallItemGeneric.png (extensions/extensionGeneric.png) skin/classic/mozapps/xpinstall/xpinstallConfirm.css (extensions/xpinstallConfirm.css) -#endif skin/classic/mozapps/handling/handling.css (handling/handling.css) #ifdef MOZ_PHOENIX @@ -111,9 +76,4 @@ toolkit.jar: #elif MOZ_SEPARATE_MANIFEST_FOR_THEME_OVERRIDES [extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: #endif -#ifdef MOZ_WEBEXTENSIONS -% override chrome://mozapps/skin/extensions/category-extensions.svg chrome://mozapps/skin/extensions/extensionGeneric.svg -% override chrome://mozapps/skin/extensions/category-languages.png chrome://mozapps/skin/extensions/localeGeneric.png -% override chrome://mozapps/skin/extensions/category-themes.png chrome://mozapps/skin/extensions/themeGeneric.png -#endif % override chrome://mozapps/skin/plugins/notifyPluginCrashed.png chrome://mozapps/skin/plugins/notifyPluginGeneric.png diff --git a/toolkit/themes/osx/mozapps/webextensions/about.css b/toolkit/themes/osx/mozapps/webextensions/about.css deleted file mode 100644 index c13ce5f59..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/about.css +++ /dev/null @@ -1,78 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#genericAbout { - padding: 0px; - min-height: 200px; - max-height: 400px; - width: 30em; -} - -#clientBox { - background-color: -moz-Dialog; - color: -moz-DialogText; -} - -.basic-info { - padding: 10px; -} - -#extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/extensionGeneric.svg"); - max-width: 64px; - max-height: 64px; - margin-inline-end: 6px; -} - -#genericAbout[addontype="theme"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/themeGeneric.png"); -} - -#genericAbout[addontype="locale"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/localeGeneric.png"); -} - -#genericAbout[addontype="plugin"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png"); -} - -#genericAbout[addontype="dictionary"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png"); -} - -#extensionName { - font-size: 200%; - font-weight: bolder; -} - -#extensionVersion { - font-weight: bold; -} - -#extensionDescription { - margin-top: 4px; -} - -#groove { - margin-top: 8px; -} - -#extensionDetailsBox { - overflow: auto; - min-height: 100px; -} - -.boxIndent { - margin-inline-start: 18px; -} - -#extensionCreator, .contributor { - margin: 0px; -} - -.sectionTitle { - padding: 2px 0px 3px 0px; - margin-top: 3px; - font-weight: bold; -} diff --git a/toolkit/themes/osx/mozapps/webextensions/blocklist.css b/toolkit/themes/osx/mozapps/webextensions/blocklist.css deleted file mode 100644 index 02f6e1d49..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/blocklist.css +++ /dev/null @@ -1,20 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -richlistitem { - padding-top: 6px; - padding-bottom: 6px; - padding-inline-start: 7px; - padding-inline-end: 7px; - border-bottom: 1px solid #C0C0C0; -} - -.addon-name-version { - font-size: 110%; -} - -.blockedLabel { - font-weight: bold; - font-style: italic; -} diff --git a/toolkit/themes/osx/mozapps/webextensions/cancel.png b/toolkit/themes/osx/mozapps/webextensions/cancel.png Binary files differdeleted file mode 100644 index 0d98ab235..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/cancel.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-available.png b/toolkit/themes/osx/mozapps/webextensions/category-available.png Binary files differdeleted file mode 100644 index d1b737ab0..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-available.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-dictionaries.png b/toolkit/themes/osx/mozapps/webextensions/category-dictionaries.png Binary files differdeleted file mode 100644 index 54ae4f93f..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-dictionaries.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-discover.png b/toolkit/themes/osx/mozapps/webextensions/category-discover.png Binary files differdeleted file mode 100644 index a6f5b49b3..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-discover.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-experiments.png b/toolkit/themes/osx/mozapps/webextensions/category-experiments.png Binary files differdeleted file mode 100644 index a9d00545e..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-experiments.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-plugins.png b/toolkit/themes/osx/mozapps/webextensions/category-plugins.png Binary files differdeleted file mode 100644 index 5c4d8bf47..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-plugins.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-recent.png b/toolkit/themes/osx/mozapps/webextensions/category-recent.png Binary files differdeleted file mode 100644 index 7ecfc7d4c..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-recent.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-search.png b/toolkit/themes/osx/mozapps/webextensions/category-search.png Binary files differdeleted file mode 100644 index 52e91a7ce..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-search.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/category-service.png b/toolkit/themes/osx/mozapps/webextensions/category-service.png Binary files differdeleted file mode 100644 index 997c8541c..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/category-service.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/dictionaryGeneric-16.png b/toolkit/themes/osx/mozapps/webextensions/dictionaryGeneric-16.png Binary files differdeleted file mode 100644 index 4ad1a1a82..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/dictionaryGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/dictionaryGeneric.png b/toolkit/themes/osx/mozapps/webextensions/dictionaryGeneric.png Binary files differdeleted file mode 100644 index 54ae4f93f..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/dictionaryGeneric.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/discover-logo.png b/toolkit/themes/osx/mozapps/webextensions/discover-logo.png Binary files differdeleted file mode 100644 index cd50735a8..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/discover-logo.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/eula.css b/toolkit/themes/osx/mozapps/webextensions/eula.css deleted file mode 100644 index 05aeb3c1c..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/eula.css +++ /dev/null @@ -1,47 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#icon { - list-style-image: url("chrome://mozapps/skin/extensions/extensionGeneric.svg"); - max-width: 48px; - max-height: 48px; - margin-inline-end: 6px; -} - -#eula-dialog[addontype="theme"] #icon { - list-style-image: url("chrome://mozapps/skin/extensions/themeGeneric.png"); -} - -#eula-dialog[addontype="locale"] #icon { - list-style-image: url("chrome://mozapps/skin/extensions/localeGeneric.png"); -} - -#eula-dialog[addontype="plugin"] #icon { - list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png"); -} - -#eula-dialog[addontype="dictionary"] #icon { - list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png"); -} - -#heading-container { - -moz-box-align: center; -} - -#heading { - font-size: 120%; -} - -#eula { - -moz-appearance: none; - color: -moz-FieldText; - background-color: -moz-Field; - margin: 1em; - border: 1px solid; - -moz-border-top-colors: ActiveBorder; - -moz-border-right-colors: ActiveBorder; - -moz-border-bottom-colors: ActiveBorder; - -moz-border-left-colors: ActiveBorder; -} - diff --git a/toolkit/themes/osx/mozapps/webextensions/experimentGeneric.png b/toolkit/themes/osx/mozapps/webextensions/experimentGeneric.png Binary files differdeleted file mode 100644 index a9d00545e..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/experimentGeneric.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/extensionGeneric-16.png b/toolkit/themes/osx/mozapps/webextensions/extensionGeneric-16.png Binary files differdeleted file mode 100644 index fc6c8a258..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/extensionGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/extensions.css b/toolkit/themes/osx/mozapps/webextensions/extensions.css deleted file mode 100644 index cbdd9a5b0..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/extensions.css +++ /dev/null @@ -1,51 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -%include ../../../shared/webextensions/extensions.inc.css - -.no-auto-hide > .menulist-dropmarker { - padding-inline-start: 0px !important; -} - -#header-utils-btn { - list-style-image: url("chrome://mozapps/skin/extensions/utilities.svg#utilities"); - margin-inline-end: 18px; -} - -#header-utils-btn > .toolbarbutton-menu-dropmarker { - list-style-image: url("chrome://mozapps/skin/extensions/toolbarbutton-dropmarker.png"); - padding: 0; - margin-inline-start: 2px; -} - -.sorter[checkState="1"] { - list-style-image: url("chrome://global/skin/arrow/arrow-dn.gif"); -} - -.sorter[checkState="2"] { - list-style-image: url("chrome://global/skin/arrow/arrow-up.gif"); -} - -.addon .relnotes-toggle { - -moz-box-direction: reverse; - list-style-image: url("chrome://global/skin/arrow/arrow-dn.gif"); -} - -.addon[show-relnotes] .relnotes-toggle { - list-style-image: url("chrome://global/skin/arrow/arrow-up.gif"); -} - -.download-progress { - margin-top: 3px; - margin-bottom: 3px; -} - -.meta-rating > .star { - list-style-image: url("chrome://mozapps/skin/extensions/rating-not-won.png"); - padding: 0 1px; -} - -.meta-rating > .star[on="true"] { - list-style-image: url("chrome://mozapps/skin/extensions/rating-won.png"); -} diff --git a/toolkit/themes/osx/mozapps/webextensions/heart.png b/toolkit/themes/osx/mozapps/webextensions/heart.png Binary files differdeleted file mode 100644 index 655f4c4be..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/heart.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/localeGeneric.png b/toolkit/themes/osx/mozapps/webextensions/localeGeneric.png Binary files differdeleted file mode 100644 index 4d9ac5ad8..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/localeGeneric.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/newaddon.css b/toolkit/themes/osx/mozapps/webextensions/newaddon.css deleted file mode 100644 index 5856c08b5..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/newaddon.css +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -%include ../../../shared/webextensions/newaddon.inc.css diff --git a/toolkit/themes/osx/mozapps/webextensions/rating-not-won.png b/toolkit/themes/osx/mozapps/webextensions/rating-not-won.png Binary files differdeleted file mode 100644 index 2761f1925..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/rating-not-won.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/rating-won.png b/toolkit/themes/osx/mozapps/webextensions/rating-won.png Binary files differdeleted file mode 100644 index 336dd8f6e..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/rating-won.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/search.png b/toolkit/themes/osx/mozapps/webextensions/search.png Binary files differdeleted file mode 100644 index 93196dbbf..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/search.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/themeGeneric-16.png b/toolkit/themes/osx/mozapps/webextensions/themeGeneric-16.png Binary files differdeleted file mode 100644 index 190bb30d7..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/themeGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/themeGeneric.png b/toolkit/themes/osx/mozapps/webextensions/themeGeneric.png Binary files differdeleted file mode 100644 index be645f76d..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/themeGeneric.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/toolbarbutton-dropmarker.png b/toolkit/themes/osx/mozapps/webextensions/toolbarbutton-dropmarker.png Binary files differdeleted file mode 100644 index e7674c62a..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/toolbarbutton-dropmarker.png +++ /dev/null diff --git a/toolkit/themes/osx/mozapps/webextensions/update.css b/toolkit/themes/osx/mozapps/webextensions/update.css deleted file mode 100644 index a0a016868..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/update.css +++ /dev/null @@ -1,28 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.throbber { - list-style-image: url("chrome://global/skin/icons/loading.png"); - width: 16px; - height: 16px; - margin-top: 5px; - margin-bottom: 5px; - margin-inline-start: 5px; - margin-inline-end: 2px; -} - -@media (min-resolution: 2dppx) { - .throbber { - list-style-image: url("chrome://global/skin/icons/loading@2x.png"); - } -} - -.alertBox { - background-color: InfoBackground; - color: InfoText; - border: 1px outset InfoBackground; - margin-left: 3px; - margin-right: 3px; - padding: 5px; -} diff --git a/toolkit/themes/osx/mozapps/webextensions/xpinstallConfirm.css b/toolkit/themes/osx/mozapps/webextensions/xpinstallConfirm.css deleted file mode 100644 index 48af7c1a9..000000000 --- a/toolkit/themes/osx/mozapps/webextensions/xpinstallConfirm.css +++ /dev/null @@ -1,90 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#xpinstallheader { - margin-bottom: 2em; -} - -.alert-icon { - width: 48px; - height: 48px; - list-style-image: url("chrome://global/skin/icons/warning-large.png"); - margin-top: 0 !important; - margin-bottom: 6px !important; - margin-inline-start: 6px !important; - margin-inline-end: 20px !important; -} - -#itemList { - -moz-appearance: listbox; - margin: 3px 4px 10px 4px; - height: 14em; -} - -#itemWarningIntro { - margin-inline-start: 8px; -} - -#dialogContentBox { - padding: 5px; -} - -installitem { - padding: 5px 0 5px 5px; - border-bottom: 1px dotted #C0C0C0; - margin-bottom: 5px; -} - -.warning { - font-weight: bold; - font-size: 1.25em; - margin-bottom: 1em; -} - -.xpinstallIconContainer { - width: 32px; - height: 32px; - margin-inline-end: 5px; -} - -.xpinstallItemName { - font-weight: bold; -} - -.xpinstallItemSigned { - font-style: italic; - font-size: 0.9em; -} - -.xpinstallItemURL { - -moz-appearance: none; - border: none; - background-color: Window; - margin-top: 2px; - margin-bottom: 1px; - margin-inline-start: 6px; - margin-inline-end: 5px; -} - -.xpinstallItemIcon { - max-width: 32px; - max-height: 32px; - list-style-image: url("chrome://mozapps/skin/extensions/extensionGeneric.svg"); -} - -installitem[type="theme"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/extensions/themeGeneric.png"); -} - -installitem[type="locale"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/extensions/localeGeneric.png"); -} - -installitem[type="plugin"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png"); -} - -installitem[type="dictionary"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png"); -} diff --git a/toolkit/themes/shared/non-mac.jar.inc.mn b/toolkit/themes/shared/non-mac.jar.inc.mn index 23f9da519..b17e9d7ba 100644 --- a/toolkit/themes/shared/non-mac.jar.inc.mn +++ b/toolkit/themes/shared/non-mac.jar.inc.mn @@ -36,10 +36,12 @@ skin/classic/global/arrow/arrow-down.png (../../windows/global/arrow/arrow-down.png) skin/classic/global/arrow/arrow-lft.gif (../../windows/global/arrow/arrow-lft.gif) skin/classic/global/arrow/arrow-lft-dis.gif (../../windows/global/arrow/arrow-lft-dis.gif) + skin/classic/global/arrow/arrow-lft-hov.gif (../../windows/global/arrow/arrow-lft-hov.gif) skin/classic/global/arrow/arrow-lft-sharp.gif (../../windows/global/arrow/arrow-lft-sharp.gif) skin/classic/global/arrow/arrow-lft-sharp-end.gif (../../windows/global/arrow/arrow-lft-sharp-end.gif) skin/classic/global/arrow/arrow-rit.gif (../../windows/global/arrow/arrow-rit.gif) skin/classic/global/arrow/arrow-rit-dis.gif (../../windows/global/arrow/arrow-rit-dis.gif) + skin/classic/global/arrow/arrow-rit-hov.gif (../../windows/global/arrow/arrow-rit-hov.gif) skin/classic/global/arrow/arrow-rit-sharp.gif (../../windows/global/arrow/arrow-rit-sharp.gif) skin/classic/global/arrow/arrow-rit-sharp-end.gif (../../windows/global/arrow/arrow-rit-sharp-end.gif) skin/classic/global/arrow/arrow-up.gif (../../windows/global/arrow/arrow-up.gif) @@ -109,40 +111,15 @@ skin/classic/mozapps/downloads/downloadButtons.png (../../windows/mozapps/downloads/downloadButtons.png) skin/classic/mozapps/downloads/unknownContentType.css (../../windows/mozapps/downloads/unknownContentType.css) -#ifdef MOZ_WEBEXTENSIONS - skin/classic/mozapps/extensions/about.css (../../windows/mozapps/webextensions/about.css) - skin/classic/mozapps/extensions/blocklist.css (../../windows/mozapps/webextensions/blocklist.css) - skin/classic/mozapps/extensions/update.css (../../windows/mozapps/webextensions/update.css) - skin/classic/mozapps/extensions/discover-logo.png (../../windows/mozapps/webextensions/discover-logo.png) - skin/classic/mozapps/extensions/experimentGeneric.png (../../windows/mozapps/webextensions/experimentGeneric.png) - skin/classic/mozapps/extensions/rating-won.png (../../windows/mozapps/webextensions/rating-won.png) - skin/classic/mozapps/extensions/rating-not-won.png (../../windows/mozapps/webextensions/rating-not-won.png) - skin/classic/mozapps/extensions/cancel.png (../../windows/mozapps/webextensions/cancel.png) - skin/classic/mozapps/extensions/eula.css (../../windows/mozapps/webextensions/eula.css) -#endif skin/classic/mozapps/handling/handling.css (../../windows/mozapps/handling/handling.css) skin/classic/mozapps/plugins/pluginBlocked-64.png (../../windows/mozapps/plugins/pluginBlocked-64.png) skin/classic/mozapps/plugins/pluginHelp-16.png (../../windows/mozapps/plugins/pluginHelp-16.png) skin/classic/mozapps/profile/profileSelection.css (../../windows/mozapps/profile/profileSelection.css) skin/classic/mozapps/update/downloadButtons.png (../../windows/mozapps/update/downloadButtons.png) -#ifdef MOZ_WEBEXTENSIONS -* skin/classic/mozapps/xpinstall/xpinstallConfirm.css (../../windows/mozapps/webextensions/xpinstallConfirm.css) -#endif #ifdef MOZ_PHOENIX [browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: #elif MOZ_SEPARATE_MANIFEST_FOR_THEME_OVERRIDES [extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: #endif -#ifdef MOZ_WEBEXTENSIONS -% override chrome://global/skin/arrow/arrow-lft-hov.gif chrome://global/skin/arrow/arrow-lft.gif -% override chrome://global/skin/arrow/arrow-rit-hov.gif chrome://global/skin/arrow/arrow-rit.gif -% override chrome://mozapps/skin/extensions/category-dictionaries.png chrome://mozapps/skin/extensions/dictionaryGeneric.png -% override chrome://mozapps/skin/extensions/category-experiments.png chrome://mozapps/skin/extensions/experimentGeneric.png -% override chrome://mozapps/skin/extensions/category-extensions.svg chrome://mozapps/skin/extensions/extensionGeneric.svg -% override chrome://mozapps/skin/extensions/category-languages.png chrome://mozapps/skin/extensions/localeGeneric.png -% override chrome://mozapps/skin/extensions/category-themes.png chrome://mozapps/skin/extensions/themeGeneric.png -% override chrome://mozapps/skin/plugins/notifyPluginGeneric.png chrome://mozapps/skin/plugins/pluginGeneric-16.png -% override chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png chrome://mozapps/skin/extensions/extensionGeneric.png -#endif % override chrome://mozapps/skin/plugins/notifyPluginCrashed.png chrome://mozapps/skin/plugins/pluginGeneric-16.png diff --git a/toolkit/themes/windows/global/arrow/arrow-lft-hov.gif b/toolkit/themes/windows/global/arrow/arrow-lft-hov.gif Binary files differnew file mode 100644 index 000000000..2c814ca65 --- /dev/null +++ b/toolkit/themes/windows/global/arrow/arrow-lft-hov.gif diff --git a/toolkit/themes/windows/global/arrow/arrow-rit-hov.gif b/toolkit/themes/windows/global/arrow/arrow-rit-hov.gif Binary files differnew file mode 100644 index 000000000..d0a401647 --- /dev/null +++ b/toolkit/themes/windows/global/arrow/arrow-rit-hov.gif diff --git a/toolkit/themes/windows/mozapps/jar.mn b/toolkit/themes/windows/mozapps/jar.mn index 0d77a4e79..9f7562995 100644 --- a/toolkit/themes/windows/mozapps/jar.mn +++ b/toolkit/themes/windows/mozapps/jar.mn @@ -6,23 +6,6 @@ toolkit.jar: #include ../../shared/mozapps.inc.mn skin/classic/mozapps/downloads/downloadIcon.png (downloads/downloadIcon.png) skin/classic/mozapps/downloads/downloads.css (downloads/downloads.css) -#ifdef MOZ_WEBEXTENSIONS -* skin/classic/mozapps/extensions/extensions.css (webextensions/extensions.css) - skin/classic/mozapps/extensions/category-search.png (webextensions/category-search.png) - skin/classic/mozapps/extensions/category-discover.png (webextensions/category-discover.png) - skin/classic/mozapps/extensions/category-plugins.png (webextensions/category-plugins.png) - skin/classic/mozapps/extensions/category-service.png (webextensions/category-service.png) - skin/classic/mozapps/extensions/category-recent.png (webextensions/category-recent.png) - skin/classic/mozapps/extensions/category-available.png (webextensions/category-available.png) - skin/classic/mozapps/extensions/extensionGeneric-16.png (webextensions/extensionGeneric-16.png) - skin/classic/mozapps/extensions/themeGeneric.png (webextensions/themeGeneric.png) - skin/classic/mozapps/extensions/themeGeneric-16.png (webextensions/themeGeneric-16.png) - skin/classic/mozapps/extensions/dictionaryGeneric.png (webextensions/dictionaryGeneric.png) - skin/classic/mozapps/extensions/dictionaryGeneric-16.png (webextensions/dictionaryGeneric-16.png) - skin/classic/mozapps/extensions/localeGeneric.png (webextensions/localeGeneric.png) - skin/classic/mozapps/extensions/heart.png (webextensions/heart.png) -* skin/classic/mozapps/extensions/newaddon.css (webextensions/newaddon.css) -#else skin/classic/mozapps/extensions/about.css (extensions/about.css) skin/classic/mozapps/extensions/blocklist.css (extensions/blocklist.css) * skin/classic/mozapps/extensions/extensions.css (extensions/extensions.css) @@ -67,7 +50,6 @@ toolkit.jar: skin/classic/mozapps/extensions/newaddon.css (extensions/newaddon.css) * skin/classic/mozapps/xpinstall/xpinstallConfirm.css (extensions/xpinstallConfirm.css) skin/classic/mozapps/xpinstall/xpinstallItemGeneric.png (extensions/extensionGeneric.png) -#endif skin/classic/mozapps/passwordmgr/key.png (passwordmgr/key.png) skin/classic/mozapps/passwordmgr/key-16.png (passwordmgr/key-16.png) skin/classic/mozapps/passwordmgr/key-64.png (passwordmgr/key-64.png) diff --git a/toolkit/themes/windows/mozapps/webextensions/about.css b/toolkit/themes/windows/mozapps/webextensions/about.css deleted file mode 100644 index 19eaddca8..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/about.css +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#genericAbout { - padding: 0px; - min-height: 200px; - max-height: 400px; - width: 30em; -} - -#clientBox { - background-color: -moz-Dialog; - color: -moz-DialogText; -} - -@media (-moz-windows-compositor) { - #genericAbout { - -moz-appearance: -moz-win-glass; - background: transparent; - } - - #clientBox { - -moz-appearance: -moz-win-exclude-glass; - } -} - - -.basic-info { - padding: 10px; -} - -#extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/extensionGeneric.svg"); - max-width: 64px; - max-height: 64px; - margin-inline-end: 6px; -} - -#genericAbout[addontype="theme"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/themeGeneric.png"); -} - -#genericAbout[addontype="locale"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/localeGeneric.png"); -} - -#genericAbout[addontype="plugin"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png"); -} - -#genericAbout[addontype="dictionary"] #extensionIcon { - list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png"); -} - -#extensionName { - font-size: 200%; - font-weight: bolder; -} - -#extensionVersion { - font-weight: bold; -} - -#extensionDescription { - margin-top: 4px; -} - -#groove { - margin-top: 8px; -} - -#extensionDetailsBox { - overflow: auto; - min-height: 100px; -} - -.boxIndent { - margin-inline-start: 18px; -} - -#extensionCreator, .contributor { - margin: 0px; -} - -.sectionTitle { - padding: 2px 0px 3px 0px; - margin-top: 3px; - font-weight: bold; -} - diff --git a/toolkit/themes/windows/mozapps/webextensions/blocklist.css b/toolkit/themes/windows/mozapps/webextensions/blocklist.css deleted file mode 100644 index 1cdbb35ac..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/blocklist.css +++ /dev/null @@ -1,20 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -richlistitem { - padding-top: 6px; - padding-bottom: 6px; - padding-inline-start: 7px; - padding-inline-end: 7px; - border-bottom: 1px solid #C0C0C0; -} - -.addonName { - font-weight: bold; -} - -.blockedLabel { - font-weight: bold; - font-style: italic; -} diff --git a/toolkit/themes/windows/mozapps/webextensions/cancel.png b/toolkit/themes/windows/mozapps/webextensions/cancel.png Binary files differdeleted file mode 100644 index 0d98ab235..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/cancel.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/category-available.png b/toolkit/themes/windows/mozapps/webextensions/category-available.png Binary files differdeleted file mode 100644 index 9341f2aa7..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/category-available.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/category-discover.png b/toolkit/themes/windows/mozapps/webextensions/category-discover.png Binary files differdeleted file mode 100644 index af954a613..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/category-discover.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/category-plugins.png b/toolkit/themes/windows/mozapps/webextensions/category-plugins.png Binary files differdeleted file mode 100644 index 100a90307..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/category-plugins.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/category-recent.png b/toolkit/themes/windows/mozapps/webextensions/category-recent.png Binary files differdeleted file mode 100644 index d65158646..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/category-recent.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/category-search.png b/toolkit/themes/windows/mozapps/webextensions/category-search.png Binary files differdeleted file mode 100644 index 52e91a7ce..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/category-search.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/category-service.png b/toolkit/themes/windows/mozapps/webextensions/category-service.png Binary files differdeleted file mode 100644 index 997c8541c..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/category-service.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/dictionaryGeneric-16.png b/toolkit/themes/windows/mozapps/webextensions/dictionaryGeneric-16.png Binary files differdeleted file mode 100644 index 37e2a5e4c..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/dictionaryGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/dictionaryGeneric.png b/toolkit/themes/windows/mozapps/webextensions/dictionaryGeneric.png Binary files differdeleted file mode 100644 index b26bb7100..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/dictionaryGeneric.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/discover-logo.png b/toolkit/themes/windows/mozapps/webextensions/discover-logo.png Binary files differdeleted file mode 100644 index cd50735a8..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/discover-logo.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/eula.css b/toolkit/themes/windows/mozapps/webextensions/eula.css deleted file mode 100644 index 05aeb3c1c..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/eula.css +++ /dev/null @@ -1,47 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#icon { - list-style-image: url("chrome://mozapps/skin/extensions/extensionGeneric.svg"); - max-width: 48px; - max-height: 48px; - margin-inline-end: 6px; -} - -#eula-dialog[addontype="theme"] #icon { - list-style-image: url("chrome://mozapps/skin/extensions/themeGeneric.png"); -} - -#eula-dialog[addontype="locale"] #icon { - list-style-image: url("chrome://mozapps/skin/extensions/localeGeneric.png"); -} - -#eula-dialog[addontype="plugin"] #icon { - list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png"); -} - -#eula-dialog[addontype="dictionary"] #icon { - list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png"); -} - -#heading-container { - -moz-box-align: center; -} - -#heading { - font-size: 120%; -} - -#eula { - -moz-appearance: none; - color: -moz-FieldText; - background-color: -moz-Field; - margin: 1em; - border: 1px solid; - -moz-border-top-colors: ActiveBorder; - -moz-border-right-colors: ActiveBorder; - -moz-border-bottom-colors: ActiveBorder; - -moz-border-left-colors: ActiveBorder; -} - diff --git a/toolkit/themes/windows/mozapps/webextensions/experimentGeneric.png b/toolkit/themes/windows/mozapps/webextensions/experimentGeneric.png Binary files differdeleted file mode 100644 index a9d00545e..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/experimentGeneric.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/extensionGeneric-16.png b/toolkit/themes/windows/mozapps/webextensions/extensionGeneric-16.png Binary files differdeleted file mode 100644 index 2724b9e7c..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/extensionGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/extensions.css b/toolkit/themes/windows/mozapps/webextensions/extensions.css deleted file mode 100644 index 7c4aed05e..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/extensions.css +++ /dev/null @@ -1,42 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -%include ../../../shared/webextensions/extensions.inc.css - -#header-utils-btn { - list-style-image: url("chrome://mozapps/skin/extensions/utilities.svg#utilities"); - margin-inline-end: 16px; -} - -@media not all and (-moz-windows-default-theme) { - #header-utils-btn { - list-style-image: url("chrome://mozapps/skin/extensions/utilities.svg#utilities-native"); - } -} - -.sorter[checkState="1"] { - list-style-image: url("chrome://global/skin/arrow/arrow-dn.gif"); -} - -.sorter[checkState="2"] { - list-style-image: url("chrome://global/skin/arrow/arrow-up.gif"); -} - -.addon .relnotes-toggle { - -moz-box-direction: reverse; - list-style-image: url("chrome://global/skin/arrow/arrow-dn.gif"); -} - -.addon[show-relnotes] .relnotes-toggle { - list-style-image: url("chrome://global/skin/arrow/arrow-up.gif"); -} - -.meta-rating > .star { - list-style-image: url("chrome://mozapps/skin/extensions/rating-not-won.png"); - padding: 0 1px; -} - -.meta-rating > .star[on="true"] { - list-style-image: url("chrome://mozapps/skin/extensions/rating-won.png"); -} diff --git a/toolkit/themes/windows/mozapps/webextensions/heart.png b/toolkit/themes/windows/mozapps/webextensions/heart.png Binary files differdeleted file mode 100644 index 655f4c4be..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/heart.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/localeGeneric.png b/toolkit/themes/windows/mozapps/webextensions/localeGeneric.png Binary files differdeleted file mode 100644 index 623ba3a6a..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/localeGeneric.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/newaddon.css b/toolkit/themes/windows/mozapps/webextensions/newaddon.css deleted file mode 100644 index 5856c08b5..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/newaddon.css +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -%include ../../../shared/webextensions/newaddon.inc.css diff --git a/toolkit/themes/windows/mozapps/webextensions/rating-not-won.png b/toolkit/themes/windows/mozapps/webextensions/rating-not-won.png Binary files differdeleted file mode 100644 index 2761f1925..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/rating-not-won.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/rating-won.png b/toolkit/themes/windows/mozapps/webextensions/rating-won.png Binary files differdeleted file mode 100644 index 336dd8f6e..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/rating-won.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/themeGeneric-16.png b/toolkit/themes/windows/mozapps/webextensions/themeGeneric-16.png Binary files differdeleted file mode 100644 index ff13ce37f..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/themeGeneric-16.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/themeGeneric.png b/toolkit/themes/windows/mozapps/webextensions/themeGeneric.png Binary files differdeleted file mode 100644 index 9cea549dd..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/themeGeneric.png +++ /dev/null diff --git a/toolkit/themes/windows/mozapps/webextensions/update.css b/toolkit/themes/windows/mozapps/webextensions/update.css deleted file mode 100644 index 0db179330..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/update.css +++ /dev/null @@ -1,28 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.throbber { - list-style-image: url("chrome://global/skin/icons/loading.png"); - width: 16px; - height: 16px; - margin-top: 5px; - margin-bottom: 5px; - margin-inline-start: 5px; - margin-inline-end: 2px; -} - -@media (min-resolution: 1.1dppx) { - .throbber { - list-style-image: url("chrome://global/skin/icons/loading@2x.png"); - } -} - -.alertBox { - background-color: InfoBackground; - color: InfoText; - border: 1px outset InfoBackground; - margin-left: 3px; - margin-right: 3px; - padding: 5px; -} diff --git a/toolkit/themes/windows/mozapps/webextensions/xpinstallConfirm.css b/toolkit/themes/windows/mozapps/webextensions/xpinstallConfirm.css deleted file mode 100644 index 42db4cd4d..000000000 --- a/toolkit/themes/windows/mozapps/webextensions/xpinstallConfirm.css +++ /dev/null @@ -1,101 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#xpinstallheader { - margin-bottom: 2em; -} - -#itemList { - -moz-appearance: listbox; - margin: 3px 4px 10px 4px; - height: 14em; - border: 2px solid; - -moz-border-top-colors: ThreeDShadow ThreeDDarkShadow; - -moz-border-right-colors: ThreeDHighlight ThreeDLightShadow; - -moz-border-bottom-colors: ThreeDHighlight ThreeDLightShadow; - -moz-border-left-colors: ThreeDShadow ThreeDDarkShadow; - background-color: -moz-Field; - color: -moz-FieldText; -} - -#itemWarningIntro { - margin-inline-start: 8px; -} - -#dialogContentBox { - padding: 5px; -} - -installitem { - padding-top: 5px; - padding-bottom: 5px; - padding-inline-start: 5px; - padding-inline-end: 0; - border-bottom: 1px dotted #C0C0C0; - margin-bottom: 5px; -} - -.alert-icon { -%ifdef XP_WIN - list-style-image: url("chrome://global/skin/icons/warning-large.png"); - width: 48px; - height: 48px; -%endif - margin-inline-end: 20px; -} - -.warning { - font-weight: bold; - font-size: 1.25em; - margin-bottom: 1em; -} - -.xpinstallIconContainer { - width: 32px; - height: 32px; - margin-inline-end: 5px; -} - -.xpinstallItemName { - font-weight: bold; -} - -.xpinstallItemSigned { - font-style: italic; - font-size: 0.9em; -} - -.xpinstallItemURL { - -moz-appearance: none; - border: none; - padding: 0; - background-color: -moz-Field; - color: -moz-FieldText; - margin-top: 1px; - margin-bottom: 1px; - margin-inline-start: 6px; - margin-inline-end: 5px; -} - -.xpinstallItemIcon { - max-width: 32px; - max-height: 32px; - list-style-image: url("chrome://mozapps/skin/extensions/extensionGeneric.svg"); -} - -installitem[type="theme"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/extensions/themeGeneric.png"); -} - -installitem[type="locale"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/extensions/localeGeneric.png"); -} - -installitem[type="plugin"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png"); -} - -installitem[type="dictionary"] .xpinstallItemIcon { - list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png"); -} diff --git a/toolkit/xre/nsUpdateDriver.cpp b/toolkit/xre/nsUpdateDriver.cpp index ab0bdf005..aac856d6e 100644 --- a/toolkit/xre/nsUpdateDriver.cpp +++ b/toolkit/xre/nsUpdateDriver.cpp @@ -212,10 +212,8 @@ GetStatusFileContents(nsIFile *statusFile, char (&buf)[Size]) typedef enum { eNoUpdateAction, ePendingUpdate, - ePendingService, ePendingElevate, eAppliedUpdate, - eAppliedService, } UpdateStatus; /** @@ -233,22 +231,14 @@ GetUpdateStatus(nsIFile* dir, nsCOMPtr<nsIFile> &statusFile) char buf[32]; if (GetStatusFileContents(statusFile, buf)) { const char kPending[] = "pending"; - const char kPendingService[] = "pending-service"; const char kPendingElevate[] = "pending-elevate"; const char kApplied[] = "applied"; - const char kAppliedService[] = "applied-service"; if (!strncmp(buf, kPendingElevate, sizeof(kPendingElevate) - 1)) { return ePendingElevate; } - if (!strncmp(buf, kPendingService, sizeof(kPendingService) - 1)) { - return ePendingService; - } if (!strncmp(buf, kPending, sizeof(kPending) - 1)) { return ePendingUpdate; } - if (!strncmp(buf, kAppliedService, sizeof(kAppliedService) - 1)) { - return eAppliedService; - } if (!strncmp(buf, kApplied, sizeof(kApplied) - 1)) { return eAppliedUpdate; } @@ -760,9 +750,6 @@ ApplyUpdate(nsIFile *greDir, nsIFile *updateDir, nsIFile *statusFile, // We used to write out "Applying" to the update.status file here. // Instead we do this from within the updater application now. - // This is so that we don't overwrite the status of pending-service - // in the Windows case. This change was made for all platforms so - // that it stays consistent across all OS. // On platforms where we are not calling execv, we may need to make the // updater executable wait for the calling process to exit. Otherwise, the @@ -969,17 +956,15 @@ ProcessUpdates(nsIFile *greDir, nsIFile *appDir, nsIFile *updRootDir, } break; } - // Intentional fallthrough to ePendingUpdate and ePendingService. + // Intentional fallthrough to ePendingUpdate. MOZ_FALLTHROUGH; } - case ePendingUpdate: - case ePendingService: { + case ePendingUpdate: { ApplyUpdate(greDir, updatesDir, statusFile, appDir, argc, argv, restart, isOSUpdate, osApplyToDir, pid); break; } case eAppliedUpdate: - case eAppliedService: // An update was staged and needs to be switched so the updated application // is used. SwitchToUpdatedApp(greDir, updatesDir, appDir, argc, argv); |